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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b7d121f9233f121988b7811ac803943a3d2d784ccf018e31ecb1dd681747c632
4
- data.tar.gz: 0d8b3debcd71cd67bec213d7614528d61584b45f1a380b35536d5e738d848871
3
+ metadata.gz: 1a20ec84a09c4074cd56944e07f4ea7f14ffab15e8f9fe41d4f288ac3cc2f9ce
4
+ data.tar.gz: 47e886d32518f52ba779f4ce0727737653b7e719d7b266ca8a2769ab7ff2bb97
5
5
  SHA512:
6
- metadata.gz: 6c82716d751374815ebe15f4fdc0763c3ff79dbf58b1985d9d8a2bbaa70e19fe010174563401fc23c811533009ad054c8d0de7130502ce80a10e2346f309d6f7
7
- data.tar.gz: 97bf97006557c7fa4e6f5c19d83d47b81c48dd751bee5d66a6d812ee5072edeeaf5a3e95b8b09ec42bc3ad778e1fae3b886c93c63cbd676240cdfef96b413786
6
+ metadata.gz: 9c6275d694e51d5bfa1d5dd5f375af5079b0fdd96bb38252f322279df514e3e1e7945840ccf9bba14495b4e55cf7c33a4dfbfefce903402e5fe1c7345cae6984
7
+ data.tar.gz: 909725e026330771fd81e0b5fd0077f7f92621dc017cda6b882a7e5f276db8be589edab63857823e1c604af9b58a7a968f90f825b78652022c4a90b6f86c8285
data/CHANGELOG.md CHANGED
@@ -7,6 +7,60 @@ Versioning follows [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ---
9
9
 
10
+ ## [Unreleased]
11
+
12
+ ---
13
+
14
+ ## [1.1.0] — 2026-03-30
15
+
16
+ ### Added
17
+
18
+ - Appraisal-style gemfiles for CI and local testing: `gemfiles/rails_7.gemfile` and `gemfiles/rails_8.gemfile` (with lockfiles), pinning `activerecord` / `railties` to Rails 7.x and 8.x respectively.
19
+ - `--namespace` flag on `railsmith:model_service` and `railsmith:operation` generators.
20
+ Wraps the generated class in the given modules (e.g. `--namespace=Billing::Services`).
21
+ When a namespace is provided on `model_service`, `domain` is automatically set from its first segment.
22
+ Pass `--namespace=Operations` explicitly to match the pre-1.1 default layout.
23
+ - `Railsmith::Context` — canonical context value object (replaces `Railsmith::DomainContext` for new code).
24
+ Accepts `domain:` (preferred) and arbitrary top-level keyword args (`actor_id:`, `request_id:`, etc.) without a nested `:meta` hash.
25
+ `#[]` accessor, `#blank_domain?`, `#to_h` (backward-compatible shape: `{ current_domain: ..., **extras }`).
26
+ - `Context#request_id` — auto-generated UUID (`SecureRandom.uuid`) when no `request_id:` is supplied; explicit values always win (e.g. `X-Request-Id`).
27
+ - `Context.build(value)` — coerces context-like values into a `Context` (`Context` as-is, hash with `:domain` / `:current_domain`, or `nil`/`{}` for a minimal context with auto `request_id`).
28
+ - `Railsmith::Context.current` and `Railsmith::Context.with(**kwargs, &block)` — thread-local context for the request scope; nested blocks restore the previous value.
29
+ - `domain` DSL on `BaseService` subclasses — preferred alias for `service_domain` (aligned with `Context`’s `domain:` kwarg).
30
+ - `find` and `list` actions on model-backed services — `find` returns `Result.success(value: record)` or `not_found`; `list` defaults to `model_class.all` (override for filtering).
31
+
32
+ ### Changed
33
+
34
+ - `railsmith:model_service MODEL` — default output is `app/services/<model>_service.rb` with **no module wrapper** (was `app/services/operations/<model>_service.rb` in `module Operations`). Existing code is unaffected.
35
+ - `railsmith:operation NAME` — default hierarchy is `<Domain>::…::<Operation>` without an interstitial `Operations` module (was `.../operations/...` on disk and in module nesting). Use `--namespace=Operations` for the old layout.
36
+ - `Railsmith::BaseService::ContextPropagation` — renamed from `DomainContextPropagation`; reads both `:current_domain` and `:domain` for compatibility.
37
+ - `context:` on `BaseService.call` is **optional**; omitting it, or passing `nil`/`{}`, yields a real `Context` with an auto `request_id` (no `ArgumentError`).
38
+ - `BaseService.call` context resolution: explicit `context:` > thread-local `Context.current` > auto-built empty context.
39
+ - **Ruby**: minimum supported version is **>= 3.1.0** (was >= 3.2.0). RuboCop `TargetRubyVersion` is aligned to 3.1.
40
+ - `Railsmith::Context` implementation: `.with` / `.build` use `thread_context_from` and `build_from_hash` as `private_class_method`s; `#[]` uses `%i[current_domain domain]` for domain lookup (behavior unchanged).
41
+ - Packaged gem file list: `gemfiles/`, `.ruby-version`, and `.tool-versions` excluded from the gem tarball.
42
+
43
+ ### Deprecated
44
+
45
+ - `Railsmith::DomainContext` — deprecation warning on `.new`; removed in a future major release. Use `Railsmith::Context`.
46
+ - `current_domain:` on `Context.new` — use `domain:` (warning when used).
47
+ - `service_domain` on `BaseService` — use `domain` (warning when used); removed in a future major release.
48
+
49
+ ### Fixed
50
+
51
+ - Removed obsolete `Style/ArgumentsForwarding` RuboCop disables in bulk helpers (`bulk_actions`, `bulk_execution`).
52
+ - `activerecord` is an explicit runtime dependency (`>= 7.0, < 9.0`); avoids opaque `NoMethodError` when CRUD/bulk run outside a full Rails load.
53
+ - Root `Gemfile` pins `connection_pool`, `nokogiri`, `erb`, and `zeitwerk` under `RUBY_VERSION < "3.2"`, matching `gemfiles/rails_7.gemfile`, so Ruby 3.1 resolves without forcing `BUNDLE_GEMFILE`.
54
+ - `railsmith:operation` — `initialize` uses `Railsmith::Context.build(context)` instead of `deep_dup` on context; frozen `Context` instances work; generated stub uses `context[:domain]` only.
55
+ - `railsmith:model_service` — domain mode no longer emits `_service.rb` when the model name omits the domain prefix; generator comments reference `domain`, not `service_domain`.
56
+
57
+ ### Development
58
+
59
+ - GitHub Actions **CI**: lint (RuboCop, Ruby 3.3); test matrix Ruby 3.1–3.3 × Rails 7/8 gemfiles (Ruby 3.1 excluded with Rails 8). `permissions: contents: read`, `fail-fast: false`, `ruby/setup-ruby` `gemfile:` for installs.
60
+ - `railsmith:model_service` generator: file-level `Metrics/ClassLength` RuboCop disable/enable around `ModelServiceGenerator` only.
61
+
62
+ ---
63
+
10
64
  ## [1.0.0] — 2026-03-29
11
65
 
12
66
  First stable release. Public DSL and result contract are now frozen.
@@ -62,3 +116,7 @@ First stable release. Public DSL and result contract are now frozen.
62
116
  ## [0.1.0] — pre-release
63
117
 
64
118
  Internal bootstrap release. Gem skeleton, CI baseline, and initial service scaffolding. Not intended for production use.
119
+
120
+ [Unreleased]: https://github.com/samaswin/railsmith/compare/v1.1.0...HEAD
121
+ [1.1.0]: https://github.com/samaswin/railsmith/compare/v1.0.0...v1.1.0
122
+ [1.0.0]: https://github.com/samaswin/railsmith/releases/tag/v1.0.0
data/MIGRATION.md CHANGED
@@ -1,5 +1,275 @@
1
1
  # Migration Guide
2
2
 
3
+ ## Upgrading from 1.0.0 to 1.1.0
4
+
5
+ ### Ruby >= 3.1 (non-breaking)
6
+
7
+ Railsmith **1.0.0** declared `required_ruby_version >= 3.2.0`. **1.1.0** lowers the minimum to **>= 3.1.0** so apps on Ruby 3.1 can depend on the gem without changing service code.
8
+
9
+ If you already run Ruby 3.2 or newer, no action is required.
10
+
11
+ ---
12
+
13
+ ### `DomainContext` → `Context` (non-breaking, deprecation warning)
14
+
15
+
16
+ `Railsmith::DomainContext` is deprecated in favour of `Railsmith::Context`. The old class still works but prints a deprecation warning on every `.new` call. It will be removed in the next major release.
17
+
18
+ #### New API at a glance
19
+
20
+ ```ruby
21
+ # Before (1.0.0)
22
+ Railsmith::DomainContext.new(
23
+ current_domain: :billing,
24
+ meta: { request_id: "req-abc", actor_id: current_user.id }
25
+ )
26
+
27
+ # After (1.1.0)
28
+ Railsmith::Context.new(
29
+ domain: :billing,
30
+ actor_id: current_user.id # top-level, no :meta wrapper
31
+ )
32
+ ```
33
+
34
+ Key differences:
35
+
36
+ | | `DomainContext` (old) | `Context` (new) |
37
+ |---|---|---|
38
+ | Class | `Railsmith::DomainContext` | `Railsmith::Context` |
39
+ | Domain kwarg | `current_domain:` | `domain:` |
40
+ | Extra fields | nested under `meta:` | top-level kwargs |
41
+ | `to_h` shape | `{ current_domain:, **meta }` | same (unchanged) |
42
+
43
+ #### Migration steps
44
+
45
+ 1. **Find all `DomainContext` usages:**
46
+ ```
47
+ grep -r "DomainContext" app/ spec/
48
+ ```
49
+
50
+ 2. **Replace the class name and kwargs:**
51
+ ```ruby
52
+ # Before
53
+ ctx = Railsmith::DomainContext.new(current_domain: :billing, meta: { request_id: "r1", actor_id: 42 })
54
+
55
+ # After
56
+ ctx = Railsmith::Context.new(domain: :billing, request_id: "r1", actor_id: 42)
57
+ ```
58
+
59
+ 3. **If you read `ctx.meta` directly**, switch to individual readers:
60
+ ```ruby
61
+ ctx.meta[:actor_id] # before
62
+ ctx[:actor_id] # after
63
+ ```
64
+
65
+ 4. **No changes needed at the call site.** `to_h` output is identical — services reading `context[:current_domain]` continue to work without modification.
66
+
67
+ #### Passing a context hash directly (unchanged)
68
+
69
+ Services that receive a plain hash (e.g. `context: { current_domain: :billing, actor_id: 42 }`) are unaffected. The hash shape is preserved by `Context#to_h`.
70
+
71
+ ---
72
+
73
+ ### Generator defaults — no forced namespace (non-breaking)
74
+
75
+ The `railsmith:model_service` and `railsmith:operation` generators no longer wrap generated classes in an `Operations::` module by default.
76
+
77
+ #### model_service generator
78
+
79
+ | | 1.0.0 default | 1.1.0 default |
80
+ |---|---|---|
81
+ | Command | `rails g railsmith:model_service User` | same |
82
+ | Output file | `app/services/operations/user_service.rb` | `app/services/user_service.rb` |
83
+ | Module wrapper | `module Operations` | none |
84
+ | Call site | `Operations::UserService.call(...)` | `UserService.call(...)` |
85
+
86
+ **Existing services are not broken** — any service already generated under `Operations::` continues to work without changes. The change only affects newly generated files.
87
+
88
+ To generate with an explicit namespace (e.g. when you want domain grouping):
89
+
90
+ ```bash
91
+ rails generate railsmith:model_service Invoice --namespace=Billing::Services
92
+ # => app/services/billing/services/invoice_service.rb
93
+ # => module Billing; module Services; class InvoiceService
94
+ # => domain :billing (auto-added from first segment)
95
+ ```
96
+
97
+ To preserve the old `Operations::` default in a project that still wants it, pass `--namespace=Operations`:
98
+
99
+ ```bash
100
+ rails generate railsmith:model_service User --namespace=Operations
101
+ # => app/services/operations/user_service.rb (same as before)
102
+ ```
103
+
104
+ #### operation generator
105
+
106
+ | | 1.0.0 default | 1.1.0 default |
107
+ |---|---|---|
108
+ | Command | `rails g railsmith:operation Billing::Invoices::Create` | same |
109
+ | Output file | `app/domains/billing/operations/invoices/create.rb` | `app/domains/billing/invoices/create.rb` |
110
+ | Module hierarchy | `Billing::Operations::Invoices::Create` | `Billing::Invoices::Create` |
111
+
112
+ **Existing operations are not broken** — files already under `.../operations/...` are unaffected.
113
+
114
+ To restore the old `Operations` interstitial module:
115
+
116
+ ```bash
117
+ rails generate railsmith:operation Billing::Invoices::Create --namespace=Operations
118
+ # => app/domains/billing/operations/invoices/create.rb (same as before)
119
+ ```
120
+
121
+ ---
122
+
123
+ ### Auto-generated `request_id` (non-breaking)
124
+
125
+ `Railsmith::Context` now assigns a UUID `request_id` automatically at construction when one is not provided.
126
+
127
+ ```ruby
128
+ ctx = Railsmith::Context.new(domain: :billing, actor_id: 42)
129
+ ctx.request_id # => "550e8400-e29b-41d4-a716-446655440000" (auto-generated)
130
+ ctx.to_h # => { current_domain: :billing, actor_id: 42, request_id: "550e8400-..." }
131
+ ```
132
+
133
+ To forward an existing request ID (e.g. from an incoming HTTP header), pass it explicitly — it is never overwritten:
134
+
135
+ ```ruby
136
+ ctx = Railsmith::Context.new(domain: :web, request_id: request.headers["X-Request-Id"])
137
+ ctx.request_id # => whatever the header contained
138
+ ```
139
+
140
+ **No migration required.** All existing code that already passes `request_id:` continues to work unchanged. Code that omitted it now gets a UUID instead of `nil` in `to_h` — this is the intended behaviour.
141
+
142
+ If you have specs that assert `Context#to_h` equals an exact hash without a `request_id` key, update them to use `include(...)` or pass an explicit `request_id:` to fix the value:
143
+
144
+ ```ruby
145
+ # Before (will fail — to_h now always includes request_id)
146
+ expect(ctx.to_h).to eq(current_domain: :billing, actor_id: 42)
147
+
148
+ # After — option A: assert on the keys you care about
149
+ expect(ctx.to_h).to include(current_domain: :billing, actor_id: 42)
150
+
151
+ # After — option B: fix the request_id to make equality deterministic
152
+ ctx = Railsmith::Context.new(domain: :billing, actor_id: 42, request_id: "r1")
153
+ expect(ctx.to_h).to eq(current_domain: :billing, actor_id: 42, request_id: "r1")
154
+ ```
155
+
156
+ ---
157
+
158
+ ### `context:` is now optional at the call site (non-breaking)
159
+
160
+ `BaseService.call` no longer requires `context:`. Omitting it, passing `nil`, or passing `{}` all produce a valid `Context` with an auto-generated `request_id`.
161
+
162
+ ```ruby
163
+ # All of these are equivalent and valid in 1.1.0
164
+ UserService.call(action: :create, params: { attributes: { name: "Alice" } })
165
+ UserService.call(action: :create, params: { ... }, context: {})
166
+ UserService.call(action: :create, params: { ... }, context: nil)
167
+ ```
168
+
169
+ If you previously passed `context: {}` as a no-op placeholder, you can remove it — the behaviour is identical.
170
+
171
+ When you do pass a context value, `Context.build` handles coercion:
172
+
173
+ | Value passed | Result |
174
+ |---|---|
175
+ | A `Railsmith::Context` | used as-is |
176
+ | A hash with `:domain` or `:current_domain` | wrapped in `Context.new(**hash)` |
177
+ | `nil` or `{}` | new `Context` with auto `request_id` |
178
+
179
+ **No migration required.** Existing code that passes a full context is unaffected.
180
+
181
+ ---
182
+
183
+ ### New read actions: `find` and `list` (non-breaking, additive)
184
+
185
+ Two new CRUD actions are available on all model-backed services.
186
+
187
+ ```ruby
188
+ # Find a single record by ID
189
+ result = UserService.call(action: :find, params: { id: 1 })
190
+ result.value # => <User id=1>
191
+
192
+ # List all records (override to filter)
193
+ result = UserService.call(action: :list, params: {})
194
+ result.value # => [<User>, ...]
195
+ ```
196
+
197
+ Default `list` calls `model_class.all`. Override it when you need filtering:
198
+
199
+ ```ruby
200
+ class UserService < Railsmith::BaseService
201
+ model User
202
+
203
+ def list
204
+ users = User.where(active: params[:active]).order(:name)
205
+ Result.success(value: users)
206
+ end
207
+ end
208
+ ```
209
+
210
+ **No migration required.** These are new methods; existing overrides are unaffected. If you had a custom `find` or `list` method that returns a different shape, it will shadow the default — check your override's return value matches `Result.success`/`Result.failure`.
211
+
212
+ ---
213
+
214
+ ### Thread-local context propagation (opt-in, non-breaking)
215
+
216
+ `Railsmith::Context.with(...)` sets a thread-local context for the duration of a block. Services automatically inherit it when no explicit `context:` is passed.
217
+
218
+ ```ruby
219
+ # Set once at the edge (e.g. ApplicationController)
220
+ around_action do |_, block|
221
+ Railsmith::Context.with(domain: :web, actor_id: current_user&.id) { block.call }
222
+ end
223
+
224
+ # Services pick it up automatically — no need to thread it through every call
225
+ UserService.call(action: :create, params: { ... })
226
+ ```
227
+
228
+ Resolution order: **explicit `context:` arg > `Context.current` > auto-built empty context**.
229
+
230
+ Explicit `context:` always wins, so existing code that passes context explicitly is completely unaffected.
231
+
232
+ `Context.current` returns the current thread-local `Context` or `nil`. `Context.with` restores the previous value after the block, making it safe for nested calls and concurrent requests.
233
+
234
+ **No migration required.** This is fully opt-in.
235
+
236
+ ---
237
+
238
+ ### `service_domain` → `domain` DSL (non-breaking, deprecation warning)
239
+
240
+ The `service_domain` class macro is deprecated in favour of `domain`.
241
+
242
+ ```ruby
243
+ # Before (1.0.0)
244
+ class InvoiceService < Railsmith::BaseService
245
+ model Invoice
246
+ service_domain :billing
247
+ end
248
+
249
+ # After (1.1.0)
250
+ class InvoiceService < Railsmith::BaseService
251
+ model Invoice
252
+ domain :billing
253
+ end
254
+ ```
255
+
256
+ `service_domain` still works but emits a deprecation warning. It will be removed in the next major release.
257
+
258
+ #### Migration steps
259
+
260
+ ```
261
+ grep -r "service_domain" app/
262
+ ```
263
+
264
+ Replace each occurrence:
265
+
266
+ ```ruby
267
+ service_domain :billing # before
268
+ domain :billing # after
269
+ ```
270
+
271
+ ---
272
+
3
273
  ## Upgrading from 0.x (pre-release) to 1.0.0
4
274
 
5
275
  Railsmith 1.0.0 is the first stable release. If you were using the 0.x development version, the changes below are required before upgrading.
@@ -13,7 +283,7 @@ Railsmith 1.0.0 is the first stable release. If you were using the 0.x developme
13
283
  | Ruby | >= 3.2.0 | >= 3.2.0 |
14
284
  | Rails | 7.0–8.x | 7.0–8.x |
15
285
 
16
- No changes to minimum runtime requirements.
286
+ No other changes to minimum runtime requirements at the 1.0.0 release. (Ruby **3.1** is supported starting in **1.1.0**; see the section above.)
17
287
 
18
288
  ---
19
289
 
@@ -78,11 +348,11 @@ The `on_cross_domain_violation` config callback still fires and is the recommend
78
348
 
79
349
  Domain-scoped services are now always generated under `app/domains/<domain>/services/`. If you used the generator during 0.x development and accepted a different default path, move the files and update `require` paths accordingly.
80
350
 
81
- | Generator | Output path (1.0.0) |
82
- |-----------|---------------------|
83
- | `railsmith:model_service User` | `app/services/operations/user_service.rb` |
84
- | `railsmith:model_service Billing::Invoice --domain=Billing` | `app/domains/billing/services/invoice_service.rb` |
85
- | `railsmith:operation Billing::Invoices::Create` | `app/domains/billing/operations/invoices/create.rb` |
351
+ | Generator | Output path (1.0.0) | Output path (1.1.0) |
352
+ |-----------|---------------------|---------------------|
353
+ | `railsmith:model_service User` | `app/services/operations/user_service.rb` | `app/services/user_service.rb` |
354
+ | `railsmith:model_service Billing::Invoice --domain=Billing` | `app/domains/billing/services/invoice_service.rb` | `app/domains/billing/services/invoice_service.rb` |
355
+ | `railsmith:operation Billing::Invoices::Create` | `app/domains/billing/operations/invoices/create.rb` | `app/domains/billing/invoices/create.rb` |
86
356
 
87
357
  ---
88
358
 
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Railsmith is a service-layer gem for Rails. It standardizes domain-oriented service boundaries with sensible defaults for CRUD operations, bulk operations, result handling, and cross-domain enforcement.
4
4
 
5
- **Requirements**: Ruby >= 3.2.0, Rails 7.0–8.x
5
+ **Requirements**: Ruby >= 3.1.0, Rails 7.0–8.x
6
6
 
7
7
  ---
8
8
 
@@ -33,10 +33,9 @@ rails generate railsmith:model_service User
33
33
  Call it:
34
34
 
35
35
  ```ruby
36
- result = Operations::UserService.call(
36
+ result = UserService.call(
37
37
  action: :create,
38
- params: { attributes: { name: "Alice", email: "alice@example.com" } },
39
- context: {}
38
+ params: { attributes: { name: "Alice", email: "alice@example.com" } }
40
39
  )
41
40
 
42
41
  if result.success?
@@ -79,31 +78,29 @@ result.error.to_h # => { code: "not_found", message: "User not found", deta
79
78
  |---------|--------|
80
79
  | `rails g railsmith:install` | Initializer + service directories |
81
80
  | `rails g railsmith:domain Billing` | `app/domains/billing.rb` + subdirectories |
82
- | `rails g railsmith:model_service User` | `app/services/operations/user_service.rb` |
81
+ | `rails g railsmith:model_service User` | `app/services/user_service.rb` |
83
82
  | `rails g railsmith:model_service Billing::Invoice --domain=Billing` | `app/domains/billing/services/invoice_service.rb` |
84
- | `rails g railsmith:operation Billing::Invoices::Create` | `app/domains/billing/operations/invoices/create.rb` |
83
+ | `rails g railsmith:operation Billing::Invoices::Create` | `app/domains/billing/invoices/create.rb` |
85
84
 
86
85
  ---
87
86
 
88
87
  ## CRUD Actions
89
88
 
90
- Services that declare a `model` inherit `create`, `update`, and `destroy` with automatic exception mapping:
89
+ Services that declare a `model` inherit `create`, `update`, `destroy`, `find`, and `list` with automatic exception mapping:
91
90
 
92
91
  ```ruby
93
- module Operations
94
- class UserService < Railsmith::BaseService
95
- model(User)
96
- end
92
+ class UserService < Railsmith::BaseService
93
+ model(User)
97
94
  end
98
95
 
99
- # create
100
- Operations::UserService.call(action: :create, params: { attributes: { email: "a@b.com" } }, context: {})
96
+ # create (context: is optional from 1.1 onward)
97
+ UserService.call(action: :create, params: { attributes: { email: "a@b.com" } })
101
98
 
102
99
  # update
103
- Operations::UserService.call(action: :update, params: { id: 1, attributes: { email: "new@b.com" } }, context: {})
100
+ UserService.call(action: :update, params: { id: 1, attributes: { email: "new@b.com" } })
104
101
 
105
102
  # destroy
106
- Operations::UserService.call(action: :destroy, params: { id: 1 }, context: {})
103
+ UserService.call(action: :destroy, params: { id: 1 })
107
104
  ```
108
105
 
109
106
  Common ActiveRecord exceptions (`RecordNotFound`, `RecordInvalid`, `RecordNotUnique`) are caught and converted to structured failure results automatically.
@@ -114,27 +111,24 @@ Common ActiveRecord exceptions (`RecordNotFound`, `RecordInvalid`, `RecordNotUni
114
111
 
115
112
  ```ruby
116
113
  # bulk_create
117
- Operations::UserService.call(
114
+ UserService.call(
118
115
  action: :bulk_create,
119
116
  params: {
120
117
  items: [{ name: "Alice", email: "a@b.com" }, { name: "Bob", email: "b@b.com" }],
121
118
  transaction_mode: :best_effort # or :all_or_nothing
122
- },
123
- context: {}
119
+ }
124
120
  )
125
121
 
126
122
  # bulk_update
127
- Operations::UserService.call(
123
+ UserService.call(
128
124
  action: :bulk_update,
129
- params: { items: [{ id: 1, attributes: { name: "Alice Smith" } }] },
130
- context: {}
125
+ params: { items: [{ id: 1, attributes: { name: "Alice Smith" } }] }
131
126
  )
132
127
 
133
128
  # bulk_destroy
134
- Operations::UserService.call(
129
+ UserService.call(
135
130
  action: :bulk_destroy,
136
- params: { items: [1, 2, 3] },
137
- context: {}
131
+ params: { items: [1, 2, 3] }
138
132
  )
139
133
  ```
140
134
 
@@ -156,24 +150,21 @@ module Billing
156
150
  module Services
157
151
  class InvoiceService < Railsmith::BaseService
158
152
  model(Billing::Invoice)
159
- service_domain :billing
153
+ domain :billing
160
154
  end
161
155
  end
162
156
  end
163
157
  ```
164
158
 
165
- Pass context on every call:
159
+ Pass context when you need domain or tracing data (`context:` is optional; omit it to use thread-local `Context.current` or an auto-built context):
166
160
 
167
161
  ```ruby
168
- ctx = Railsmith::DomainContext.new(
169
- current_domain: :billing,
170
- meta: { request_id: "req-abc" }
171
- ).to_h
162
+ ctx = Railsmith::Context.new(domain: :billing, request_id: "req-abc")
172
163
 
173
164
  Billing::Services::InvoiceService.call(action: :create, params: { ... }, context: ctx)
174
165
  ```
175
166
 
176
- When `current_domain` in the context differs from a service's declared `service_domain`, Railsmith emits a `cross_domain.warning.railsmith` instrumentation event.
167
+ When the context domain differs from a service's declared `domain`, Railsmith emits a `cross_domain.warning.railsmith` instrumentation event.
177
168
 
178
169
  Configure enforcement in `config/initializers/railsmith.rb`:
179
170
 
@@ -225,6 +216,8 @@ See [Migration](MIGRATION.md#embedding-architecture-checks-from-ruby) for option
225
216
  - [Quickstart](docs/quickstart.md) — install, generate, first call
226
217
  - [Cookbook](docs/cookbook.md) — CRUD, bulk, domain context, error mapping, observability
227
218
  - [Legacy Adoption Guide](docs/legacy-adoption.md) — incremental migration strategy
219
+ - [Migration](MIGRATION.md) — upgrading from 1.0.x to 1.1.x (and earlier releases)
220
+ - [Changelog](CHANGELOG.md)
228
221
 
229
222
  ---
230
223
 
@@ -236,8 +229,19 @@ bundle exec rake spec # run tests
236
229
  bin/console # interactive prompt
237
230
  ```
238
231
 
232
+ CI runs the suite against Rails 7 and Rails 8 using [`gemfiles/rails_7.gemfile`](gemfiles/rails_7.gemfile) and [`gemfiles/rails_8.gemfile`](gemfiles/rails_8.gemfile) (Ruby 3.1–3.3; Rails 8 is not paired with Ruby 3.1 in CI). To reproduce a matrix cell locally:
233
+
234
+ ```bash
235
+ BUNDLE_GEMFILE=gemfiles/rails_7.gemfile bundle install
236
+ BUNDLE_GEMFILE=gemfiles/rails_7.gemfile bundle exec rspec
237
+ ```
238
+
239
239
  To install locally: `bundle exec rake install`.
240
240
 
241
+ ### Releasing
242
+
243
+ With `lib/railsmith/version.rb` and `CHANGELOG.md` updated and committed, run `bundle exec rake release` to tag `v` + version, build the gem, and push to RubyGems (requires `gem push` credentials and a clean git state). To publish manually: `gem build railsmith.gemspec` then `gem push railsmith-X.Y.Z.gem`.
244
+
241
245
  ---
242
246
 
243
247
  ## Contributing