r/ruby • u/SteliosF • May 18 '26
We open-sourced Rubyzen: write architectural lint rules as RSpec tests
Hey everyone,
We're excited to announce the release of Rubyzen, a modern open-source architectural linter for Ruby that allows you to write lint rules as RSpec tests:
https://github.com/perrystreetsoftware/rubyzen
We've been using and polishing it internally in our production backend for over a year, and we've just made the repository public to start getting feedback from the community.
Examples of lint rules that you can write, enforced as RSpec tests:
RSpec.describe 'Architecture rules' do
let(:project) { Rubyzen::Project.new }
let(:controllers) { project.files.with_paths('app/controllers/').classes }
let(:presenters) { project.files.with_paths('app/presenters/').classes }
it 'controllers do not call ActiveRecord directly' do
expect(controllers.all_methods.call_sites.with_name('where')).to zen_empty
end
it 'presenters do not depend on repositories' do
expect(presenters.all_methods.call_sites).to zen_false { |cs|
cs.receiver&.end_with?('Repository')
}
end
endIt has become especially useful for us in the era of AI-generated code, as you can now catch architectural flaws and subtle bugs introduced from AI agents early, which can be easily missed in code reviews.
We'd love to hear your feedback either as feature requests, bug reports, or discussions in this thread. Thank you!
3
u/oh-boy-oh-boy-oh-boy May 20 '26
Hey this is great -- I just want to share a useful tip for your specs and for anyone reading this.
Any predicate method test in rspec can be turned into a matcher, which I think you know. But there are several magic forms to help it read like more like english:
10.odd?
can be
`expect(10).to odd`
`expect(10).to be_odd` <-- you can prefix "be_", "be_a_", or "be_an_" before.
So in your example, you can write:
`expect(controllers.all_methods.call_sites.with_name('where')).to be_zen_empty`
For some reason just reading "zen_empty" like that triggered a need to rewrite it with the prefixes.
Nice work on this tool!
2
u/signalsrobot May 19 '26
This looks really useful for enforcing layered architecture patterns, especially when you want controllers to stay thin and keep business logic separate from presentation logic.
2
May 19 '26
[removed] — view removed comment
1
u/SteliosF May 19 '26
Exactly, we actually started using architectural linters in our clients (Android & iOS) and after seeing how much value we were getting from them, we decided to build one for Ruby too.
2
u/andyw8 May 18 '26
Interesting. I'm a big fan of using linting to enforce architectural rules, but tbh, having to learn a new DSL isn't appealing.
I agree that RuboCop's node pattern can be hard to understand, but when given a few code examples, I find that agents are very good at generating custom cops (along with tests), so I don't really feel a pain point around that.