Module: ArchSpec::Architectures
Overview
Bundled architecture presets. Each applies a set of components and rules in one call, invoked from the DSL through ArchSpec::DSL::Context#architecture:
architecture :rails
architecture :layered, layers: { ... }
Every preset accepts overrides for its directories, so you can keep the shape while pointing at your own paths. The presets are:
:rails (aliases :rails_mvc, :rails_way):: Conventional MVC. Keeps
controller APIs out of models and services. Options: components:,
controller_api:, share_helpers:.
:rails_strict:: :rails plus Zeitwerk name checks, a cycle check, and a
concern independence check. Options add concerns:.
:vanilla_rails:: :rails plus empty-directory rules for the 37signals
style, forbidding app/services, app/forms, app/policies, and more,
and the concern independence check. Options: components:, empty:,
controller_api:, share_helpers:, concerns:.
:layered (alias :rails_layered):: Ordered layers that may only depend
inward, with a cycle check. Option: layers: (order matters).
:hexagonal (alias :rails_hexagonal):: Ports and adapters, keeping the
domain away from adapters. Options: application:, domain:, ports:,
adapters:.
:clean (alias :rails_clean):: Clean architecture layers. Options:
+frameworks:+, +interface_adapters:+, +use_cases:+, +entities:+.
:modular_monolith (alias :bounded_contexts):: Named packages with
per-package allowlists and optional public APIs. Options: components:
(required), allow:, public:.
:cqrs (alias :rails_cqrs):: Separates commands from queries and keeps
writes out of queries. Options: commands:, queries:, read_models:,
mutating_methods:.
:event_driven (alias :rails_event_driven):: Events, publishers, and
subscribers. Options: events:, publishers:, subscribers:.
See the guides at https://archspecrb.dev/architectures/ for each in depth.
Constant Summary collapse
- DEFAULT_LAYERED =
{ interface: 'app/controllers/**/*.rb', application: %w[app/services/**/*.rb app/jobs/**/*.rb app/mailers/**/*.rb], domain: 'app/models/**/*.rb' }.freeze
- DEFAULT_RAILS_MVC =
{ controllers: 'app/controllers/**/*.rb', models: 'app/models/**/*.rb', helpers: 'app/helpers/**/*.rb', mailers: 'app/mailers/**/*.rb', jobs: 'app/jobs/**/*.rb', services: 'app/services/**/*.rb' }.freeze
- DEFAULT_HEXAGONAL =
{ application: %w[app/services/**/*.rb app/use_cases/**/*.rb], domain: 'app/domain/**/*.rb', ports: 'app/ports/**/*.rb', adapters: %w[app/adapters/**/*.rb app/integrations/**/*.rb app/infrastructure/**/*.rb] }.freeze
- DEFAULT_CLEAN =
{ frameworks: %w[app/controllers/**/*.rb app/jobs/**/*.rb app/mailers/**/*.rb], interface_adapters: %w[app/adapters/**/*.rb app/presenters/**/*.rb app/serializers/**/*.rb], use_cases: %w[app/use_cases/**/*.rb app/services/**/*.rb], entities: %w[app/entities/**/*.rb app/domain/**/*.rb app/models/**/*.rb] }.freeze
- DEFAULT_CQRS =
{ commands: 'app/commands/**/*.rb', queries: 'app/queries/**/*.rb', read_models: 'app/read_models/**/*.rb' }.freeze
- DEFAULT_EVENT_DRIVEN =
{ events: 'app/events/**/*.rb', publishers: 'app/publishers/**/*.rb', subscribers: 'app/subscribers/**/*.rb' }.freeze
- VANILLA_RAILS_EMPTY =
{ services: ['app/services/**/*.rb', 'behavior belongs on models, not service objects'], forms: ['app/forms/**/*.rb', 'use strong parameters and model validations'], policies: ['app/policies/**/*.rb', 'authorization is predicate methods on models'], decorators: ['app/decorators/**/*.rb', 'use helpers and ERB partials'], presenters: ['app/presenters/**/*.rb', 'presentation objects are POROs in app/models'], view_components: ['app/components/**/*.rb', 'use helpers and ERB partials'] }.freeze
- DEFAULT_CONCERNS =
'app/**/concerns/**/*.rb'- CONTROLLER_METHODS =
%i[render redirect_to params session cookies flash].freeze
- MUTATING_METHODS =
%i[ create create! delete delete_all destroy destroy! insert insert! save save! update update! update_attribute update_attributes update_columns upsert upsert! ].freeze
Instance Method Summary collapse
-
#apply(name, dsl, **options) ⇒ Object
Applies the named preset to
dsl, forwardingoptionsto it. - #clean(dsl, frameworks:, interface_adapters:, use_cases:, entities:) ⇒ Object
- #cqrs(dsl, commands:, queries:, read_models: nil, mutating_methods: MUTATING_METHODS) ⇒ Object
- #event_driven(dsl, events:, publishers:, subscribers:) ⇒ Object
- #hexagonal(dsl, application:, domain:, ports:, adapters:) ⇒ Object
- #layered(dsl, layers:) ⇒ Object
- #modular_monolith(dsl, components:, allow: {}, public: {}) ⇒ Object
- #rails_mvc(dsl, components:, controller_api: CONTROLLER_METHODS, share_helpers: false) ⇒ Object
- #rails_strict(dsl, components:, controller_api: CONTROLLER_METHODS, share_helpers: false, concerns: DEFAULT_CONCERNS) ⇒ Object
- #vanilla_rails(dsl, components:, empty:, controller_api: CONTROLLER_METHODS, share_helpers: false, concerns: DEFAULT_CONCERNS) ⇒ Object
Instance Method Details
#apply(name, dsl, **options) ⇒ Object
Applies the named preset to dsl, forwarding options to it. Raises
ArchSpec::Error for an unknown name. Called by
ArchSpec::DSL::Context#architecture, so you rarely call it directly.
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
# File 'lib/archspec/architectures.rb', line 109 def apply(name, dsl, **) case name.to_sym when :rails, :rails_mvc, :rails_way rails_mvc( dsl, components: .fetch(:components, DEFAULT_RAILS_MVC), controller_api: .fetch(:controller_api, CONTROLLER_METHODS), share_helpers: .fetch(:share_helpers, false) ) when :rails_strict rails_strict( dsl, components: .fetch(:components, DEFAULT_RAILS_MVC), controller_api: .fetch(:controller_api, CONTROLLER_METHODS), share_helpers: .fetch(:share_helpers, false), concerns: .fetch(:concerns, DEFAULT_CONCERNS) ) when :vanilla_rails vanilla_rails( dsl, components: .fetch(:components, DEFAULT_RAILS_MVC), empty: .fetch(:empty, VANILLA_RAILS_EMPTY), controller_api: .fetch(:controller_api, CONTROLLER_METHODS), share_helpers: .fetch(:share_helpers, false), concerns: .fetch(:concerns, DEFAULT_CONCERNS) ) when :layered, :rails_layered layered(dsl, layers: .fetch(:layers, DEFAULT_LAYERED)) when :hexagonal, :rails_hexagonal hexagonal(dsl, **with_defaults(DEFAULT_HEXAGONAL, )) when :clean, :rails_clean clean(dsl, **with_defaults(DEFAULT_CLEAN, )) when :modular_monolith, :bounded_contexts modular_monolith( dsl, components: .fetch(:components), allow: .fetch(:allow, {}), public: .fetch(:public, {}) ) when :cqrs, :rails_cqrs cqrs(dsl, **with_defaults(DEFAULT_CQRS, )) when :event_driven, :rails_event_driven event_driven(dsl, **with_defaults(DEFAULT_EVENT_DRIVEN, )) else raise Error, "Unknown ArchSpec architecture: #{name.inspect}" end end |
#clean(dsl, frameworks:, interface_adapters:, use_cases:, entities:) ⇒ Object
220 221 222 223 224 225 226 227 228 229 230 |
# File 'lib/archspec/architectures.rb', line 220 def clean(dsl, frameworks:, interface_adapters:, use_cases:, entities:) layered( dsl, layers: { frameworks: frameworks, interface_adapters: interface_adapters, use_cases: use_cases, entities: entities } ) end |
#cqrs(dsl, commands:, queries:, read_models: nil, mutating_methods: MUTATING_METHODS) ⇒ Object
247 248 249 250 251 252 253 254 255 256 |
# File 'lib/archspec/architectures.rb', line 247 def cqrs(dsl, commands:, queries:, read_models: nil, mutating_methods: MUTATING_METHODS) components = normalize_map(commands: commands, queries: queries) components[:read_models] = read_models if read_models define_components(dsl, components) proxy_for(dsl, :commands).cannot_use :queries proxy_for(dsl, :queries).cannot_use :commands proxy_for(dsl, :queries).cannot_call(*mutating_methods) dsl.no_cycles!(among: components.keys) end |
#event_driven(dsl, events:, publishers:, subscribers:) ⇒ Object
258 259 260 261 262 263 264 265 266 |
# File 'lib/archspec/architectures.rb', line 258 def event_driven(dsl, events:, publishers:, subscribers:) roles = normalize_map(events: events, publishers: publishers, subscribers: subscribers) define_components(dsl, roles) proxy_for(dsl, :events).cannot_use :publishers, :subscribers proxy_for(dsl, :publishers).can_use :events proxy_for(dsl, :subscribers).can_use :events dsl.no_cycles!(among: roles.keys) end |
#hexagonal(dsl, application:, domain:, ports:, adapters:) ⇒ Object
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 |
# File 'lib/archspec/architectures.rb', line 204 def hexagonal(dsl, application:, domain:, ports:, adapters:) roles = normalize_map( application: application, domain: domain, ports: ports, adapters: adapters ) define_components(dsl, roles) proxy_for(dsl, :application).can_use :domain, :ports proxy_for(dsl, :domain).cannot_use :adapters proxy_for(dsl, :ports).cannot_use :adapters proxy_for(dsl, :adapters).can_use :application, :domain, :ports dsl.no_cycles!(among: roles.keys) end |
#layered(dsl, layers:) ⇒ Object
191 192 193 194 195 196 197 198 199 200 201 202 |
# File 'lib/archspec/architectures.rb', line 191 def layered(dsl, layers:) ordered = normalize_map(layers) define_components(dsl, ordered) names = ordered.keys names.each_with_index do |name, index| allowed = names[(index + 1)..] || [] proxy_for(dsl, name).can_use(*allowed) end dsl.no_cycles!(among: names) end |
#modular_monolith(dsl, components:, allow: {}, public: {}) ⇒ Object
232 233 234 235 236 237 238 239 240 241 242 243 244 245 |
# File 'lib/archspec/architectures.rb', line 232 def modular_monolith(dsl, components:, allow: {}, public: {}) components = normalize_map(components) define_components(dsl, components) components.each_key do |name| allowed = Array(allow[name] || allow[name.to_s]) proxy_for(dsl, name).can_use(*allowed) patterns = Array(public[name] || public[name.to_s]) proxy_for(dsl, name).public_api(*patterns) if patterns.any? end dsl.no_cycles!(among: components.keys) end |
#rails_mvc(dsl, components:, controller_api: CONTROLLER_METHODS, share_helpers: false) ⇒ Object
157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
# File 'lib/archspec/architectures.rb', line 157 def rails_mvc(dsl, components:, controller_api: CONTROLLER_METHODS, share_helpers: false) components = normalize_map(components) define_components(dsl, components) forbidden = share_helpers ? %i[controllers] : %i[controllers helpers] proxy_for(dsl, :controllers).can_use(*components.keys & %i[models services helpers mailers jobs]) proxy_for(dsl, :models).cannot_use(*components.keys & forbidden) proxy_for(dsl, :services).cannot_use(*components.keys & forbidden) return if controller_api.empty? proxy_for(dsl, :models).cannot_call(*controller_api, receiver: :none) proxy_for(dsl, :services).cannot_call(*controller_api, receiver: :none) end |
#rails_strict(dsl, components:, controller_api: CONTROLLER_METHODS, share_helpers: false, concerns: DEFAULT_CONCERNS) ⇒ Object
172 173 174 175 176 177 178 |
# File 'lib/archspec/architectures.rb', line 172 def rails_strict(dsl, components:, controller_api: CONTROLLER_METHODS, share_helpers: false, concerns: DEFAULT_CONCERNS) components = normalize_map(components) rails_mvc(dsl, components: components, controller_api: controller_api, share_helpers: share_helpers) dsl.verify_zeitwerk_names! dsl.no_cycles!(among: components.keys) independent_concerns(dsl, concerns) end |
#vanilla_rails(dsl, components:, empty:, controller_api: CONTROLLER_METHODS, share_helpers: false, concerns: DEFAULT_CONCERNS) ⇒ Object
180 181 182 183 184 185 186 187 188 189 |
# File 'lib/archspec/architectures.rb', line 180 def vanilla_rails(dsl, components:, empty:, controller_api: CONTROLLER_METHODS, share_helpers: false, concerns: DEFAULT_CONCERNS) rails_mvc(dsl, components: components, controller_api: controller_api, share_helpers: share_helpers) empty.each do |name, (pattern, reason)| dsl.component(name, in: pattern).must_be_empty(because: reason) end independent_concerns(dsl, concerns) end |