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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1a20ec84a09c4074cd56944e07f4ea7f14ffab15e8f9fe41d4f288ac3cc2f9ce
|
|
4
|
+
data.tar.gz: 47e886d32518f52ba779f4ce0727737653b7e719d7b266ca8a2769ab7ff2bb97
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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 =
|
|
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/
|
|
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/
|
|
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 `
|
|
89
|
+
Services that declare a `model` inherit `create`, `update`, `destroy`, `find`, and `list` with automatic exception mapping:
|
|
91
90
|
|
|
92
91
|
```ruby
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
model(User)
|
|
96
|
-
end
|
|
92
|
+
class UserService < Railsmith::BaseService
|
|
93
|
+
model(User)
|
|
97
94
|
end
|
|
98
95
|
|
|
99
|
-
# create
|
|
100
|
-
|
|
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
|
-
|
|
100
|
+
UserService.call(action: :update, params: { id: 1, attributes: { email: "new@b.com" } })
|
|
104
101
|
|
|
105
102
|
# destroy
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
153
|
+
domain :billing
|
|
160
154
|
end
|
|
161
155
|
end
|
|
162
156
|
end
|
|
163
157
|
```
|
|
164
158
|
|
|
165
|
-
Pass context
|
|
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::
|
|
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
|
|
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
|