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 +4 -4
- data/README.md +120 -69
- data/lib/generators/rage_arch/dep_generator.rb +2 -0
- data/lib/generators/rage_arch/scaffold_generator.rb +0 -12
- data/lib/generators/rage_arch/templates/dep.rb.tt +1 -1
- data/lib/generators/rage_arch/templates/rage_arch.rb.tt +5 -8
- data/lib/generators/rage_arch/templates/scaffold/controller.rb.tt +8 -3
- data/lib/generators/rage_arch/templates/scaffold/create.rb.tt +0 -1
- data/lib/generators/rage_arch/templates/scaffold/destroy.rb.tt +0 -1
- data/lib/generators/rage_arch/templates/scaffold/list.rb.tt +0 -1
- data/lib/generators/rage_arch/templates/scaffold/new.rb.tt +0 -1
- data/lib/generators/rage_arch/templates/scaffold/post_repo.rb.tt +1 -1
- data/lib/generators/rage_arch/templates/scaffold/show.rb.tt +0 -1
- data/lib/generators/rage_arch/templates/scaffold/update.rb.tt +0 -1
- data/lib/generators/rage_arch/templates/use_case.rb.tt +2 -4
- data/lib/rage_arch/auto_registrar.rb +83 -0
- data/lib/rage_arch/container.rb +36 -3
- data/lib/rage_arch/railtie.rb +6 -10
- data/lib/rage_arch/rspec_helpers.rb +19 -0
- data/lib/rage_arch/subscriber_job.rb +13 -0
- data/lib/rage_arch/use_case.rb +102 -35
- data/lib/rage_arch/version.rb +1 -1
- data/lib/rage_arch.rb +37 -3
- metadata +10 -8
- data/lib/generators/rage_arch/ar_dep_generator.rb +0 -74
- data/lib/generators/rage_arch/templates/ar_dep.rb.tt +0 -46
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5977f7f0cda9c29591b5fada240de2a007facc8c3922e8f2a9c6a5ae08ed1f5b
|
|
4
|
+
data.tar.gz: 05b8e5a3a44cb57099c64b33c9cffba6a0abf80dc6256c41eabe87e86d48378e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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 #
|
|
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
|
-
#
|
|
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
|
-
#
|
|
4
|
-
#
|
|
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) {
|
|
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
|
|
@@ -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
|
-
#
|
|
4
|
+
# Auto-registered by convention from app/deps/
|
|
5
5
|
module <%= module_name %>
|
|
6
6
|
class <%= singular_name.camelize %>Repo
|
|
7
7
|
def initialize
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
data/lib/rage_arch/container.rb
CHANGED
|
@@ -2,15 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
module RageArch
|
|
4
4
|
# Container to register and resolve dependencies by symbol.
|
|
5
|
-
#
|
|
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
|
-
|
|
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 =
|
|
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
|
data/lib/rage_arch/railtie.rb
CHANGED
|
@@ -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
|
|
13
|
-
|
|
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
|
-
#
|
|
24
|
-
|
|
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
|
data/lib/rage_arch/use_case.rb
CHANGED
|
@@ -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
|
-
#
|
|
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
|
-
|
|
83
|
+
handle_undo_and_publish(sym, params, result)
|
|
42
84
|
result
|
|
43
85
|
end
|
|
44
86
|
else
|
|
45
87
|
result = super(params)
|
|
46
|
-
|
|
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.
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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?(:
|
|
144
|
-
klass.
|
|
145
|
-
|
|
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
|
-
|
|
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
|
|
data/lib/rage_arch/version.rb
CHANGED
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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.
|
|
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:
|
|
69
|
-
|
|
70
|
-
and
|
|
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:
|
|
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
|