rage_arch 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 152d8ad4517ebc11b70103f2dadbc68d14363d66e93c83ed8513755b7f4d3faa
4
- data.tar.gz: 5c15a711f1676d67644a75a66f7a7731504f56bc76ef75d1a2f420f4d900dae6
3
+ metadata.gz: 5977f7f0cda9c29591b5fada240de2a007facc8c3922e8f2a9c6a5ae08ed1f5b
4
+ data.tar.gz: 05b8e5a3a44cb57099c64b33c9cffba6a0abf80dc6256c41eabe87e86d48378e
5
5
  SHA512:
6
- metadata.gz: bb854fce9230a8e3dc7efbec5257ca1ea9f98d91bb417262d4fe3d3a014d1e47df337acffb93eea453f438d2c525f92c06d003b1e2eabc63f99d95af3a10e3bf
7
- data.tar.gz: b350e0aa5995ac0b63a30a8ade4ab78314b1755f56884998622434b585fa1f5b7bf16ade394666706b6569561ea256c1be74386f8b711232ab73c16dd8982947
6
+ metadata.gz: 25f7ef50af078a7b61663d26fd8586c5d512709b116ca206266c8d04e1d364c19caf5bf131c0886d0061fc7b1db0e7de3c47f2a827360f2a03b03d911ee5336d
7
+ data.tar.gz: 06fea4288ebf3eeb5a6e1c3a2baf2895c22514389fbb169a5741474ef500baaaa52b129d0711ee645025fc713a6bd58e2436be05979445c64ec1831206af1437
data/README.md CHANGED
@@ -35,50 +35,6 @@ result.errors # => ["Validation error"]
35
35
 
36
36
  ---
37
37
 
38
- ### `RageArch::UseCase::Base` — use cases
39
-
40
- ```ruby
41
- class CreateOrder < RageArch::UseCase::Base
42
- use_case_symbol :create_order
43
- deps :order_store, :notifications # injected by symbol
44
-
45
- def call(params = {})
46
- order = order_store.build(params)
47
- return failure(order.errors) unless order_store.save(order)
48
- notifications.notify(:order_created, order)
49
- success(order)
50
- end
51
- end
52
- ```
53
-
54
- Build and run manually:
55
-
56
- ```ruby
57
- use_case = RageArch::UseCase::Base.build(:create_order)
58
- result = use_case.call(reference: "REF-1", total_cents: 1000)
59
- ```
60
-
61
- #### `ar_dep` — inline ActiveRecord dep
62
-
63
- When a dep is a simple wrapper over an ActiveRecord model, declare it directly in the use case instead of creating a separate class:
64
-
65
- ```ruby
66
- class Posts::Create < RageArch::UseCase::Base
67
- use_case_symbol :posts_create
68
- ar_dep :post_store, Post # auto-creates an AR adapter if :post_store is not registered
69
-
70
- def call(params = {})
71
- post = post_store.build(params)
72
- return failure(post.errors.full_messages) unless post_store.save(post)
73
- success(post: post)
74
- end
75
- end
76
- ```
77
-
78
- If `:post_store` is registered in the container, that implementation is used. Otherwise, `RageArch::Deps::ActiveRecord.for(Post)` is used as fallback.
79
-
80
- ---
81
-
82
38
  ### `RageArch::Container` — dependency registration
83
39
 
84
40
  ```ruby
@@ -98,6 +54,15 @@ RageArch.resolve(:order_store) # => the registered implementation
98
54
  RageArch.registered?(:order_store) # => true
99
55
  ```
100
56
 
57
+ **Convention-based auto-registration:** Use cases from `app/use_cases/` and deps from `app/deps/` are auto-registered at boot. The initializer is only needed to override conventions or register external adapters.
58
+
59
+ **AR model auto-resolution:** If a dep symbol ends in `_store` and no file exists in `app/deps/`, rage_arch looks for an ActiveRecord model automatically:
60
+
61
+ - `:post_store` resolves to `Post`
62
+ - `:appointment_store` resolves to `Appointment`
63
+
64
+ Explicit `RageArch.register(...)` always takes priority.
65
+
101
66
  ---
102
67
 
103
68
  ### Dependencies (Deps)
@@ -129,21 +94,7 @@ module Posts
129
94
  end
130
95
  ```
131
96
 
132
- Register it in `config/initializers/rage_arch.rb`:
133
-
134
- ```ruby
135
- RageArch.register(:post_store, Posts::PostStore.new)
136
- ```
137
-
138
- #### ActiveRecord dep (generated)
139
-
140
- For deps that simply wrap an AR model with standard CRUD, use the generator:
141
-
142
- ```bash
143
- rails g rage_arch:ar_dep post_store Post
144
- ```
145
-
146
- This creates `app/deps/posts/post_store.rb` with `build`, `find`, `save`, `update`, `destroy`, and `list` methods backed by `RageArch::Deps::ActiveRecord.for(Post)`.
97
+ Auto-registered by convention from `app/deps/` — no manual registration needed.
147
98
 
148
99
  #### Generating a dep from use case analysis
149
100
 
@@ -172,6 +123,79 @@ The generator scans `app/deps/` for files matching the symbol, updates `config/i
172
123
 
173
124
  ---
174
125
 
126
+ ### `RageArch::UseCase::Base` — use cases
127
+
128
+ Use cases declare their dependencies by symbol, receive them via injection, and return a `Result`. The symbol is inferred from the class name by convention:
129
+
130
+ ```ruby
131
+ class Orders::Create < RageArch::UseCase::Base
132
+ # symbol :orders_create is inferred automatically
133
+ deps :order_store, :notifications
134
+
135
+ def call(params = {})
136
+ order = order_store.build(params)
137
+ return failure(order.errors) unless order_store.save(order)
138
+ notifications.notify(:order_created, order)
139
+ success(order: order)
140
+ end
141
+ end
142
+ ```
143
+
144
+ Symbol inference: `Orders::Create` becomes `:orders_create`. Explicit `use_case_symbol :my_symbol` still works as override.
145
+
146
+ Build and run manually:
147
+
148
+ ```ruby
149
+ use_case = RageArch::UseCase::Base.build(:orders_create)
150
+ result = use_case.call(reference: "REF-1", total_cents: 1000)
151
+ ```
152
+
153
+ ---
154
+
155
+ ### undo — automatic rollback on failure
156
+
157
+ Define `def undo(value)` on any use case. If `call` returns `failure(...)`, `undo` is called automatically:
158
+
159
+ ```ruby
160
+ class Payments::Charge < RageArch::UseCase::Base
161
+ deps :payment_gateway
162
+
163
+ def call(params = {})
164
+ charge = payment_gateway.charge(params[:amount])
165
+ return failure(["Payment failed"]) unless charge.success?
166
+ success(charge_id: charge.id)
167
+ end
168
+
169
+ def undo(value)
170
+ payment_gateway.refund(value[:charge_id]) if value
171
+ end
172
+ end
173
+ ```
174
+
175
+ **Cascade undo with `use_cases`:** When a parent use case orchestrates children via `use_cases` and returns failure, each successfully-completed child has its `undo` called in reverse order:
176
+
177
+ ```ruby
178
+ class Bookings::Create < RageArch::UseCase::Base
179
+ use_cases :payments_charge, :slots_reserve, :notifications_send
180
+
181
+ def call(params = {})
182
+ charge = payments_charge.call(amount: params[:amount])
183
+ return charge unless charge.success?
184
+
185
+ reserve = slots_reserve.call(slot_id: params[:slot_id])
186
+ return failure(reserve.errors) unless reserve.success?
187
+ # If this fails, slots_reserve.undo and payments_charge.undo run automatically
188
+
189
+ notifications_send.call(booking: params)
190
+ success(booking: params)
191
+ end
192
+ end
193
+ ```
194
+
195
+ No DSL, no configuration. Just define `undo` where you need rollback.
196
+
197
+ ---
198
+
175
199
  ### `RageArch::Controller` — thin controller mixin
176
200
 
177
201
  ```ruby
@@ -202,13 +226,12 @@ end
202
226
 
203
227
  ### `RageArch::EventPublisher` — domain events
204
228
 
205
- Every use case automatically publishes an event when it finishes. Other use cases subscribe to react:
229
+ Every use case automatically publishes an event when it finishes. Other use cases subscribe to react. **Subscribers run asynchronously via ActiveJob by default:**
206
230
 
207
231
  ```ruby
208
232
  class Notifications::SendPostCreatedEmail < RageArch::UseCase::Base
209
- use_case_symbol :send_post_created_email
210
233
  deps :mailer
211
- subscribe :posts_create # runs when :posts_create event is published
234
+ subscribe :posts_create # async by default
212
235
 
213
236
  def call(payload = {})
214
237
  return success unless payload[:success]
@@ -218,6 +241,12 @@ class Notifications::SendPostCreatedEmail < RageArch::UseCase::Base
218
241
  end
219
242
  ```
220
243
 
244
+ **Synchronous subscribers** (opt-in):
245
+
246
+ ```ruby
247
+ subscribe :posts_create, async: false
248
+ ```
249
+
221
250
  Subscribe to multiple events or everything:
222
251
 
223
252
  ```ruby
@@ -237,7 +266,6 @@ skip_auto_publish
237
266
 
238
267
  ```ruby
239
268
  class CreateOrderWithNotification < RageArch::UseCase::Base
240
- use_case_symbol :create_order_with_notification
241
269
  deps :order_store
242
270
  use_cases :orders_create, :notifications_send
243
271
 
@@ -263,7 +291,6 @@ end
263
291
  | `rails g rage_arch:use_case CreateOrder` | Generates a base use case file |
264
292
  | `rails g rage_arch:use_case orders/create` | Generates a namespaced use case (`Orders::Create`) |
265
293
  | `rails g rage_arch:dep post_store` | Generates a dep class by scanning method calls in use cases |
266
- | `rails g rage_arch:ar_dep post_store Post` | Generates a dep that wraps an ActiveRecord model |
267
294
  | `rails g rage_arch:dep_switch post_store` | Lists implementations and switches which one is registered |
268
295
  | `rails g rage_arch:dep_switch post_store PostgresPostStore` | Directly activates a specific implementation |
269
296
 
@@ -274,7 +301,23 @@ end
274
301
  ```ruby
275
302
  # spec/rails_helper.rb
276
303
  require "rage_arch/rspec_matchers"
304
+ require "rage_arch/rspec_helpers"
277
305
  require "rage_arch/fake_event_publisher"
306
+
307
+ RSpec.configure do |config|
308
+ config.include RageArch::RSpecHelpers # auto-isolates each test
309
+ end
310
+ ```
311
+
312
+ **Test isolation:** `RageArch::RSpecHelpers` wraps each example in `RageArch.isolate`, so dep registrations never bleed between tests.
313
+
314
+ **Manual isolation** (without the helper):
315
+
316
+ ```ruby
317
+ RageArch.isolate do
318
+ RageArch.register(:payment_gateway, FakeGateway.new)
319
+ # registrations inside are scoped; originals restored on exit
320
+ end
278
321
  ```
279
322
 
280
323
  **Result matchers:**
@@ -306,6 +349,9 @@ config.rage_arch.auto_publish_events = false
306
349
 
307
350
  # Disable boot verification (default: true)
308
351
  config.rage_arch.verify_deps = false
352
+
353
+ # Run all subscribers synchronously — useful for test environments (default: true)
354
+ config.rage_arch.async_subscribers = false
309
355
  ```
310
356
 
311
357
  ---
@@ -314,16 +360,17 @@ config.rage_arch.verify_deps = false
314
360
 
315
361
  At boot, `RageArch.verify_deps!` runs automatically and raises if it finds wiring problems. It checks:
316
362
 
317
- - Every dep declared with `deps :symbol` is registered in the container
363
+ - Every dep declared with `deps :symbol` is registered in the container (or auto-resolved for `_store` deps)
318
364
  - Every method called on a dep is implemented by the registered object (via static analysis)
319
365
  - Every use case declared with `use_cases :symbol` exists in the registry
366
+ - Warns if `use_case_symbol` doesn't match the convention-inferred symbol
320
367
 
321
368
  Example error output:
322
369
 
323
370
  ```
324
371
  RageArch boot verification failed:
325
- UseCase :posts_create (Posts::Create) declares dep :post_store — not registered in container
326
- UseCase :posts_create (Posts::Create) calls :post_store#save — Posts::PostStore does not implement #save
372
+ UseCase :posts_create (Posts::Create) declares dep :post_store — not registered in container and no AR model found
373
+ UseCase :posts_create (Posts::Create) calls dep :post_store#save — Posts::PostStore does not implement #save
327
374
  UseCase :posts_notify (Posts::Notify) declares use_cases :email_send — not registered in use case registry
328
375
  ```
329
376
 
@@ -346,9 +393,13 @@ end
346
393
 
347
394
  ## Documentation
348
395
 
349
- - [`doc/REFERENCE.md`](doc/REFERENCE.md) — Full API reference with all options and examples
350
- - [`doc/DOCUMENTATION.md`](doc/DOCUMENTATION.md) — Detailed behaviour (use cases, deps, events, config)
351
396
  - [`doc/GETTING_STARTED.md`](doc/GETTING_STARTED.md) — Getting started guide with common tasks
397
+ - [`doc/DOCUMENTATION.md`](doc/DOCUMENTATION.md) — Detailed behaviour (use cases, deps, events, config)
398
+ - [`doc/REFERENCE.md`](doc/REFERENCE.md) — Quick-lookup API reference (classes, methods, options)
399
+
400
+ ## AI context
401
+
402
+ If you use an AI coding agent, point it to [`IA.md`](IA.md) for a compact reference of the full gem API, conventions, and architecture.
352
403
 
353
404
  ## License
354
405
 
@@ -27,6 +27,8 @@ module RageArch
27
27
  end
28
28
  end
29
29
 
30
+ private
31
+
30
32
  # When the dep file already exists, parse it for existing method names and insert only stubs for missing ones.
31
33
  def add_missing_methods_only(relative_path)
32
34
  full_path = File.join(destination_root, relative_path)
@@ -20,7 +20,6 @@ module RageArch
20
20
  invoke_rails_scaffold_views
21
21
  create_controller
22
22
  add_route
23
- inject_register_ar
24
23
  end
25
24
 
26
25
  private
@@ -65,17 +64,6 @@ module RageArch
65
64
  route "resources :#{plural_name}"
66
65
  end
67
66
 
68
- def inject_register_ar
69
- initializer_path = File.join(destination_root, "config/initializers/rage_arch.rb")
70
- return unless File.exist?(initializer_path)
71
- content = File.read(initializer_path)
72
- return if content.include?("register_ar(:#{repo_symbol})")
73
- inject_line = " RageArch.register_ar(:#{repo_symbol}, #{model_class_name})\n"
74
- content.sub!(/(Rails\.application\.config\.after_initialize do\s*\n)/m, "\\1#{inject_line}")
75
- File.write(initializer_path, content)
76
- say_status :inject, "config/initializers/rage_arch.rb (register_ar :#{repo_symbol})", :green
77
- end
78
-
79
67
  def plural_name
80
68
  name.underscore.pluralize
81
69
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Dep for :<%= symbol_name %>.
4
4
  # Methods detected from use cases: <%= @methods.join(', ') %>.
5
- # Register in config/initializers/rage_arch.rb: RageArch.register(:<%= symbol_name %>, <%= full_class_name %>.new)
5
+ # Auto-registered by convention from app/deps/
6
6
  module <%= module_name %>
7
7
  class <%= class_name %>
8
8
  <% @methods.each do |method_name| -%>
@@ -1,19 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Register your app's deps here. Deps are grouped by module (e.g. app/deps/posts/post_repo.rb → Posts::PostRepo).
4
- # Use RageArch.register(:symbol, ClassName.new) or RageArch.register_ar(:symbol, Model) for AR-backed deps.
3
+ # rage_arch auto-registers use cases from app/use_cases/ and deps from app/deps/
4
+ # Add registrations here only to override conventions or register external adapters.
5
+ #
6
+ # Example:
7
+ # RageArch.register(:payment_gateway, Payments::StripeGateway.new)
5
8
 
6
9
  Rails.application.config.after_initialize do
7
- # Deps
8
- # RageArch.register(:post_repo, Posts::PostRepo.new)
9
- # RageArch.register_ar(:user_repo, User)
10
-
11
10
  # Event publisher: use cases that declare subscribe :event_name are wired here.
12
11
  publisher = RageArch::EventPublisher.new
13
12
  RageArch::UseCase::Base.wire_subscriptions_to(publisher)
14
13
  RageArch.register(:event_publisher, publisher)
15
14
 
16
- # Called here (after all deps are registered) instead of relying on the Railtie,
17
- # which runs before this block. Skipped during asset precompilation.
18
15
  RageArch.verify_deps! unless ENV["SECRET_KEY_BASE_DUMMY"].present?
19
16
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  class <%= module_name %>Controller < ApplicationController
4
4
  def index
5
- run :<%= list_symbol %>, {},
5
+ run :<%= list_symbol %>,
6
6
  success: ->(r) { @<%= plural_name %> = r.value[:<%= singular_name %>s]; render :index },
7
7
  failure: ->(r) { redirect_to root_path, alert: r.errors.join(", ") }
8
8
  end
@@ -14,7 +14,7 @@ class <%= module_name %>Controller < ApplicationController
14
14
  end
15
15
 
16
16
  def new
17
- run :<%= new_symbol %>, {},
17
+ run :<%= new_symbol %>,
18
18
  success: ->(r) { @<%= singular_name %> = r.value[:<%= singular_name %>]; render :new },
19
19
  failure: ->(r) { redirect_to <%= plural_name %>_path, alert: r.errors.join(", ") }
20
20
  end
@@ -22,7 +22,12 @@ class <%= module_name %>Controller < ApplicationController
22
22
  def create
23
23
  run :<%= create_symbol %>, <%= singular_name %>_params,
24
24
  success: ->(r) { redirect_to <%= singular_name %>_path(r.value[:<%= singular_name %>].id), notice: "<%= model_class_name %> was successfully created." },
25
- failure: ->(r) { @<%= singular_name %> = run_result(:<%= new_symbol %>, {}).value[:<%= singular_name %>]; flash_errors(r); render :new, status: :unprocessable_entity }
25
+ failure: ->(r) {
26
+ new_result = run_result(:<%= new_symbol %>)
27
+ @<%= singular_name %> = new_result.success? ? new_result.value[:<%= singular_name %>] : nil
28
+ flash_errors(r)
29
+ render :new, status: :unprocessable_entity
30
+ }
26
31
  end
27
32
 
28
33
  def edit
@@ -2,7 +2,6 @@
2
2
 
3
3
  module <%= module_name %>
4
4
  class Create < RageArch::UseCase::Base
5
- use_case_symbol :<%= create_symbol %>
6
5
  deps :<%= repo_symbol %>
7
6
 
8
7
  def call(params = {})
@@ -2,7 +2,6 @@
2
2
 
3
3
  module <%= module_name %>
4
4
  class Destroy < RageArch::UseCase::Base
5
- use_case_symbol :<%= destroy_symbol %>
6
5
  deps :<%= repo_symbol %>
7
6
 
8
7
  def call(params = {})
@@ -2,7 +2,6 @@
2
2
 
3
3
  module <%= module_name %>
4
4
  class List < RageArch::UseCase::Base
5
- use_case_symbol :<%= list_symbol %>
6
5
  deps :<%= repo_symbol %>
7
6
 
8
7
  def call(_params = {})
@@ -2,7 +2,6 @@
2
2
 
3
3
  module <%= module_name %>
4
4
  class New < RageArch::UseCase::Base
5
- use_case_symbol :<%= new_symbol %>
6
5
  deps :<%= repo_symbol %>
7
6
 
8
7
  def call(_params = {})
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Dep for :<%= repo_symbol %>. Wraps the <%= model_class_name %> Active Record model.
4
- # Registered by rage_arch:scaffold in config/initializers/rage_arch.rb
4
+ # Auto-registered by convention from app/deps/
5
5
  module <%= module_name %>
6
6
  class <%= singular_name.camelize %>Repo
7
7
  def initialize
@@ -2,7 +2,6 @@
2
2
 
3
3
  module <%= module_name %>
4
4
  class Show < RageArch::UseCase::Base
5
- use_case_symbol :<%= show_symbol %>
6
5
  deps :<%= repo_symbol %>
7
6
 
8
7
  def call(params = {})
@@ -2,7 +2,6 @@
2
2
 
3
3
  module <%= module_name %>
4
4
  class Update < RageArch::UseCase::Base
5
- use_case_symbol :<%= update_symbol %>
6
5
  deps :<%= repo_symbol %>
7
6
 
8
7
  def call(params = {})
@@ -4,13 +4,11 @@
4
4
  module <%= class_path.map(&:camelize).join('::') %>
5
5
  <%- end -%>
6
6
  class <%= file_name.camelize %> < RageArch::UseCase::Base
7
- use_case_symbol :<%= file_name %>
8
-
9
- deps # e.g. :order_store, :notifications
7
+ # deps :example_repo
10
8
 
11
9
  def call(params = {})
12
10
  # TODO: implement use case logic
13
- RageArch::Result.success(params)
11
+ success(params)
14
12
  end
15
13
  end
16
14
  <%- class_path.each do -%>
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RageArch
4
+ # Auto-registers use cases and deps at boot by convention.
5
+ # Use cases: subclasses of RageArch::UseCase::Base → registered by inferred symbol.
6
+ # Deps: classes under app/deps/ → registered by inferred symbol.
7
+ # AR auto-resolution: deps ending in _store with no file in app/deps/ → resolves AR model.
8
+ class AutoRegistrar
9
+ class << self
10
+ def run
11
+ register_use_cases
12
+ register_deps
13
+ resolve_store_deps
14
+ end
15
+
16
+ private
17
+
18
+ def register_use_cases
19
+ RageArch::UseCase::Base.registry.each do |sym, _klass|
20
+ # Already in registry via use_case_symbol or infer_use_case_symbol — nothing to do.
21
+ # The act of calling use_case_symbol (explicit or inferred) registers the class.
22
+ end
23
+
24
+ # Ensure all subclasses have their symbol inferred and registered
25
+ RageArch::UseCase::Base.descendants.each do |klass|
26
+ next if klass.name.nil? # anonymous classes
27
+ klass.use_case_symbol # triggers inference + registration if not already set
28
+ end
29
+ end
30
+
31
+ def register_deps
32
+ return unless defined?(Rails) && Rails.application
33
+
34
+ deps_dir = Rails.root.join("app", "deps")
35
+ return unless deps_dir.exist?
36
+
37
+ Dir[deps_dir.join("**/*.rb")].sort.each do |file|
38
+ require file
39
+ end
40
+
41
+ # Register any class defined under app/deps/ that isn't already registered
42
+ ObjectSpace.each_object(Class).select do |klass|
43
+ next unless klass.name
44
+ next if klass.name.start_with?("RageArch::")
45
+
46
+ # Check if this class was loaded from app/deps/
47
+ source_file = begin
48
+ Object.const_source_location(klass.name)&.first
49
+ rescue
50
+ nil
51
+ end
52
+ next unless source_file && source_file.start_with?(deps_dir.to_s)
53
+
54
+ sym = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(klass.name)).to_sym
55
+ unless Container.registered?(sym)
56
+ Container.register(sym, klass.new)
57
+ end
58
+ end
59
+ end
60
+
61
+ def resolve_store_deps
62
+ # For each use case, check declared deps ending in _store
63
+ RageArch::UseCase::Base.registry.each_value do |klass|
64
+ klass.declared_deps.each do |dep_sym|
65
+ next if Container.registered?(dep_sym)
66
+ next unless dep_sym.to_s.end_with?("_store")
67
+
68
+ model_name = dep_sym.to_s.sub(/_store\z/, "").camelize
69
+ model_class = begin
70
+ model_name.constantize
71
+ rescue NameError
72
+ nil
73
+ end
74
+
75
+ if model_class && defined?(ActiveRecord::Base) && model_class < ActiveRecord::Base
76
+ Container.register(dep_sym, Deps::ActiveRecord.for(model_class))
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -2,15 +2,19 @@
2
2
 
3
3
  module RageArch
4
4
  # Container to register and resolve dependencies by symbol.
5
- # Usage: RageArch.register(:order_store, MyAdapter.new); RageArch.resolve(:order_store)
5
+ # Supports scoped isolation via RageArch.isolate for test environments.
6
6
  class Container
7
7
  class << self
8
8
  def register(symbol, implementation = nil, &block)
9
- registry[symbol] = block || implementation
9
+ if current_scope
10
+ current_scope[symbol] = block || implementation
11
+ else
12
+ registry[symbol] = block || implementation
13
+ end
10
14
  end
11
15
 
12
16
  def resolve(symbol)
13
- entry = registry[symbol]
17
+ entry = scoped_lookup(symbol)
14
18
  raise KeyError, "Dep not registered: #{symbol.inspect}" unless entry
15
19
 
16
20
  if entry.respond_to?(:call) && entry.is_a?(Proc)
@@ -23,6 +27,7 @@ module RageArch
23
27
  end
24
28
 
25
29
  def registered?(symbol)
30
+ return true if current_scope&.key?(symbol)
26
31
  registry.key?(symbol)
27
32
  end
28
33
 
@@ -33,6 +38,34 @@ module RageArch
33
38
  def reset!
34
39
  @registry = {}
35
40
  end
41
+
42
+ # --- Scope isolation for tests ---
43
+
44
+ def push_scope
45
+ scope_stack.push({})
46
+ end
47
+
48
+ def pop_scope
49
+ scope_stack.pop
50
+ end
51
+
52
+ private
53
+
54
+ def scope_stack
55
+ Thread.current[:rage_arch_container_scopes] ||= []
56
+ end
57
+
58
+ def current_scope
59
+ scope_stack.last
60
+ end
61
+
62
+ def scoped_lookup(symbol)
63
+ if current_scope&.key?(symbol)
64
+ current_scope[symbol]
65
+ else
66
+ registry[symbol]
67
+ end
68
+ end
36
69
  end
37
70
  end
38
71
  end
@@ -8,24 +8,20 @@ module RageArch
8
8
  config.rage_arch = ActiveSupport::OrderedOptions.new
9
9
  config.rage_arch.auto_publish_events = true
10
10
  config.rage_arch.verify_deps = true
11
+ config.rage_arch.async_subscribers = true
11
12
 
12
- # Load use case files so they register their symbols in the registry.
13
- # Without this, build(:symbol) would fail until the use case constant was referenced.
14
- config.after_initialize do |app|
15
- # Skip everything during asset precompilation — no deps are registered then.
13
+ # Load use case and dep files, then auto-register by convention.
14
+ initializer "rage_arch.auto_register", after: :eager_load! do |app|
16
15
  next if ENV["SECRET_KEY_BASE_DUMMY"].present?
17
16
 
17
+ # Load use case files so they register their symbols in the registry.
18
18
  use_cases_dir = app.root.join("app/use_cases")
19
19
  if use_cases_dir.exist?
20
20
  Dir[use_cases_dir.join("**/*.rb")].sort.each { |f| require f }
21
21
  end
22
22
 
23
- # NOTE: verify_deps! is intentionally NOT called here. This after_initialize
24
- # runs before the app's own initializers' after_initialize blocks, so deps
25
- # registered there would not be visible yet. Apps should call
26
- # RageArch.verify_deps! manually at the end of their own after_initialize
27
- # (config/initializers/rage_arch.rb), after all deps are registered.
28
- # Set config.rage_arch.verify_deps = false to opt out.
23
+ # Auto-register use cases and deps by convention.
24
+ RageArch::AutoRegistrar.run
29
25
  end
30
26
  end
31
27
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RageArch
4
+ # RSpec helper that wraps each example in RageArch.isolate,
5
+ # preventing test pollution from dep registrations.
6
+ #
7
+ # Usage in spec/rails_helper.rb:
8
+ # require "rage_arch/rspec_helpers"
9
+ # RSpec.configure do |config|
10
+ # config.include RageArch::RSpecHelpers
11
+ # end
12
+ module RSpecHelpers
13
+ def self.included(base)
14
+ base.around(:each) do |example|
15
+ RageArch.isolate { example.run }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RageArch
4
+ class SubscriberJob < ActiveJob::Base
5
+ queue_as :default
6
+
7
+ def perform(subscriber_symbol, payload)
8
+ sym = subscriber_symbol.to_sym
9
+ payload = payload.transform_keys(&:to_sym) if payload.is_a?(Hash)
10
+ RageArch::UseCase::Base.build(sym).call(payload)
11
+ end
12
+ end
13
+ end
@@ -1,8 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/inflector"
4
+
3
5
  module RageArch
4
6
  module UseCase
5
- # Runs another use case by symbol. Used when a use case declares use_cases :other_symbol.
7
+ # Tracks successful use case executions for cascade undo on failure.
8
+ class ExecutionTracker
9
+ def initialize
10
+ @recorded = []
11
+ end
12
+
13
+ def record(use_case_instance, result)
14
+ @recorded << { use_case: use_case_instance, result: result }
15
+ end
16
+
17
+ def undo_all
18
+ @recorded.reverse_each do |entry|
19
+ uc = entry[:use_case]
20
+ uc.undo(entry[:result].value) if uc.respond_to?(:undo)
21
+ end
22
+ end
23
+
24
+ def clear
25
+ @recorded.clear
26
+ end
27
+ end
28
+
29
+ # Wraps a use case runner to track successful calls for cascade undo.
30
+ class UseCaseProxy
31
+ def initialize(symbol, tracker:)
32
+ @symbol = symbol
33
+ @tracker = tracker
34
+ end
35
+
36
+ def call(params = {})
37
+ use_case = Base.build(@symbol)
38
+ result = use_case.call(params)
39
+ @tracker.record(use_case, result) if result.success?
40
+ result
41
+ end
42
+ end
43
+
44
+ # Runs another use case by symbol (legacy, used when no tracker is active).
6
45
  class Runner
7
46
  def initialize(symbol)
8
47
  @symbol = symbol
@@ -17,7 +56,6 @@ module RageArch
17
56
  #
18
57
  # Usage:
19
58
  # class CreateOrder < RageArch::UseCase::Base
20
- # use_case_symbol :create_order
21
59
  # deps :order_store, :notifications
22
60
  #
23
61
  # def call(params = {})
@@ -32,24 +70,38 @@ module RageArch
32
70
  module Instrumentation
33
71
  def call(params = {})
34
72
  sym = self.class.use_case_symbol
73
+
74
+ # Set up execution tracker for cascade undo via use_cases
75
+ @_execution_tracker = ExecutionTracker.new
76
+
35
77
  if defined?(ActiveSupport::Notifications)
36
78
  ActiveSupport::Notifications.instrument("rage_arch.use_case.run", symbol: sym, params: params) do |payload|
37
79
  result = super(params)
38
80
  payload[:success] = result.success?
39
81
  payload[:errors] = result.errors unless result.success?
40
82
  payload[:result] = result
41
- auto_publish_if_enabled(sym, params, result)
83
+ handle_undo_and_publish(sym, params, result)
42
84
  result
43
85
  end
44
86
  else
45
87
  result = super(params)
46
- auto_publish_if_enabled(sym, params, result)
88
+ handle_undo_and_publish(sym, params, result)
47
89
  result
48
90
  end
49
91
  end
50
92
 
51
93
  private
52
94
 
95
+ def handle_undo_and_publish(use_case_symbol, params, result)
96
+ if result.failure?
97
+ # Cascade undo: reverse-order undo of tracked child use cases
98
+ @_execution_tracker&.undo_all
99
+ # Self undo
100
+ undo(result.value) if respond_to?(:undo)
101
+ end
102
+ auto_publish_if_enabled(use_case_symbol, params, result)
103
+ end
104
+
53
105
  def auto_publish_if_enabled(use_case_symbol, params, result)
54
106
  return unless auto_publish_enabled?
55
107
  return unless self.class.container.registered?(:event_publisher)
@@ -78,13 +130,14 @@ module RageArch
78
130
  super
79
131
  subclass.prepend(Instrumentation)
80
132
  end
133
+
81
134
  def use_case_symbol(sym = nil)
82
135
  if sym
83
136
  @use_case_symbol = sym
84
137
  Base.registry[sym] = self
85
138
  sym
86
139
  else
87
- @use_case_symbol
140
+ @use_case_symbol ||= infer_use_case_symbol
88
141
  end
89
142
  end
90
143
 
@@ -99,12 +152,18 @@ module RageArch
99
152
  end
100
153
 
101
154
  # Declare other use cases this one can call. In call(), use e.g. posts_create.call(params)
102
- # to run that use case and get its Result. Same layer can reference by symbol.
155
+ # to run that use case and get its Result. Tracked for cascade undo on failure.
103
156
  def use_cases(*symbols)
104
157
  @declared_use_cases ||= []
105
158
  @declared_use_cases.concat(symbols)
106
159
  symbols.each do |sym|
107
- define_method(sym) { Runner.new(sym) }
160
+ define_method(sym) do
161
+ if @_execution_tracker
162
+ UseCaseProxy.new(sym, tracker: @_execution_tracker)
163
+ else
164
+ Runner.new(sym)
165
+ end
166
+ end
108
167
  end
109
168
  private(*symbols) if symbols.any?
110
169
  symbols
@@ -117,13 +176,20 @@ module RageArch
117
176
  # Subscribe this use case to domain events. When the event is published, this use case's call(payload) runs.
118
177
  # You can subscribe to multiple events: subscribe :post_created, :post_updated
119
178
  # Special: subscribe :all to run on every published event (payload will include :event).
120
- def subscribe(*event_names)
179
+ # By default subscribers run asynchronously via ActiveJob. Use async: false for synchronous execution.
180
+ def subscribe(*event_names, async: true)
121
181
  @subscribed_events ||= []
122
- @subscribed_events.concat(event_names.map(&:to_sym))
182
+ event_names.each do |ev|
183
+ @subscribed_events << { event: ev.to_sym, async: async }
184
+ end
123
185
  event_names
124
186
  end
125
187
 
126
188
  def subscribed_events
189
+ (@subscribed_events || []).map { |e| e[:event] }
190
+ end
191
+
192
+ def subscription_entries
127
193
  @subscribed_events || []
128
194
  end
129
195
 
@@ -140,26 +206,18 @@ module RageArch
140
206
  # subscribed_events with the publisher so they run when those events are published.
141
207
  def wire_subscriptions_to(publisher)
142
208
  registry.each do |symbol, klass|
143
- next unless klass.respond_to?(:subscribed_events)
144
- klass.subscribed_events.each do |event_name|
145
- publisher.subscribe(event_name, symbol)
209
+ next unless klass.respond_to?(:subscription_entries)
210
+ klass.subscription_entries.each do |entry|
211
+ if entry[:async] && async_subscribers_enabled?
212
+ publisher.subscribe(entry[:event], async_runner(symbol))
213
+ else
214
+ publisher.subscribe(entry[:event], symbol)
215
+ end
146
216
  end
147
217
  end
148
218
  nil
149
219
  end
150
220
 
151
- # Dep that uses Active Record for the model when not registered in the container.
152
- # Example: ar_dep :user_store, User (instead of default: RageArch::Deps::ActiveRecord.for(User))
153
- def ar_dep(symbol, model_class)
154
- @ar_deps ||= {}
155
- @ar_deps[symbol] = model_class
156
- deps(symbol)
157
- end
158
-
159
- def ar_deps
160
- @ar_deps || {}
161
- end
162
-
163
221
  def declared_deps
164
222
  @declared_deps || []
165
223
  end
@@ -175,12 +233,7 @@ module RageArch
175
233
  def build(symbol)
176
234
  klass = Base.resolve(symbol)
177
235
  deps_hash = klass.declared_deps.uniq.to_h do |s|
178
- if klass.ar_deps.key?(s)
179
- impl = container.registered?(s) ? container.resolve(s) : Deps::ActiveRecord.for(klass.ar_deps[s])
180
- [s, impl]
181
- else
182
- [s, container.resolve(s)]
183
- end
236
+ [s, container.resolve(s)]
184
237
  end
185
238
  klass.new(**deps_hash)
186
239
  end
@@ -188,6 +241,25 @@ module RageArch
188
241
  def container
189
242
  RageArch::Container
190
243
  end
244
+
245
+ private
246
+
247
+ def infer_use_case_symbol
248
+ return nil unless name
249
+ sym = ::ActiveSupport::Inflector.underscore(name).gsub("/", "_").to_sym
250
+ Base.registry[sym] = self
251
+ sym
252
+ end
253
+
254
+ def async_subscribers_enabled?
255
+ return false unless defined?(ActiveJob)
256
+ return true unless defined?(Rails) && Rails.application&.config&.respond_to?(:rage_arch) && Rails.application.config.rage_arch
257
+ Rails.application.config.rage_arch.async_subscribers != false
258
+ end
259
+
260
+ def async_runner(symbol)
261
+ ->(payload) { RageArch::SubscriberJob.perform_later(symbol.to_s, payload) }
262
+ end
191
263
  end
192
264
 
193
265
  def initialize(**injected_deps)
@@ -198,7 +270,6 @@ module RageArch
198
270
  raise NotImplementedError, "#{self.class}#call must be implemented"
199
271
  end
200
272
 
201
- # From a use case: success(value) and failure(errors) instead of RageArch::Result.success/failure.
202
273
  def success(value = nil)
203
274
  RageArch::Result.success(value)
204
275
  end
@@ -213,10 +284,6 @@ module RageArch
213
284
  @injected_deps ||= {}
214
285
  end
215
286
 
216
- # Resolve a dep: first the injected one; if missing, use the container.
217
- # Optional: dep(:symbol, default: Implementation) when not registered.
218
- # If default is an Active Record model class (e.g. Order), it is wrapped automatically
219
- # with RageArch::Deps::ActiveRecord.for(default), so you can write dep(:order_store, default: Order).
220
287
  def dep(symbol, default: nil)
221
288
  return injected_deps[symbol] if injected_deps.key?(symbol)
222
289
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RageArch
4
- VERSION = "0.1.4"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/rage_arch.rb CHANGED
@@ -8,6 +8,7 @@ require_relative "rage_arch/event_publisher"
8
8
  require_relative "rage_arch/use_case"
9
9
  require_relative "rage_arch/deps/active_record"
10
10
  require_relative "rage_arch/dep_scanner"
11
+ require_relative "rage_arch/auto_registrar"
11
12
 
12
13
  module RageArch
13
14
  class << self
@@ -29,6 +30,15 @@ module RageArch
29
30
  Container.registered?(symbol)
30
31
  end
31
32
 
33
+ # Wraps execution in a sandboxed container scope.
34
+ # Registrations inside the block are scoped; originals are restored on exit.
35
+ def isolate(&block)
36
+ Container.push_scope
37
+ yield
38
+ ensure
39
+ Container.pop_scope
40
+ end
41
+
32
42
  # Verifies that all deps and use_cases declared by registered use cases are
33
43
  # available before the app handles any request. Call after all initializers run
34
44
  # (done automatically by the Railtie unless config.rage_arch.verify_deps = false).
@@ -37,14 +47,32 @@ module RageArch
37
47
  # Returns true when everything is wired correctly.
38
48
  def verify_deps!
39
49
  errors = []
50
+ warnings = []
40
51
  scanned_methods = DepScanner.new.scan
41
52
 
42
53
  UseCase::Base.registry.each do |uc_symbol, klass|
43
- klass.declared_deps.uniq.each do |dep_sym|
44
- next if klass.ar_deps.key?(dep_sym) # ar_deps fall back to ActiveRecord, optional
54
+ # 6a. Symbol/convention mismatch warning
55
+ if klass.name
56
+ inferred = ActiveSupport::Inflector.underscore(klass.name).gsub("/", "_").to_sym
57
+ explicit = klass.instance_variable_get(:@use_case_symbol)
58
+ if explicit && explicit != inferred
59
+ warnings << " #{klass} declares use_case_symbol :#{explicit} but convention infers :#{inferred} — explicit declaration overrides convention"
60
+ end
61
+ end
62
+
63
+ # 6c. Orphaned undo warning: undo defined but no call method
64
+ if klass.method_defined?(:undo) && !klass.method_defined?(:call, false)
65
+ warnings << " #{klass} defines undo but has no call method"
66
+ end
45
67
 
68
+ klass.declared_deps.uniq.each do |dep_sym|
46
69
  unless Container.registered?(dep_sym)
47
- errors << " UseCase :#{uc_symbol} (#{klass}) declares dep :#{dep_sym} not registered in container"
70
+ # 6b. AR model not found for _store dep
71
+ if dep_sym.to_s.end_with?("_store")
72
+ errors << " UseCase :#{uc_symbol} (#{klass}) declares dep :#{dep_sym} — not registered in container and no AR model found"
73
+ else
74
+ errors << " UseCase :#{uc_symbol} (#{klass}) declares dep :#{dep_sym} — not registered in container"
75
+ end
48
76
  next
49
77
  end
50
78
 
@@ -52,6 +80,7 @@ module RageArch
52
80
  next if required_methods.nil? || required_methods.empty?
53
81
 
54
82
  entry = Container.registry[dep_sym]
83
+ entry = Container.send(:scoped_lookup, dep_sym) if entry.nil?
55
84
  impl =
56
85
  if entry.is_a?(Class)
57
86
  entry
@@ -86,6 +115,11 @@ module RageArch
86
115
  end
87
116
  end
88
117
 
118
+ # Log warnings (non-fatal)
119
+ if warnings.any? && defined?(Rails) && Rails.logger
120
+ warnings.each { |w| Rails.logger.warn("[RageArch] #{w.strip}") }
121
+ end
122
+
89
123
  raise "RageArch boot verification failed:\n#{errors.join("\n")}" if errors.any?
90
124
 
91
125
  true
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rage_arch
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rage Corp
@@ -65,23 +65,22 @@ dependencies:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
67
  version: '3.0'
68
- description: Gem to structure Rails apps with use cases, dependencies injectable by
69
- symbol, and Result object (success/failure). Includes container, use case base,
70
- and rails g rage_arch:use_case generator.
68
+ description: Structure Rails apps with use cases, auto-registered dependencies, and
69
+ Result objects. Features convention-based wiring, domain events with async subscribers,
70
+ undo/rollback, ActiveRecord integration, and generators for scaffolds, use cases,
71
+ and deps.
71
72
  email:
72
- - ''
73
+ - antonio.facundo1794@gmail.com
73
74
  executables: []
74
75
  extensions: []
75
76
  extra_rdoc_files: []
76
77
  files:
77
78
  - LICENSE
78
79
  - README.md
79
- - lib/generators/rage_arch/ar_dep_generator.rb
80
80
  - lib/generators/rage_arch/dep_generator.rb
81
81
  - lib/generators/rage_arch/dep_switch_generator.rb
82
82
  - lib/generators/rage_arch/install_generator.rb
83
83
  - lib/generators/rage_arch/scaffold_generator.rb
84
- - lib/generators/rage_arch/templates/ar_dep.rb.tt
85
84
  - lib/generators/rage_arch/templates/dep.rb.tt
86
85
  - lib/generators/rage_arch/templates/rage_arch.rb.tt
87
86
  - lib/generators/rage_arch/templates/scaffold/api_controller.rb.tt
@@ -96,6 +95,7 @@ files:
96
95
  - lib/generators/rage_arch/templates/use_case.rb.tt
97
96
  - lib/generators/rage_arch/use_case_generator.rb
98
97
  - lib/rage_arch.rb
98
+ - lib/rage_arch/auto_registrar.rb
99
99
  - lib/rage_arch/container.rb
100
100
  - lib/rage_arch/controller.rb
101
101
  - lib/rage_arch/dep.rb
@@ -105,7 +105,9 @@ files:
105
105
  - lib/rage_arch/fake_event_publisher.rb
106
106
  - lib/rage_arch/railtie.rb
107
107
  - lib/rage_arch/result.rb
108
+ - lib/rage_arch/rspec_helpers.rb
108
109
  - lib/rage_arch/rspec_matchers.rb
110
+ - lib/rage_arch/subscriber_job.rb
109
111
  - lib/rage_arch/use_case.rb
110
112
  - lib/rage_arch/version.rb
111
113
  homepage: https://github.com/AntonioFacundo/rage_arch
@@ -129,5 +131,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
129
131
  requirements: []
130
132
  rubygems_version: 4.0.3
131
133
  specification_version: 4
132
- summary: 'Clean Architecture Light for Rails: use cases, injectable deps, Result.'
134
+ summary: Convention-over-configuration Clean Architecture for Rails.
133
135
  test_files: []
@@ -1,74 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rails/generators/base"
4
- require "rage_arch/dep_scanner"
5
-
6
- module RageArch
7
- module Generators
8
- class ArDepGenerator < ::Rails::Generators::Base
9
- source_root File.expand_path("templates", __dir__)
10
-
11
- argument :symbol_arg, type: :string, required: true, banner: "SYMBOL"
12
- argument :model_arg, type: :string, required: true, banner: "MODEL"
13
-
14
- desc "Create a dep class that wraps an Active Record model (build, find, save, update, destroy, list). Scans use cases for extra method calls and adds stubs for them. Example: rails g rage_arch:ar_dep post_store Post"
15
- def create_ar_dep
16
- @extra_methods = extra_methods
17
- template "ar_dep.rb.tt", File.join("app/deps", module_dir, "#{dep_file_name}.rb")
18
- say "Register in config/initializers/rage_arch.rb: RageArch.register(:#{symbol_name}, #{full_class_name}.new)", :green
19
- end
20
-
21
- STANDARD_AR_METHODS = %i[build find save update destroy list].freeze
22
-
23
- # Methods that use cases call on this dep but are not in the standard AR adapter
24
- def extra_methods
25
- detected = scanner.methods_for(symbol_name).to_a
26
- (detected - STANDARD_AR_METHODS).sort
27
- end
28
-
29
- def symbol_name
30
- symbol_arg.to_s.underscore
31
- end
32
-
33
- def model_name
34
- model_arg.camelize
35
- end
36
-
37
- def module_dir
38
- inferred_module_dir || use_case_folder
39
- end
40
-
41
- def module_name
42
- module_dir.camelize
43
- end
44
-
45
- def use_case_folder
46
- scanner.folder_for(symbol_name)
47
- end
48
-
49
- def inferred_module_dir
50
- entity = symbol_name.split("_").first
51
- entity.pluralize
52
- end
53
-
54
- def dep_file_name
55
- symbol_name
56
- end
57
-
58
- def class_name
59
- symbol_name.camelize
60
- end
61
-
62
- def full_class_name
63
- "#{module_name}::#{class_name}"
64
- end
65
-
66
- def scanner
67
- @scanner ||= begin
68
- root = destination_root
69
- RageArch::DepScanner.new(File.join(root, "app", "use_cases")).tap(&:scan)
70
- end
71
- end
72
- end
73
- end
74
- end
@@ -1,46 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Dep for :<%= symbol_name %>. Wraps the <%= model_name %> Active Record model.
4
- # Register in config/initializers/rage_arch.rb: RageArch.register(:<%= symbol_name %>, <%= full_class_name %>.new)
5
- module <%= module_name %>
6
- class <%= class_name %>
7
- def initialize
8
- @adapter = RageArch::Deps::ActiveRecord.for(<%= model_name %>)
9
- end
10
-
11
- def build(attrs = {})
12
- @adapter.build(attrs)
13
- end
14
-
15
- def find(id)
16
- @adapter.find(id)
17
- end
18
-
19
- def save(record)
20
- @adapter.save(record)
21
- end
22
-
23
- def update(record, attrs)
24
- @adapter.update(record, attrs)
25
- end
26
-
27
- def destroy(record)
28
- @adapter.destroy(record)
29
- end
30
-
31
- def list(filters: {})
32
- @adapter.list(filters: filters)
33
- end
34
- <% if @extra_methods.any? -%>
35
-
36
- # Extra methods detected from use cases — implement as needed
37
- <% @extra_methods.each do |method_name| -%>
38
-
39
- def <%= method_name %>(*args, **kwargs)
40
- # TODO: implement (e.g. delegate to @adapter or custom logic)
41
- raise NotImplementedError, "<%= full_class_name %>#<%= method_name %>"
42
- end
43
- <% end -%>
44
- <% end -%>
45
- end
46
- end