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: conventional MVC that keeps controller APIs out of models and services. Optionscomponents:,controller_api:,share_helpers:.:rails_strict::railsplus a cycle check and a concern independence check. Adds optionconcerns:.:vanilla_rails::railsplus empty-directory rules for the 37signals style (forbiddingapp/services,app/forms,app/policies, and more) and the concern independence check. Optionscomponents:,empty:,controller_api:,share_helpers:,concerns:.:layered: ordered layers that may only depend inward, with a cycle check. Optionlayers:(order matters).:hexagonal: ports and adapters, keeping the domain away from adapters. Optionsapplication:,domain:,ports:,adapters:.:clean: clean architecture layers. Optionsframeworks:,interface_adapters:,use_cases:,entities:.:modular_monolith: named packages with per-package allowlists and optional public APIs. Optionscomponents:(required),allow:,public:.:cqrs: separates commands from queries and keeps writes out of queries. Optionscommands:,queries:,read_models:,mutating_methods:.:event_driven: events, publishers, and subscribers. Optionsevents:,publishers:,subscribers:.:ruby_conventions: generic Ruby naming idioms (no +get_+/+set_+, nois_prefix), applied project-wide. Adds no components, so it composes with any other architecture. No options.
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
- DEFAULTS =
Every option each architecture accepts, with its default. The single source of truth for #apply: option validation checks these keys, and the architecture methods receive these values merged with the caller's.
{ rails: { components: DEFAULT_RAILS_MVC, controller_api: CONTROLLER_METHODS, share_helpers: false }, rails_strict: { components: DEFAULT_RAILS_MVC, controller_api: CONTROLLER_METHODS, share_helpers: false, concerns: DEFAULT_CONCERNS }, vanilla_rails: { components: DEFAULT_RAILS_MVC, empty: VANILLA_RAILS_EMPTY, controller_api: CONTROLLER_METHODS, share_helpers: false, concerns: DEFAULT_CONCERNS }, layered: { layers: DEFAULT_LAYERED }, hexagonal: DEFAULT_HEXAGONAL, clean: DEFAULT_CLEAN, modular_monolith: { components: nil, allow: {}, public: {} }, cqrs: DEFAULT_CQRS.merge(mutating_methods: MUTATING_METHODS), event_driven: DEFAULT_EVENT_DRIVEN, ruby_conventions: {} }.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:, 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(dsl, components:, controller_api:, share_helpers:) ⇒ Object
- #rails_strict(dsl, components:, controller_api:, share_helpers:, concerns:) ⇒ Object
-
#ruby_conventions(dsl) ⇒ Object
Applies the generic Ruby naming idioms project-wide: no +get_+/+set_+ accessors and no
is_predicate prefix. - #vanilla_rails(dsl, components:, empty:, controller_api:, share_helpers:, 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.
140 141 142 143 144 145 146 147 |
# File 'lib/archspec/architectures.rb', line 140 def apply(name, dsl, **) name = architecture_name(name) defaults = DEFAULTS[name] raise Error, "unknown architecture: #{name.inspect}" unless defaults (name, defaults, ) send(name, dsl, **defaults.merge()) end |
#clean(dsl, frameworks:, interface_adapters:, use_cases:, entities:) ⇒ Object
214 215 216 217 218 219 220 221 222 223 224 |
# File 'lib/archspec/architectures.rb', line 214 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:, mutating_methods:) ⇒ Object
243 244 245 246 247 248 249 250 251 252 |
# File 'lib/archspec/architectures.rb', line 243 def cqrs(dsl, commands:, queries:, read_models:, 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
254 255 256 257 258 259 260 261 262 |
# File 'lib/archspec/architectures.rb', line 254 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_only_use :events proxy_for(dsl, :subscribers).can_only_use :events dsl.no_cycles(among: roles.keys) end |
#hexagonal(dsl, application:, domain:, ports:, adapters:) ⇒ Object
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 |
# File 'lib/archspec/architectures.rb', line 198 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_only_use :domain, :ports proxy_for(dsl, :domain).cannot_use :adapters proxy_for(dsl, :ports).cannot_use :adapters proxy_for(dsl, :adapters).can_only_use :application, :domain, :ports dsl.no_cycles(among: roles.keys) end |
#layered(dsl, layers:) ⇒ Object
185 186 187 188 189 190 191 192 193 194 195 196 |
# File 'lib/archspec/architectures.rb', line 185 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_only_use(*allowed) end dsl.no_cycles(among: names) end |
#modular_monolith(dsl, components:, allow: {}, public: {}) ⇒ Object
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 |
# File 'lib/archspec/architectures.rb', line 226 def modular_monolith(dsl, components:, allow: {}, public: {}) raise Error, 'architecture :modular_monolith requires the components: option' unless components 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_only_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(dsl, components:, controller_api:, share_helpers:) ⇒ Object
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/archspec/architectures.rb', line 149 def rails(dsl, components:, controller_api:, share_helpers:) components = normalize_map(components) missing = %i[controllers models] - components.keys if missing.any? raise Error, "the rails architectures need controllers and models components, missing: #{missing.join(', ')}" end define_components(dsl, components) forbidden = (share_helpers ? %i[controllers] : %i[controllers helpers]) & components.keys proxy_for(dsl, :controllers).can_only_use(*components.keys & %i[models services helpers mailers jobs]) (%i[models services] & components.keys).each do |name| proxy = proxy_for(dsl, name) proxy.cannot_use(*forbidden) proxy.cannot_call(*controller_api, receiver: :none) unless controller_api.empty? end end |
#rails_strict(dsl, components:, controller_api:, share_helpers:, concerns:) ⇒ Object
168 169 170 171 172 173 |
# File 'lib/archspec/architectures.rb', line 168 def rails_strict(dsl, components:, controller_api:, share_helpers:, concerns:) components = normalize_map(components) rails(dsl, components: components, controller_api: controller_api, share_helpers: share_helpers) dsl.no_cycles(among: components.keys) independent_concerns(dsl, concerns) end |
#ruby_conventions(dsl) ⇒ Object
Applies the generic Ruby naming idioms project-wide: no +get_+/+set_+
accessors and no is_ predicate prefix. Adds no components, so it composes
with any other architecture. Project-specific conventions (the with_x /
without_x pairing, the supports_*? ban) stay opt-in through the
method_names.matching(...) primitives.
269 270 271 272 273 274 |
# File 'lib/archspec/architectures.rb', line 269 def ruby_conventions(dsl) %i[instance class].each do |scope| forbid_name(dsl, /\A(get|set)_/, 'use attr_ readers and writers or plain names, not get_/set_', scope: scope) forbid_name(dsl, /\Ais_/, 'name predicates with a trailing ? and no is_ prefix (has_ is fine)', scope: scope) end end |
#vanilla_rails(dsl, components:, empty:, controller_api:, share_helpers:, concerns:) ⇒ Object
175 176 177 178 179 180 181 182 183 |
# File 'lib/archspec/architectures.rb', line 175 def vanilla_rails(dsl, components:, empty:, controller_api:, share_helpers:, concerns:) rails(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 |