railsmith 1.0.0 → 1.1.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/CHANGELOG.md +58 -0
- data/MIGRATION.md +276 -6
- data/README.md +35 -31
- data/docs/cookbook.md +146 -103
- data/docs/legacy-adoption.md +24 -30
- data/docs/quickstart.md +27 -11
- data/lib/generators/railsmith/model_service/model_service_generator.rb +69 -48
- data/lib/generators/railsmith/model_service/templates/model_service.rb.tt +18 -14
- data/lib/generators/railsmith/operation/operation_generator.rb +36 -5
- data/lib/generators/railsmith/operation/templates/operation.rb.tt +13 -16
- data/lib/railsmith/base_service/bulk_actions.rb +0 -2
- data/lib/railsmith/base_service/bulk_execution.rb +0 -4
- data/lib/railsmith/base_service/context_propagation.rb +29 -0
- data/lib/railsmith/base_service/crud_actions.rb +16 -0
- data/lib/railsmith/base_service.rb +27 -7
- data/lib/railsmith/context.rb +139 -0
- data/lib/railsmith/cross_domain_guard.rb +6 -6
- data/lib/railsmith/domain_context.rb +15 -36
- data/lib/railsmith/version.rb +1 -1
- data/lib/railsmith.rb +1 -0
- metadata +25 -4
- data/.tool-versions +0 -1
data/docs/cookbook.md
CHANGED
|
@@ -13,37 +13,37 @@ rails generate railsmith:model_service Post
|
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
```ruby
|
|
16
|
-
# app/services/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
model(Post)
|
|
20
|
-
end
|
|
16
|
+
# app/services/post_service.rb
|
|
17
|
+
class PostService < Railsmith::BaseService
|
|
18
|
+
model(Post)
|
|
21
19
|
end
|
|
22
20
|
```
|
|
23
21
|
|
|
24
|
-
The
|
|
22
|
+
The five default actions work immediately (`context:` is optional on all calls):
|
|
25
23
|
|
|
26
24
|
```ruby
|
|
27
25
|
# Create
|
|
28
|
-
result =
|
|
26
|
+
result = PostService.call(
|
|
29
27
|
action: :create,
|
|
30
|
-
params: { attributes: { title: "Hello", body: "World" } }
|
|
31
|
-
context: {}
|
|
28
|
+
params: { attributes: { title: "Hello", body: "World" } }
|
|
32
29
|
)
|
|
33
30
|
|
|
34
31
|
# Update
|
|
35
|
-
result =
|
|
32
|
+
result = PostService.call(
|
|
36
33
|
action: :update,
|
|
37
|
-
params: { id: 1, attributes: { title: "Updated" } }
|
|
38
|
-
context: {}
|
|
34
|
+
params: { id: 1, attributes: { title: "Updated" } }
|
|
39
35
|
)
|
|
40
36
|
|
|
41
37
|
# Destroy
|
|
42
|
-
result =
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
38
|
+
result = PostService.call(action: :destroy, params: { id: 1 })
|
|
39
|
+
|
|
40
|
+
# Find a single record by ID
|
|
41
|
+
result = PostService.call(action: :find, params: { id: 1 })
|
|
42
|
+
result.value # => <Post id=1>
|
|
43
|
+
|
|
44
|
+
# List all records
|
|
45
|
+
result = PostService.call(action: :list, params: {})
|
|
46
|
+
result.value # => [<Post>, ...]
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
---
|
|
@@ -53,15 +53,13 @@ result = Operations::PostService.call(
|
|
|
53
53
|
Override `sanitize_attributes` to strip or transform attributes before the record is written:
|
|
54
54
|
|
|
55
55
|
```ruby
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
model(Post)
|
|
56
|
+
class PostService < Railsmith::BaseService
|
|
57
|
+
model(Post)
|
|
59
58
|
|
|
60
|
-
|
|
59
|
+
private
|
|
61
60
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
end
|
|
61
|
+
def sanitize_attributes(attributes)
|
|
62
|
+
attributes.except(:admin_override, :internal_flag)
|
|
65
63
|
end
|
|
66
64
|
end
|
|
67
65
|
```
|
|
@@ -79,15 +77,28 @@ end
|
|
|
79
77
|
### Custom finder logic
|
|
80
78
|
|
|
81
79
|
```ruby
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
model(Post)
|
|
80
|
+
class PostService < Railsmith::BaseService
|
|
81
|
+
model(Post)
|
|
85
82
|
|
|
86
|
-
|
|
83
|
+
private
|
|
87
84
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
85
|
+
def find_record(model_klass, id)
|
|
86
|
+
model_klass.published.find_by(id: id)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Filtering list results
|
|
92
|
+
|
|
93
|
+
Override `list` to apply scopes or filters:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
class PostService < Railsmith::BaseService
|
|
97
|
+
model(Post)
|
|
98
|
+
|
|
99
|
+
def list
|
|
100
|
+
posts = Post.where(published: params[:published]).order(:created_at)
|
|
101
|
+
Result.success(value: posts)
|
|
91
102
|
end
|
|
92
103
|
end
|
|
93
104
|
```
|
|
@@ -99,22 +110,20 @@ end
|
|
|
99
110
|
Define a method with the action name. Use `Result` and `Errors` directly:
|
|
100
111
|
|
|
101
112
|
```ruby
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
model(Post)
|
|
113
|
+
class PostService < Railsmith::BaseService
|
|
114
|
+
model(Post)
|
|
105
115
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
116
|
+
def publish
|
|
117
|
+
id = params[:id]
|
|
118
|
+
return Result.failure(error: Errors.validation_error(details: { missing: ["id"] })) unless id
|
|
109
119
|
|
|
110
|
-
|
|
111
|
-
|
|
120
|
+
post = Post.find_by(id: id)
|
|
121
|
+
return Result.failure(error: Errors.not_found(message: "Post not found", details: { id: id })) unless post
|
|
112
122
|
|
|
113
|
-
|
|
123
|
+
return Result.failure(error: Errors.conflict(message: "Already published")) if post.published?
|
|
114
124
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
end
|
|
125
|
+
post.update!(published_at: Time.current)
|
|
126
|
+
Result.success(value: post)
|
|
118
127
|
end
|
|
119
128
|
end
|
|
120
129
|
```
|
|
@@ -122,11 +131,7 @@ end
|
|
|
122
131
|
Call it the same way:
|
|
123
132
|
|
|
124
133
|
```ruby
|
|
125
|
-
result =
|
|
126
|
-
action: :publish,
|
|
127
|
-
params: { id: 42 },
|
|
128
|
-
context: {}
|
|
129
|
-
)
|
|
134
|
+
result = PostService.call(action: :publish, params: { id: 42 })
|
|
130
135
|
```
|
|
131
136
|
|
|
132
137
|
---
|
|
@@ -136,30 +141,29 @@ result = Operations::PostService.call(
|
|
|
136
141
|
Pass `context` through to preserve domain tracking:
|
|
137
142
|
|
|
138
143
|
```ruby
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
)
|
|
157
|
-
result
|
|
158
|
-
end
|
|
144
|
+
class OrderService < Railsmith::BaseService
|
|
145
|
+
model(Order)
|
|
146
|
+
|
|
147
|
+
def place
|
|
148
|
+
# Validate stock via another service
|
|
149
|
+
stock_result = InventoryService.call(
|
|
150
|
+
action: :reserve,
|
|
151
|
+
params: { sku: params[:sku], qty: params[:qty] },
|
|
152
|
+
context: context # forward the same context
|
|
153
|
+
)
|
|
154
|
+
return stock_result if stock_result.failure?
|
|
155
|
+
|
|
156
|
+
OrderService.call(
|
|
157
|
+
action: :create,
|
|
158
|
+
params: { attributes: { sku: params[:sku], qty: params[:qty] } },
|
|
159
|
+
context: context
|
|
160
|
+
)
|
|
159
161
|
end
|
|
160
162
|
end
|
|
161
163
|
```
|
|
162
164
|
|
|
165
|
+
Alternatively, use thread-local context propagation (see [Thread-local context](#thread-local-context-propagation)) so you don't need to thread `context:` through every call.
|
|
166
|
+
|
|
163
167
|
---
|
|
164
168
|
|
|
165
169
|
## Bulk Operations
|
|
@@ -167,7 +171,7 @@ end
|
|
|
167
171
|
### bulk_create
|
|
168
172
|
|
|
169
173
|
```ruby
|
|
170
|
-
result =
|
|
174
|
+
result = UserService.call(
|
|
171
175
|
action: :bulk_create,
|
|
172
176
|
params: {
|
|
173
177
|
items: [
|
|
@@ -197,7 +201,7 @@ end
|
|
|
197
201
|
Each item must include an `id` key and an `attributes` key:
|
|
198
202
|
|
|
199
203
|
```ruby
|
|
200
|
-
result =
|
|
204
|
+
result = UserService.call(
|
|
201
205
|
action: :bulk_update,
|
|
202
206
|
params: {
|
|
203
207
|
items: [
|
|
@@ -217,14 +221,14 @@ Pass IDs directly or as hashes:
|
|
|
217
221
|
|
|
218
222
|
```ruby
|
|
219
223
|
# Array of IDs
|
|
220
|
-
result =
|
|
224
|
+
result = UserService.call(
|
|
221
225
|
action: :bulk_destroy,
|
|
222
226
|
params: { items: [1, 2, 3] },
|
|
223
227
|
context: {}
|
|
224
228
|
)
|
|
225
229
|
|
|
226
230
|
# Array of hashes
|
|
227
|
-
result =
|
|
231
|
+
result = UserService.call(
|
|
228
232
|
action: :bulk_destroy,
|
|
229
233
|
params: { items: [{ id: 1 }, { id: 2 }] },
|
|
230
234
|
context: {}
|
|
@@ -242,7 +246,7 @@ result = Operations::UserService.call(
|
|
|
242
246
|
|
|
243
247
|
```ruby
|
|
244
248
|
# All-or-nothing import
|
|
245
|
-
result =
|
|
249
|
+
result = UserService.call(
|
|
246
250
|
action: :bulk_create,
|
|
247
251
|
params: {
|
|
248
252
|
items: rows,
|
|
@@ -309,7 +313,7 @@ module Billing
|
|
|
309
313
|
module Services
|
|
310
314
|
class InvoiceService < Railsmith::BaseService
|
|
311
315
|
model(Billing::Invoice)
|
|
312
|
-
|
|
316
|
+
domain :billing
|
|
313
317
|
end
|
|
314
318
|
end
|
|
315
319
|
end
|
|
@@ -317,13 +321,16 @@ end
|
|
|
317
321
|
|
|
318
322
|
---
|
|
319
323
|
|
|
320
|
-
### Pass domain context on
|
|
324
|
+
### Pass domain context on a call
|
|
325
|
+
|
|
326
|
+
Use `Railsmith::Context` to attach domain and tracing data. Extra keys (`actor_id`, `request_id`, etc.) are top-level — no nested `:meta` hash:
|
|
321
327
|
|
|
322
328
|
```ruby
|
|
323
|
-
ctx = Railsmith::
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
329
|
+
ctx = Railsmith::Context.new(
|
|
330
|
+
domain: :billing,
|
|
331
|
+
actor_id: current_user.id
|
|
332
|
+
# request_id is auto-generated as a UUID when omitted
|
|
333
|
+
)
|
|
327
334
|
|
|
328
335
|
result = Billing::Services::InvoiceService.call(
|
|
329
336
|
action: :create,
|
|
@@ -332,13 +339,45 @@ result = Billing::Services::InvoiceService.call(
|
|
|
332
339
|
)
|
|
333
340
|
```
|
|
334
341
|
|
|
335
|
-
|
|
342
|
+
To forward an existing request ID (e.g. from an HTTP header):
|
|
343
|
+
|
|
344
|
+
```ruby
|
|
345
|
+
ctx = Railsmith::Context.new(
|
|
346
|
+
domain: :billing,
|
|
347
|
+
request_id: request.headers["X-Request-Id"],
|
|
348
|
+
actor_id: current_user.id
|
|
349
|
+
)
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
### Thread-local context propagation
|
|
355
|
+
|
|
356
|
+
Set context once at the edge of a request instead of threading it through every call:
|
|
357
|
+
|
|
358
|
+
```ruby
|
|
359
|
+
# app/controllers/application_controller.rb
|
|
360
|
+
around_action do |_, block|
|
|
361
|
+
Railsmith::Context.with(domain: :web, actor_id: current_user&.id) { block.call }
|
|
362
|
+
end
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Services automatically inherit the thread-local context when no explicit `context:` is passed:
|
|
366
|
+
|
|
367
|
+
```ruby
|
|
368
|
+
# No context: needed — picked up from Context.with above
|
|
369
|
+
UserService.call(action: :create, params: { attributes: { name: "Alice" } })
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Resolution order: **explicit `context:` arg > `Context.current` > auto-built context**.
|
|
373
|
+
|
|
374
|
+
`Context.with` restores the previous value after the block, making it safe for nested calls and concurrent requests.
|
|
336
375
|
|
|
337
376
|
---
|
|
338
377
|
|
|
339
378
|
### Cross-domain detection
|
|
340
379
|
|
|
341
|
-
When
|
|
380
|
+
When the context domain differs from a service's declared `domain`, Railsmith emits a warning. By default this is non-blocking.
|
|
342
381
|
|
|
343
382
|
```ruby
|
|
344
383
|
# context says :catalog, service declares :billing — warning fires
|
|
@@ -391,35 +430,39 @@ Allowlisted pairs do not emit warnings.
|
|
|
391
430
|
rails generate railsmith:operation Billing::Invoices::Finalize
|
|
392
431
|
```
|
|
393
432
|
|
|
394
|
-
Creates `app/domains/billing/
|
|
433
|
+
Creates `app/domains/billing/invoices/finalize.rb`:
|
|
395
434
|
|
|
396
435
|
```ruby
|
|
397
436
|
module Billing
|
|
398
|
-
module
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
current_domain = Railsmith::DomainContext.normalize_current_domain(context[:current_domain])
|
|
414
|
-
# Your logic here
|
|
415
|
-
Railsmith::Result.success(value: { current_domain: current_domain })
|
|
416
|
-
end
|
|
437
|
+
module Invoices
|
|
438
|
+
class Finalize
|
|
439
|
+
def self.call(params: {}, context: {})
|
|
440
|
+
new(params:, context:).call
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
attr_reader :params, :context
|
|
444
|
+
|
|
445
|
+
def initialize(params:, context:)
|
|
446
|
+
@params = Railsmith.deep_dup(params || {})
|
|
447
|
+
@context = Railsmith::Context.build(context)
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def call
|
|
451
|
+
Railsmith::Result.success(value: {})
|
|
417
452
|
end
|
|
418
453
|
end
|
|
419
454
|
end
|
|
420
455
|
end
|
|
421
456
|
```
|
|
422
457
|
|
|
458
|
+
To keep the old `Operations` interstitial module:
|
|
459
|
+
|
|
460
|
+
```bash
|
|
461
|
+
rails generate railsmith:operation Billing::Invoices::Finalize --namespace=Operations
|
|
462
|
+
# => app/domains/billing/operations/invoices/finalize.rb
|
|
463
|
+
# => Billing::Operations::Invoices::Finalize
|
|
464
|
+
```
|
|
465
|
+
|
|
423
466
|
---
|
|
424
467
|
|
|
425
468
|
## Error Mapping
|
|
@@ -494,10 +537,10 @@ The default `create`, `update`, and `destroy` actions catch and map these except
|
|
|
494
537
|
```ruby
|
|
495
538
|
class UsersController < ApplicationController
|
|
496
539
|
def create
|
|
497
|
-
result =
|
|
540
|
+
result = UserService.call(
|
|
498
541
|
action: :create,
|
|
499
542
|
params: { attributes: user_params },
|
|
500
|
-
context:
|
|
543
|
+
context: Railsmith::Context.new(domain: :identity)
|
|
501
544
|
)
|
|
502
545
|
|
|
503
546
|
if result.success?
|
data/docs/legacy-adoption.md
CHANGED
|
@@ -61,7 +61,7 @@ Pick the simplest model from your backlog (few validations, no callbacks). Gener
|
|
|
61
61
|
rails generate railsmith:model_service Post
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
Open `app/services/operations/post_service.rb
|
|
64
|
+
Open `app/services/post_service.rb` (or `app/services/operations/post_service.rb` if you passed `--namespace=Operations`). For now, leave it as generated — the default CRUD actions are enough for the first replacement.
|
|
65
65
|
|
|
66
66
|
Find the controller that creates posts. Replace the inline model call:
|
|
67
67
|
|
|
@@ -83,10 +83,9 @@ end
|
|
|
83
83
|
|
|
84
84
|
```ruby
|
|
85
85
|
def create
|
|
86
|
-
result =
|
|
86
|
+
result = PostService.call(
|
|
87
87
|
action: :create,
|
|
88
|
-
params: { attributes: post_params.to_h }
|
|
89
|
-
context: {}
|
|
88
|
+
params: { attributes: post_params.to_h }
|
|
90
89
|
)
|
|
91
90
|
|
|
92
91
|
if result.success?
|
|
@@ -126,20 +125,18 @@ end
|
|
|
126
125
|
New service action:
|
|
127
126
|
|
|
128
127
|
```ruby
|
|
129
|
-
# app/services/
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
Result.success(value: user)
|
|
142
|
-
end
|
|
128
|
+
# app/services/user_service.rb
|
|
129
|
+
class UserService < Railsmith::BaseService
|
|
130
|
+
model(User)
|
|
131
|
+
|
|
132
|
+
def upgrade
|
|
133
|
+
id = params[:id]
|
|
134
|
+
user = User.find_by(id: id)
|
|
135
|
+
return Result.failure(error: Errors.not_found(details: { id: id })) unless user
|
|
136
|
+
|
|
137
|
+
user.update!(plan: "paid", upgraded_at: Time.current)
|
|
138
|
+
BillingMailer.upgraded(user).deliver_later
|
|
139
|
+
Result.success(value: user)
|
|
143
140
|
end
|
|
144
141
|
end
|
|
145
142
|
```
|
|
@@ -148,10 +145,9 @@ Controller:
|
|
|
148
145
|
|
|
149
146
|
```ruby
|
|
150
147
|
def upgrade
|
|
151
|
-
result =
|
|
148
|
+
result = UserService.call(
|
|
152
149
|
action: :upgrade,
|
|
153
|
-
params: { id: params[:id] }
|
|
154
|
-
context: {}
|
|
150
|
+
params: { id: params[:id] }
|
|
155
151
|
)
|
|
156
152
|
result.success? ? redirect_to result.value : redirect_back(fallback_location: root_path)
|
|
157
153
|
end
|
|
@@ -169,14 +165,14 @@ rails generate railsmith:model_service Billing::Invoice --domain=Billing
|
|
|
169
165
|
rails generate railsmith:model_service Billing::Payment --domain=Billing
|
|
170
166
|
```
|
|
171
167
|
|
|
172
|
-
Move the service files from `app/services/operations/`
|
|
168
|
+
Move the service files from `app/services/` (or `app/services/operations/` if you used the old default) into `app/domains/billing/services/` and add `domain`:
|
|
173
169
|
|
|
174
170
|
```ruby
|
|
175
171
|
module Billing
|
|
176
172
|
module Services
|
|
177
173
|
class InvoiceService < Railsmith::BaseService
|
|
178
174
|
model(Billing::Invoice)
|
|
179
|
-
|
|
175
|
+
domain :billing
|
|
180
176
|
end
|
|
181
177
|
end
|
|
182
178
|
end
|
|
@@ -231,14 +227,13 @@ Use this checklist per model:
|
|
|
231
227
|
**For services**, write isolated unit specs:
|
|
232
228
|
|
|
233
229
|
```ruby
|
|
234
|
-
# spec/services/
|
|
235
|
-
RSpec.describe
|
|
230
|
+
# spec/services/post_service_spec.rb
|
|
231
|
+
RSpec.describe PostService do
|
|
236
232
|
describe "#create" do
|
|
237
233
|
it "creates a post" do
|
|
238
234
|
result = described_class.call(
|
|
239
235
|
action: :create,
|
|
240
|
-
params: { attributes: { title: "Hello", body: "World" } }
|
|
241
|
-
context: {}
|
|
236
|
+
params: { attributes: { title: "Hello", body: "World" } }
|
|
242
237
|
)
|
|
243
238
|
expect(result).to be_success
|
|
244
239
|
expect(result.value).to be_a(Post)
|
|
@@ -248,8 +243,7 @@ RSpec.describe Operations::PostService do
|
|
|
248
243
|
it "returns validation_error when title is blank" do
|
|
249
244
|
result = described_class.call(
|
|
250
245
|
action: :create,
|
|
251
|
-
params: { attributes: { title: "", body: "World" } }
|
|
252
|
-
context: {}
|
|
246
|
+
params: { attributes: { title: "", body: "World" } }
|
|
253
247
|
)
|
|
254
248
|
expect(result).to be_failure
|
|
255
249
|
expect(result.code).to eq("validation_error")
|
|
@@ -274,7 +268,7 @@ return Result.failure(error: Errors.validation_error(
|
|
|
274
268
|
```
|
|
275
269
|
|
|
276
270
|
**Forgetting to forward `context:`.**
|
|
277
|
-
When a service calls another service,
|
|
271
|
+
When a service calls another service, pass `context: context` unless you rely on thread-local `Railsmith::Context.with` at the controller edge — then nested calls inherit the same context automatically.
|
|
278
272
|
|
|
279
273
|
**Wrapping service calls in rescue.**
|
|
280
274
|
Don't. Services return `Result.failure` for all expected error conditions. Rescuing exceptions at the caller layer bypasses error mapping and loses structure.
|
data/docs/quickstart.md
CHANGED
|
@@ -14,7 +14,7 @@ Then:
|
|
|
14
14
|
bundle install
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
**Requirements**: Ruby >= 3.
|
|
17
|
+
**Requirements**: Ruby >= 3.1.0, Rails 7.0–8.x.
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
@@ -48,27 +48,32 @@ end
|
|
|
48
48
|
rails generate railsmith:model_service User
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
Creates `app/services/
|
|
51
|
+
Creates `app/services/user_service.rb`:
|
|
52
52
|
|
|
53
53
|
```ruby
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
model(User)
|
|
57
|
-
end
|
|
54
|
+
class UserService < Railsmith::BaseService
|
|
55
|
+
model(User)
|
|
58
56
|
end
|
|
59
57
|
```
|
|
60
58
|
|
|
61
|
-
The `model` declaration wires up the
|
|
59
|
+
The `model` declaration wires up the default CRUD actions (`create`, `update`, `destroy`, `find`, `list`) and the three bulk actions (`bulk_create`, `bulk_update`, `bulk_destroy`) automatically — no extra code needed for standard cases.
|
|
60
|
+
|
|
61
|
+
To generate under a namespace, pass `--namespace`:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
rails generate railsmith:model_service User --namespace=Operations
|
|
65
|
+
# => app/services/operations/user_service.rb
|
|
66
|
+
# => module Operations; class UserService
|
|
67
|
+
```
|
|
62
68
|
|
|
63
69
|
---
|
|
64
70
|
|
|
65
71
|
## 4. Make Your First Call
|
|
66
72
|
|
|
67
73
|
```ruby
|
|
68
|
-
result =
|
|
74
|
+
result = UserService.call(
|
|
69
75
|
action: :create,
|
|
70
|
-
params: { attributes: { name: "Alice", email: "alice@example.com" } }
|
|
71
|
-
context: {}
|
|
76
|
+
params: { attributes: { name: "Alice", email: "alice@example.com" } }
|
|
72
77
|
)
|
|
73
78
|
|
|
74
79
|
if result.success?
|
|
@@ -81,6 +86,16 @@ end
|
|
|
81
86
|
|
|
82
87
|
Every service call returns a `Railsmith::Result`. You never rescue exceptions from service calls — failures surface as structured `Result` objects.
|
|
83
88
|
|
|
89
|
+
`context:` is optional. When omitted, Railsmith builds a context automatically (with an auto-generated `request_id`). Pass one explicitly to attach domain, actor, or tracing data:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
UserService.call(
|
|
93
|
+
action: :create,
|
|
94
|
+
params: { attributes: { name: "Alice", email: "alice@example.com" } },
|
|
95
|
+
context: Railsmith::Context.new(domain: :identity, actor_id: current_user.id)
|
|
96
|
+
)
|
|
97
|
+
```
|
|
98
|
+
|
|
84
99
|
---
|
|
85
100
|
|
|
86
101
|
## 5. Result Contract at a Glance
|
|
@@ -106,5 +121,6 @@ result.error.to_h # => { code: ..., message: ..., details: ... }
|
|
|
106
121
|
|
|
107
122
|
## 6. Next Steps
|
|
108
123
|
|
|
109
|
-
- **[Cookbook](cookbook.md)** — CRUD customization, bulk operations, domain context, error mapping, custom actions.
|
|
124
|
+
- **[Cookbook](cookbook.md)** — CRUD customization, bulk operations, domain context, thread-local context, error mapping, custom actions.
|
|
110
125
|
- **[Legacy Adoption Guide](legacy-adoption.md)** — Incrementally migrate an existing Rails app to Railsmith.
|
|
126
|
+
- **[Migration Guide](../MIGRATION.md)** — Upgrade notes from 1.0.0 to 1.1.0.
|