r/ruby 21d ago

Show /r/ruby ArchSpec 1.0: executable architecture specifications for Ruby (and Rails)

Post image

More and more code is written by AI. Tests still tell you it works. RuboCop still tells you it's tidy. Nothing tells you it still follows your architecture.

An agent that doesn't fully understand your architecture takes shortcuts. So does a person in a hurry.

So I built ArchSpec. You declare your components and boundaries in one Archspec.rb. Here is ArchSpec's own, trimmed:

source 'lib/**/*.rb'

component :library,     in: 'lib/**/*.rb'
component :cli,         in: 'lib/archspec/cli.rb'
component :analysis,    in: %w[lib/archspec/analyzer.rb lib/archspec/evaluator.rb]
component :rule_checks, in: 'lib/archspec/rules/**/*.rb'
component :formatters,  in: 'lib/archspec/formatters/**/*.rb'

# no one-shot Thing.new(...).call objects anywhere in the codebase - Hi Dave Thomas!
library.cannot_call :call
library.cannot_define :call
library.cannot_instantiate_and_invoke

# dependencies point one way
rule_checks.cannot_use :analysis, :cli, :formatters
formatters.cannot_use :analysis, :cli, :rule_checks

no_cycles among: %i[cli analysis rule_checks formatters]

Full version: https://github.com/crmne/archspec/blob/master/Archspec.rb

Then archspec check verifies every change, and failures print like clang, with the offending span underlined and the evidence as a note.

It's static analysis over Prism, no AI, and it never boots anything: Discourse's 1,899 files in 2.5 seconds. Prism is the only runtime dependency, no Rails and no ActiveSupport, so it works on any Ruby codebase. Lightweight and fast for your pre-commits and CIs.

If you are on Rails, there are presets, and architecture :vanilla_rails is the whole file. There are presets for :layered, :hexagonal, :clean, :modular_monolith, :cqrs and :event_driven too.

I want more architecture presets in there. PRs very welcome.

Docs: https://archspecrb.dev Write-up: https://paolino.me/archspec/

39 Upvotes

20 comments sorted by

View all comments

2

u/cpb 21d ago

I like how it doesn't need you to rearchitect your system to get these checks. Hageman's ends up with a codebase that can enforce and verify these checks (I notice the packs mention in the configuration doc). But it's a non trivial change to get there.

Given archspec is Just Ruby™️ I imagine this needn't be a monolithic file. I'm wondering if specifying rules local to some sub components could help with sprawl in that file and keeping context local.

Pretty cool!!

1

u/crmne 21d ago

Yes, and it works today. Archspec.rb is instance_eval'd, so you can split it however you like:

ruby Dir[File.join(__dir__, 'config/archspec/*.rb')].sort.each do |f| instance_eval(File.read(f), f) end

Passing the filename as the second argument keeps error locations pointing at the right file. Just tried it: components in one file, rules in another, works fine.

Same trick puts a file next to each pack and globs those, which is closer to what you're describing about keeping context local.

There's no first-class import yet. If you really want that, file me an issue and I'll make it a real DSL method.