Module: ArchSpec::Architectures

Extended by:
Architectures
Included in:
Architectures
Defined in:
lib/archspec/architectures.rb

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
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

Instance Method Details

#apply(name, dsl, **options) ⇒ Object



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/archspec/architectures.rb', line 68

def apply(name, dsl, **options)
  case name.to_sym
  when :rails, :rails_mvc, :rails_way
    rails_mvc(dsl, components: options.fetch(:components, DEFAULT_RAILS_MVC))
  when :rails_strict
    rails_strict(dsl, components: options.fetch(:components, DEFAULT_RAILS_MVC))
  when :vanilla_rails
    vanilla_rails(
      dsl,
      components: options.fetch(:components, DEFAULT_RAILS_MVC),
      empty: options.fetch(:empty, VANILLA_RAILS_EMPTY)
    )
  when :layered, :rails_layered
    layered(dsl, layers: options.fetch(:layers, DEFAULT_LAYERED))
  when :hexagonal, :rails_hexagonal
    hexagonal(dsl, **with_defaults(DEFAULT_HEXAGONAL, options))
  when :clean, :rails_clean
    clean(dsl, **with_defaults(DEFAULT_CLEAN, options))
  when :modular_monolith, :bounded_contexts
    modular_monolith(dsl, components: options.fetch(:components), allow: options.fetch(:allow, {}))
  when :cqrs, :rails_cqrs
    cqrs(dsl, **with_defaults(DEFAULT_CQRS, options))
  when :event_driven, :rails_event_driven
    event_driven(dsl, **with_defaults(DEFAULT_EVENT_DRIVEN, options))
  else
    raise Error, "Unknown ArchSpec architecture: #{name.inspect}"
  end
end

#clean(dsl, frameworks:, interface_adapters:, use_cases:, entities:) ⇒ Object



152
153
154
155
156
157
158
159
160
161
162
# File 'lib/archspec/architectures.rb', line 152

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



176
177
178
179
180
181
182
183
184
185
# File 'lib/archspec/architectures.rb', line 176

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



187
188
189
190
191
192
193
194
195
# File 'lib/archspec/architectures.rb', line 187

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



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/archspec/architectures.rb', line 136

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



123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/archspec/architectures.rb', line 123

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: {}) ⇒ Object



164
165
166
167
168
169
170
171
172
173
174
# File 'lib/archspec/architectures.rb', line 164

def modular_monolith(dsl, components:, allow: {})
  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)
  end

  dsl.no_cycles!(among: components.keys)
end

#rails_mvc(dsl, components:) ⇒ Object



97
98
99
100
101
102
103
104
105
106
# File 'lib/archspec/architectures.rb', line 97

def rails_mvc(dsl, components:)
  components = normalize_map(components)
  define_components(dsl, components)

  proxy_for(dsl, :controllers).can_use(*components.keys & %i[models services helpers mailers jobs])
  proxy_for(dsl, :models).cannot_use(*components.keys & %i[controllers helpers])
  proxy_for(dsl, :services).cannot_use(*components.keys & %i[controllers helpers])
  proxy_for(dsl, :models).cannot_call(*CONTROLLER_METHODS)
  proxy_for(dsl, :services).cannot_call(*CONTROLLER_METHODS)
end

#rails_strict(dsl, components:) ⇒ Object



108
109
110
111
112
113
# File 'lib/archspec/architectures.rb', line 108

def rails_strict(dsl, components:)
  components = normalize_map(components)
  rails_mvc(dsl, components: components)
  dsl.verify_zeitwerk_names!
  dsl.no_cycles!(among: components.keys)
end

#vanilla_rails(dsl, components:, empty:) ⇒ Object



115
116
117
118
119
120
121
# File 'lib/archspec/architectures.rb', line 115

def vanilla_rails(dsl, components:, empty:)
  rails_mvc(dsl, components: components)

  empty.each do |name, (pattern, reason)|
    dsl.component(name, in: pattern).must_be_empty(because: reason)
  end
end