rage_arch 0.1.3 → 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 +222 -18
- 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/dep_scanner.rb +1 -1
- data/lib/rage_arch/railtie.rb +9 -13
- 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 +105 -38
- data/lib/rage_arch/version.rb +1 -1
- data/lib/rage_arch.rb +38 -4
- 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,39 +35,165 @@ result.errors # => ["Validation error"]
|
|
|
35
35
|
|
|
36
36
|
---
|
|
37
37
|
|
|
38
|
+
### `RageArch::Container` — dependency registration
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
# Register by instance
|
|
42
|
+
RageArch.register(:order_store, MyApp::Deps::OrderStore.new)
|
|
43
|
+
|
|
44
|
+
# Register with a block (lazy evaluation)
|
|
45
|
+
RageArch.register(:mailer) { Mailer.new }
|
|
46
|
+
|
|
47
|
+
# Register an ActiveRecord model as dep (wraps it automatically)
|
|
48
|
+
RageArch.register_ar(:user_store, User)
|
|
49
|
+
|
|
50
|
+
# Resolve
|
|
51
|
+
RageArch.resolve(:order_store) # => the registered implementation
|
|
52
|
+
|
|
53
|
+
# Check if registered
|
|
54
|
+
RageArch.registered?(:order_store) # => true
|
|
55
|
+
```
|
|
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
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
### Dependencies (Deps)
|
|
69
|
+
|
|
70
|
+
A dep is any object that a use case needs from the outside world: persistence, mailers, external APIs, caches, etc. No base class required — any Ruby object can be a dep.
|
|
71
|
+
|
|
72
|
+
#### Writing a dep manually
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
# app/deps/posts/post_store.rb
|
|
76
|
+
module Posts
|
|
77
|
+
class PostStore
|
|
78
|
+
def build(attrs = {})
|
|
79
|
+
Post.new(attrs)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def save(record)
|
|
83
|
+
record.save
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def find(id)
|
|
87
|
+
Post.find_by(id: id)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def list(filters: {})
|
|
91
|
+
Post.where(filters).to_a
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Auto-registered by convention from `app/deps/` — no manual registration needed.
|
|
98
|
+
|
|
99
|
+
#### Generating a dep from use case analysis
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
rails g rage_arch:dep post_store
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Scans your use cases for method calls on `:post_store` and generates a class with stub methods for each one. If the file already exists, only missing methods are added.
|
|
106
|
+
|
|
107
|
+
#### Switching dep implementations
|
|
108
|
+
|
|
109
|
+
Use `dep_switch` to swap between multiple implementations of the same dep:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# Interactive — lists all available implementations and prompts you to choose
|
|
113
|
+
rails g rage_arch:dep_switch post_store
|
|
114
|
+
|
|
115
|
+
# Direct — activate a specific implementation
|
|
116
|
+
rails g rage_arch:dep_switch post_store PostgresPostStore
|
|
117
|
+
|
|
118
|
+
# Switch to ActiveRecord adapter
|
|
119
|
+
rails g rage_arch:dep_switch post_store ar
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The generator scans `app/deps/` for files matching the symbol, updates `config/initializers/rage_arch.rb` by commenting out the old registration and adding the new one.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
38
126
|
### `RageArch::UseCase::Base` — use cases
|
|
39
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
|
+
|
|
40
130
|
```ruby
|
|
41
|
-
class
|
|
42
|
-
|
|
43
|
-
deps :order_store, :notifications
|
|
131
|
+
class Orders::Create < RageArch::UseCase::Base
|
|
132
|
+
# symbol :orders_create is inferred automatically
|
|
133
|
+
deps :order_store, :notifications
|
|
44
134
|
|
|
45
135
|
def call(params = {})
|
|
46
136
|
order = order_store.build(params)
|
|
47
137
|
return failure(order.errors) unless order_store.save(order)
|
|
48
138
|
notifications.notify(:order_created, order)
|
|
49
|
-
success(order)
|
|
139
|
+
success(order: order)
|
|
50
140
|
end
|
|
51
141
|
end
|
|
52
142
|
```
|
|
53
143
|
|
|
144
|
+
Symbol inference: `Orders::Create` becomes `:orders_create`. Explicit `use_case_symbol :my_symbol` still works as override.
|
|
145
|
+
|
|
54
146
|
Build and run manually:
|
|
55
147
|
|
|
56
148
|
```ruby
|
|
57
|
-
use_case = RageArch::UseCase::Base.build(:
|
|
149
|
+
use_case = RageArch::UseCase::Base.build(:orders_create)
|
|
58
150
|
result = use_case.call(reference: "REF-1", total_cents: 1000)
|
|
59
151
|
```
|
|
60
152
|
|
|
61
153
|
---
|
|
62
154
|
|
|
63
|
-
###
|
|
155
|
+
### undo — automatic rollback on failure
|
|
156
|
+
|
|
157
|
+
Define `def undo(value)` on any use case. If `call` returns `failure(...)`, `undo` is called automatically:
|
|
64
158
|
|
|
65
159
|
```ruby
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
69
173
|
```
|
|
70
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
|
+
|
|
71
197
|
---
|
|
72
198
|
|
|
73
199
|
### `RageArch::Controller` — thin controller mixin
|
|
@@ -84,17 +210,28 @@ end
|
|
|
84
210
|
- `run_result(symbol, params)` — runs and returns the `Result` directly
|
|
85
211
|
- `flash_errors(result)` — sets `flash.now[:alert]` from `result.errors`
|
|
86
212
|
|
|
213
|
+
**API controller example (JSON):**
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
class Api::PostsController < ApplicationController
|
|
217
|
+
def create
|
|
218
|
+
run :posts_create, post_params,
|
|
219
|
+
success: ->(r) { render json: r.value[:post], status: :created },
|
|
220
|
+
failure: ->(r) { render json: { errors: r.errors }, status: :unprocessable_entity }
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
```
|
|
224
|
+
|
|
87
225
|
---
|
|
88
226
|
|
|
89
227
|
### `RageArch::EventPublisher` — domain events
|
|
90
228
|
|
|
91
|
-
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:**
|
|
92
230
|
|
|
93
231
|
```ruby
|
|
94
232
|
class Notifications::SendPostCreatedEmail < RageArch::UseCase::Base
|
|
95
|
-
use_case_symbol :send_post_created_email
|
|
96
233
|
deps :mailer
|
|
97
|
-
subscribe :posts_create #
|
|
234
|
+
subscribe :posts_create # async by default
|
|
98
235
|
|
|
99
236
|
def call(payload = {})
|
|
100
237
|
return success unless payload[:success]
|
|
@@ -104,6 +241,12 @@ class Notifications::SendPostCreatedEmail < RageArch::UseCase::Base
|
|
|
104
241
|
end
|
|
105
242
|
```
|
|
106
243
|
|
|
244
|
+
**Synchronous subscribers** (opt-in):
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
subscribe :posts_create, async: false
|
|
248
|
+
```
|
|
249
|
+
|
|
107
250
|
Subscribe to multiple events or everything:
|
|
108
251
|
|
|
109
252
|
```ruby
|
|
@@ -123,7 +266,6 @@ skip_auto_publish
|
|
|
123
266
|
|
|
124
267
|
```ruby
|
|
125
268
|
class CreateOrderWithNotification < RageArch::UseCase::Base
|
|
126
|
-
use_case_symbol :create_order_with_notification
|
|
127
269
|
deps :order_store
|
|
128
270
|
use_cases :orders_create, :notifications_send
|
|
129
271
|
|
|
@@ -147,8 +289,10 @@ end
|
|
|
147
289
|
| `rails g rage_arch:scaffold Post title:string --api` | Same but API-only (JSON responses) |
|
|
148
290
|
| `rails g rage_arch:scaffold Post title:string --skip-model` | Skip model/migration if it already exists |
|
|
149
291
|
| `rails g rage_arch:use_case CreateOrder` | Generates a base use case file |
|
|
292
|
+
| `rails g rage_arch:use_case orders/create` | Generates a namespaced use case (`Orders::Create`) |
|
|
150
293
|
| `rails g rage_arch:dep post_store` | Generates a dep class by scanning method calls in use cases |
|
|
151
|
-
| `rails g rage_arch:
|
|
294
|
+
| `rails g rage_arch:dep_switch post_store` | Lists implementations and switches which one is registered |
|
|
295
|
+
| `rails g rage_arch:dep_switch post_store PostgresPostStore` | Directly activates a specific implementation |
|
|
152
296
|
|
|
153
297
|
---
|
|
154
298
|
|
|
@@ -157,7 +301,23 @@ end
|
|
|
157
301
|
```ruby
|
|
158
302
|
# spec/rails_helper.rb
|
|
159
303
|
require "rage_arch/rspec_matchers"
|
|
304
|
+
require "rage_arch/rspec_helpers"
|
|
160
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
|
|
161
321
|
```
|
|
162
322
|
|
|
163
323
|
**Result matchers:**
|
|
@@ -179,23 +339,67 @@ publisher.clear
|
|
|
179
339
|
|
|
180
340
|
---
|
|
181
341
|
|
|
342
|
+
## Configuration
|
|
343
|
+
|
|
344
|
+
```ruby
|
|
345
|
+
# config/application.rb or config/initializers/rage_arch.rb
|
|
346
|
+
|
|
347
|
+
# Disable automatic event publishing when use cases finish (default: true)
|
|
348
|
+
config.rage_arch.auto_publish_events = false
|
|
349
|
+
|
|
350
|
+
# Disable boot verification (default: true)
|
|
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
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
182
359
|
## Boot verification
|
|
183
360
|
|
|
184
|
-
At boot, `RageArch.verify_deps!` runs automatically and raises if
|
|
361
|
+
At boot, `RageArch.verify_deps!` runs automatically and raises if it finds wiring problems. It checks:
|
|
362
|
+
|
|
363
|
+
- Every dep declared with `deps :symbol` is registered in the container (or auto-resolved for `_store` deps)
|
|
364
|
+
- Every method called on a dep is implemented by the registered object (via static analysis)
|
|
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
|
|
367
|
+
|
|
368
|
+
Example error output:
|
|
369
|
+
|
|
370
|
+
```
|
|
371
|
+
RageArch boot verification failed:
|
|
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
|
|
374
|
+
UseCase :posts_notify (Posts::Notify) declares use_cases :email_send — not registered in use case registry
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Disable with `config.rage_arch.verify_deps = false`.
|
|
185
378
|
|
|
186
379
|
---
|
|
187
380
|
|
|
188
381
|
## Instrumentation
|
|
189
382
|
|
|
190
|
-
Every use case emits `"
|
|
383
|
+
Every use case emits `"rage_arch.use_case.run"` via `ActiveSupport::Notifications` with payload `symbol`, `params`, `success`, `errors`, `result`.
|
|
384
|
+
|
|
385
|
+
```ruby
|
|
386
|
+
ActiveSupport::Notifications.subscribe("rage_arch.use_case.run") do |*args|
|
|
387
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
388
|
+
Rails.logger.info "[UseCase] #{event.payload[:symbol]} (#{event.duration.round}ms) success=#{event.payload[:success]}"
|
|
389
|
+
end
|
|
390
|
+
```
|
|
191
391
|
|
|
192
392
|
---
|
|
193
393
|
|
|
194
394
|
## Documentation
|
|
195
395
|
|
|
196
|
-
- [`doc/REFERENCE.md`](doc/REFERENCE.md) — Full API reference with all options and examples
|
|
197
|
-
- [`doc/DOCUMENTATION.md`](doc/DOCUMENTATION.md) — Detailed behaviour (use cases, deps, events, config)
|
|
198
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.
|
|
199
403
|
|
|
200
404
|
## License
|
|
201
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
|
|
@@ -4,7 +4,7 @@ require "set"
|
|
|
4
4
|
|
|
5
5
|
module RageArch
|
|
6
6
|
# Scans use case files to find dep symbols and the methods called on each dep.
|
|
7
|
-
# Used by the
|
|
7
|
+
# Used by the rage_arch:dep generator to create stub classes with the right methods.
|
|
8
8
|
# Also tracks which use case path each symbol appears in (for folder inference).
|
|
9
9
|
class DepScanner
|
|
10
10
|
def initialize(use_cases_root = nil)
|
data/lib/rage_arch/railtie.rb
CHANGED
|
@@ -5,27 +5,23 @@ require_relative "controller"
|
|
|
5
5
|
|
|
6
6
|
module RageArch
|
|
7
7
|
class Railtie < ::Rails::Railtie
|
|
8
|
-
config.
|
|
9
|
-
config.
|
|
10
|
-
config.
|
|
8
|
+
config.rage_arch = ActiveSupport::OrderedOptions.new
|
|
9
|
+
config.rage_arch.auto_publish_events = true
|
|
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.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
|
-
ActiveSupport::Notifications.instrument("
|
|
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)
|
|
@@ -66,8 +118,8 @@ module RageArch
|
|
|
66
118
|
|
|
67
119
|
def auto_publish_enabled?
|
|
68
120
|
return false if self.class.skip_auto_publish?
|
|
69
|
-
return true unless defined?(Rails) && Rails.application.config.respond_to?(:
|
|
70
|
-
Rails.application.config.
|
|
121
|
+
return true unless defined?(Rails) && Rails.application.config.respond_to?(:rage_arch) && Rails.application.config.rage_arch
|
|
122
|
+
Rails.application.config.rage_arch.auto_publish_events != false
|
|
71
123
|
end
|
|
72
124
|
end
|
|
73
125
|
|
|
@@ -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,22 +30,49 @@ 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
|
-
# (done automatically by the Railtie unless config.
|
|
44
|
+
# (done automatically by the Railtie unless config.rage_arch.verify_deps = false).
|
|
35
45
|
#
|
|
36
46
|
# Raises RuntimeError listing every missing dep/use_case if any are absent.
|
|
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
|