railsmith 1.0.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 +7 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +64 -0
- data/LICENSE.txt +21 -0
- data/MIGRATION.md +156 -0
- data/README.md +249 -0
- data/Rakefile +14 -0
- data/docs/cookbook.md +605 -0
- data/docs/legacy-adoption.md +283 -0
- data/docs/quickstart.md +110 -0
- data/lib/generators/railsmith/domain/domain_generator.rb +57 -0
- data/lib/generators/railsmith/domain/templates/domain.rb.tt +14 -0
- data/lib/generators/railsmith/install/install_generator.rb +21 -0
- data/lib/generators/railsmith/install/templates/railsmith.rb +10 -0
- data/lib/generators/railsmith/model_service/model_service_generator.rb +121 -0
- data/lib/generators/railsmith/model_service/templates/model_service.rb.tt +28 -0
- data/lib/generators/railsmith/operation/operation_generator.rb +88 -0
- data/lib/generators/railsmith/operation/templates/operation.rb.tt +27 -0
- data/lib/railsmith/arch_checks/cli.rb +79 -0
- data/lib/railsmith/arch_checks/direct_model_access_checker.rb +94 -0
- data/lib/railsmith/arch_checks/missing_service_usage_checker.rb +206 -0
- data/lib/railsmith/arch_checks/violation.rb +14 -0
- data/lib/railsmith/arch_checks.rb +7 -0
- data/lib/railsmith/arch_report.rb +96 -0
- data/lib/railsmith/base_service/bulk_actions.rb +77 -0
- data/lib/railsmith/base_service/bulk_contract.rb +56 -0
- data/lib/railsmith/base_service/bulk_execution.rb +68 -0
- data/lib/railsmith/base_service/bulk_params.rb +56 -0
- data/lib/railsmith/base_service/crud_actions.rb +63 -0
- data/lib/railsmith/base_service/crud_error_mapping.rb +78 -0
- data/lib/railsmith/base_service/crud_model_resolution.rb +36 -0
- data/lib/railsmith/base_service/crud_record_helpers.rb +60 -0
- data/lib/railsmith/base_service/crud_transactions.rb +31 -0
- data/lib/railsmith/base_service/domain_context_propagation.rb +29 -0
- data/lib/railsmith/base_service/dup_helpers.rb +15 -0
- data/lib/railsmith/base_service/validation.rb +67 -0
- data/lib/railsmith/base_service.rb +96 -0
- data/lib/railsmith/configuration.rb +18 -0
- data/lib/railsmith/cross_domain_guard.rb +90 -0
- data/lib/railsmith/cross_domain_warning_formatter.rb +66 -0
- data/lib/railsmith/deep_dup.rb +20 -0
- data/lib/railsmith/domain_context.rb +44 -0
- data/lib/railsmith/errors.rb +50 -0
- data/lib/railsmith/instrumentation.rb +64 -0
- data/lib/railsmith/railtie.rb +10 -0
- data/lib/railsmith/result.rb +60 -0
- data/lib/railsmith/version.rb +5 -0
- data/lib/railsmith.rb +31 -0
- data/lib/tasks/railsmith.rake +24 -0
- data/sig/railsmith.rbs +4 -0
- metadata +116 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b7d121f9233f121988b7811ac803943a3d2d784ccf018e31ecb1dd681747c632
|
|
4
|
+
data.tar.gz: 0d8b3debcd71cd67bec213d7614528d61584b45f1a380b35536d5e738d848871
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6c82716d751374815ebe15f4fdc0763c3ff79dbf58b1985d9d8a2bbaa70e19fe010174563401fc23c811533009ad054c8d0de7130502ce80a10e2346f309d6f7
|
|
7
|
+
data.tar.gz: 97bf97006557c7fa4e6f5c19d83d47b81c48dd751bee5d66a6d812ee5072edeeaf5a3e95b8b09ec42bc3ad778e1fae3b886c93c63cbd676240cdfef96b413786
|
data/.tool-versions
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ruby 3.3.8
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to Railsmith are documented here.
|
|
4
|
+
|
|
5
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
6
|
+
Versioning follows [Semantic Versioning](https://semver.org/).
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## [1.0.0] — 2026-03-29
|
|
11
|
+
|
|
12
|
+
First stable release. Public DSL and result contract are now frozen.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
#### Core
|
|
17
|
+
- `Railsmith::Result` — immutable value object with `success?`, `failure?`, `value`, `error`, `code`, `meta`, and `to_h`.
|
|
18
|
+
- `Railsmith::Errors` — normalized error builders: `validation_error`, `not_found`, `conflict`, `unauthorized`, `unexpected`.
|
|
19
|
+
- `Railsmith::BaseService` — lifecycle entrypoint `call(action:, params:, context:)` with deterministic hook ordering and subclass override points.
|
|
20
|
+
|
|
21
|
+
#### CRUD
|
|
22
|
+
- Default `create`, `update`, and `destroy` actions on any service that declares `model(ModelClass)`.
|
|
23
|
+
- Automatic exception mapping: `ActiveRecord::RecordNotFound` → `not_found`, `ActiveRecord::RecordInvalid` → `validation_error`, `ActiveRecord::RecordNotUnique` → `conflict`.
|
|
24
|
+
- Safe record lookup helper with consistent not-found failure shape.
|
|
25
|
+
|
|
26
|
+
#### Bulk Operations
|
|
27
|
+
- `bulk_create`, `bulk_update`, `bulk_destroy` on model-backed services.
|
|
28
|
+
- Per-item result aggregation with batch `summary` (`total`, `success_count`, `failure_count`, `all_succeeded`).
|
|
29
|
+
- Transaction modes: `:all_or_nothing` (rollback on any failure) and `:best_effort` (commit successful items).
|
|
30
|
+
- Configurable batch size limit.
|
|
31
|
+
|
|
32
|
+
#### Domain Context
|
|
33
|
+
- `Railsmith::DomainContext` — carries `current_domain` and arbitrary `meta` through a call chain.
|
|
34
|
+
- `service_domain :name` declaration on `BaseService` subclasses.
|
|
35
|
+
- Context propagation guard: emits `cross_domain.warning.railsmith` ActiveSupport instrumentation event when context domain differs from service domain.
|
|
36
|
+
- Allowlist configuration for approved cross-domain crossings.
|
|
37
|
+
- `on_cross_domain_violation` callback hook for custom handling.
|
|
38
|
+
|
|
39
|
+
#### Architecture Checks
|
|
40
|
+
- `Railsmith::ArchChecks::DirectModelAccessChecker` — static analysis for controllers that access models directly.
|
|
41
|
+
- `Railsmith::ArchChecks::MissingServiceUsageChecker` — flags controller actions that touch models without calling a service-style entrypoint.
|
|
42
|
+
- Text and JSON report formatters (`Railsmith::ArchReport`).
|
|
43
|
+
- `Railsmith::ArchChecks::Cli` — Ruby API for the same scan as `railsmith:arch_check`, with optional `env:`, `output:`, and `warn_proc:` for tests and embedding.
|
|
44
|
+
- `rake railsmith:arch_check` task with `RAILSMITH_PATHS`, `RAILSMITH_FORMAT`, and `RAILSMITH_FAIL_ON_ARCH_VIOLATIONS` environment variable support; the task delegates to `Railsmith::ArchChecks::Cli.run` (same report shape and exit semantics for callers).
|
|
45
|
+
|
|
46
|
+
#### Generators
|
|
47
|
+
- `railsmith:install` — creates `config/initializers/railsmith.rb` and `app/services/` directory tree.
|
|
48
|
+
- `railsmith:domain NAME` — scaffolds a domain module skeleton with conventional subdirectories.
|
|
49
|
+
- `railsmith:model_service MODEL` — scaffolds a `BaseService` subclass, namespace-aware with `--domain` flag.
|
|
50
|
+
- `railsmith:operation NAME` — scaffolds a plain-Ruby operation with `call` entrypoint returning `Railsmith::Result`.
|
|
51
|
+
|
|
52
|
+
#### Configuration
|
|
53
|
+
- `Railsmith.configure` block with: `warn_on_cross_domain_calls`, `strict_mode`, `on_cross_domain_violation`, `cross_domain_allowlist`, `fail_on_arch_violations`.
|
|
54
|
+
|
|
55
|
+
#### Documentation
|
|
56
|
+
- [Quickstart](docs/quickstart.md)
|
|
57
|
+
- [Cookbook](docs/cookbook.md) — CRUD, bulk, domain context, error mapping, observability
|
|
58
|
+
- [Legacy Adoption Guide](docs/legacy-adoption.md) — incremental migration strategy
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## [0.1.0] — pre-release
|
|
63
|
+
|
|
64
|
+
Internal bootstrap release. Gem skeleton, CI baseline, and initial service scaffolding. Not intended for production use.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 samaswin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/MIGRATION.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Migration Guide
|
|
2
|
+
|
|
3
|
+
## Upgrading from 0.x (pre-release) to 1.0.0
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
### Requirements
|
|
10
|
+
|
|
11
|
+
| | 0.x | 1.0.0 |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| Ruby | >= 3.2.0 | >= 3.2.0 |
|
|
14
|
+
| Rails | 7.0–8.x | 7.0–8.x |
|
|
15
|
+
|
|
16
|
+
No changes to minimum runtime requirements.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
### Result contract — now frozen
|
|
21
|
+
|
|
22
|
+
The `Railsmith::Result` interface is stable and will not change in any 1.x release.
|
|
23
|
+
|
|
24
|
+
**No action required** if you are already using the documented API (`success?`, `failure?`, `value`, `error`, `code`, `meta`, `to_h`).
|
|
25
|
+
|
|
26
|
+
If you were accessing any internal instance variables directly (e.g., `result.instance_variable_get(:@data)`), switch to the public API before upgrading.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
### Error builders — keyword arguments required
|
|
31
|
+
|
|
32
|
+
All `Railsmith::Errors` factory methods now require keyword arguments.
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
# Before (0.x, positional — no longer accepted)
|
|
36
|
+
Railsmith::Errors.not_found("User not found", { model: "User" })
|
|
37
|
+
|
|
38
|
+
# After (1.0.0)
|
|
39
|
+
Railsmith::Errors.not_found(message: "User not found", details: { model: "User" })
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Both `message:` and `details:` are optional but must be passed as keywords when provided.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
### `BaseService.call` — `context:` is now required
|
|
47
|
+
|
|
48
|
+
In 0.x, `context:` was optional and defaulted to `{}` silently. In 1.0.0, omitting `context:` raises `ArgumentError`.
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
# Before (0.x — context omitted)
|
|
52
|
+
MyService.call(action: :create, params: { ... })
|
|
53
|
+
|
|
54
|
+
# After (1.0.0 — context required)
|
|
55
|
+
MyService.call(action: :create, params: { ... }, context: {})
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Pass `context: {}` at minimum. Pass a `Railsmith::DomainContext` hash when using domain boundaries.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
### Cross-domain warnings — ActiveSupport instrumentation only
|
|
63
|
+
|
|
64
|
+
In 0.x, cross-domain violations could be configured to write directly to `Rails.logger`. In 1.0.0, all violation events are emitted exclusively via ActiveSupport Instrumentation (`cross_domain.warning.railsmith`). Wire up your own subscriber if you need log output:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
# config/initializers/railsmith.rb
|
|
68
|
+
ActiveSupport::Notifications.subscribe("cross_domain.warning.railsmith") do |_name, _start, _finish, _id, payload|
|
|
69
|
+
Rails.logger.warn("[Railsmith] cross-domain: #{payload.inspect}")
|
|
70
|
+
end
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The `on_cross_domain_violation` config callback still fires and is the recommended place for custom handling.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
### Generator output paths — finalized
|
|
78
|
+
|
|
79
|
+
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
|
+
|
|
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` |
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
### Initializer — new configuration keys
|
|
90
|
+
|
|
91
|
+
Add any missing keys to `config/initializers/railsmith.rb`. All keys have safe defaults so omitting them will not raise, but explicit configuration is recommended.
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
Railsmith.configure do |config|
|
|
95
|
+
config.warn_on_cross_domain_calls = true # default: true
|
|
96
|
+
config.strict_mode = false # default: false (reserved for v1.2)
|
|
97
|
+
config.fail_on_arch_violations = false # default: false
|
|
98
|
+
config.cross_domain_allowlist = [] # default: []
|
|
99
|
+
config.on_cross_domain_violation = nil # default: nil (no-op)
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
### Bulk operations — transaction mode default
|
|
106
|
+
|
|
107
|
+
The default `transaction_mode` for bulk operations changed from `:best_effort` in 0.x to `:all_or_nothing` in 1.0.0.
|
|
108
|
+
|
|
109
|
+
If you rely on partial-success behavior, explicitly pass `transaction_mode: :best_effort`:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
MyService.call(
|
|
113
|
+
action: :bulk_create,
|
|
114
|
+
params: { items: [...], transaction_mode: :best_effort },
|
|
115
|
+
context: {}
|
|
116
|
+
)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
### Upgrade steps
|
|
122
|
+
|
|
123
|
+
1. Update `Gemfile`: `gem "railsmith", "~> 1.0"`
|
|
124
|
+
2. Run `bundle install`.
|
|
125
|
+
3. Run `bundle exec rspec` — fix any `ArgumentError` on `call` (add `context: {}`).
|
|
126
|
+
4. Search for positional `Railsmith::Errors.*` calls and convert to keywords.
|
|
127
|
+
5. Review initializer against the full key list above.
|
|
128
|
+
6. Run `rake railsmith:arch_check` as a smoke test.
|
|
129
|
+
7. Deploy.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Embedding architecture checks from Ruby
|
|
134
|
+
|
|
135
|
+
You do not need this section for a normal upgrade. The `railsmith:arch_check` Rake task behaves the same from the shell (`RAILSMITH_PATHS`, `RAILSMITH_FORMAT`, `RAILSMITH_FAIL_ON_ARCH_VIOLATIONS`, and `Railsmith.configure { |c| c.fail_on_arch_violations }`).
|
|
136
|
+
|
|
137
|
+
If you maintain custom Rake tasks, CI scripts in Ruby, or tests that should run the same scan without shelling out, call the library entrypoint:
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
require "railsmith/arch_checks"
|
|
141
|
+
|
|
142
|
+
status = Railsmith::ArchChecks::Cli.run
|
|
143
|
+
# 0 — success or warn-only; 1 — fail-on enabled and violations present
|
|
144
|
+
|
|
145
|
+
# Optional: isolated env, capture output, or custom warnings
|
|
146
|
+
# require "stringio"
|
|
147
|
+
# out = StringIO.new
|
|
148
|
+
# warnings = []
|
|
149
|
+
# status = Railsmith::ArchChecks::Cli.run(
|
|
150
|
+
# env: { "RAILSMITH_PATHS" => "app/controllers", "RAILSMITH_FORMAT" => "text" },
|
|
151
|
+
# output: out,
|
|
152
|
+
# warn_proc: ->(message) { warnings << message }
|
|
153
|
+
# )
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Replace `rake railsmith:arch_check` with `Cli.run` only when you explicitly need an in-process API; the task remains the supported default for apps.
|
data/README.md
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# Railsmith
|
|
2
|
+
|
|
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
|
+
|
|
5
|
+
**Requirements**: Ruby >= 3.2.0, Rails 7.0–8.x
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# Gemfile
|
|
13
|
+
gem "railsmith"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
bundle install
|
|
18
|
+
rails generate railsmith:install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The install generator creates `config/initializers/railsmith.rb` and the `app/services/` directory tree.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
Generate a service for a model:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
rails generate railsmith:model_service User
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Call it:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
result = Operations::UserService.call(
|
|
37
|
+
action: :create,
|
|
38
|
+
params: { attributes: { name: "Alice", email: "alice@example.com" } },
|
|
39
|
+
context: {}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if result.success?
|
|
43
|
+
puts result.value.id
|
|
44
|
+
else
|
|
45
|
+
puts result.error.message # => "Validation failed"
|
|
46
|
+
puts result.error.details # => { errors: { email: ["is invalid"] } }
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
See [docs/quickstart.md](docs/quickstart.md) for a full walkthrough.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Result Contract
|
|
55
|
+
|
|
56
|
+
Every service call returns a `Railsmith::Result`. You never rescue exceptions from service calls.
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
# Success
|
|
60
|
+
result = Railsmith::Result.success(value: { id: 123 }, meta: { request_id: "abc" })
|
|
61
|
+
result.success? # => true
|
|
62
|
+
result.value # => { id: 123 }
|
|
63
|
+
result.meta # => { request_id: "abc" }
|
|
64
|
+
result.to_h # => { success: true, value: { id: 123 }, meta: { request_id: "abc" } }
|
|
65
|
+
|
|
66
|
+
# Failure
|
|
67
|
+
error = Railsmith::Errors.not_found(message: "User not found", details: { model: "User", id: 1 })
|
|
68
|
+
result = Railsmith::Result.failure(error:)
|
|
69
|
+
result.failure? # => true
|
|
70
|
+
result.code # => "not_found"
|
|
71
|
+
result.error.to_h # => { code: "not_found", message: "User not found", details: { ... } }
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Generators
|
|
77
|
+
|
|
78
|
+
| Command | Output |
|
|
79
|
+
|---------|--------|
|
|
80
|
+
| `rails g railsmith:install` | Initializer + service directories |
|
|
81
|
+
| `rails g railsmith:domain Billing` | `app/domains/billing.rb` + subdirectories |
|
|
82
|
+
| `rails g railsmith:model_service User` | `app/services/operations/user_service.rb` |
|
|
83
|
+
| `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` |
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## CRUD Actions
|
|
89
|
+
|
|
90
|
+
Services that declare a `model` inherit `create`, `update`, and `destroy` with automatic exception mapping:
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
module Operations
|
|
94
|
+
class UserService < Railsmith::BaseService
|
|
95
|
+
model(User)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# create
|
|
100
|
+
Operations::UserService.call(action: :create, params: { attributes: { email: "a@b.com" } }, context: {})
|
|
101
|
+
|
|
102
|
+
# update
|
|
103
|
+
Operations::UserService.call(action: :update, params: { id: 1, attributes: { email: "new@b.com" } }, context: {})
|
|
104
|
+
|
|
105
|
+
# destroy
|
|
106
|
+
Operations::UserService.call(action: :destroy, params: { id: 1 }, context: {})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Common ActiveRecord exceptions (`RecordNotFound`, `RecordInvalid`, `RecordNotUnique`) are caught and converted to structured failure results automatically.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Bulk Operations
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
# bulk_create
|
|
117
|
+
Operations::UserService.call(
|
|
118
|
+
action: :bulk_create,
|
|
119
|
+
params: {
|
|
120
|
+
items: [{ name: "Alice", email: "a@b.com" }, { name: "Bob", email: "b@b.com" }],
|
|
121
|
+
transaction_mode: :best_effort # or :all_or_nothing
|
|
122
|
+
},
|
|
123
|
+
context: {}
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# bulk_update
|
|
127
|
+
Operations::UserService.call(
|
|
128
|
+
action: :bulk_update,
|
|
129
|
+
params: { items: [{ id: 1, attributes: { name: "Alice Smith" } }] },
|
|
130
|
+
context: {}
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# bulk_destroy
|
|
134
|
+
Operations::UserService.call(
|
|
135
|
+
action: :bulk_destroy,
|
|
136
|
+
params: { items: [1, 2, 3] },
|
|
137
|
+
context: {}
|
|
138
|
+
)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
All bulk results include a `summary` (`total`, `success_count`, `failure_count`, `all_succeeded`) and per-item detail. See [docs/cookbook.md](docs/cookbook.md) for the full result shape.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Domain Boundaries
|
|
146
|
+
|
|
147
|
+
Tag services with a bounded context and track it through all calls:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
rails generate railsmith:domain Billing
|
|
151
|
+
rails generate railsmith:model_service Billing::Invoice --domain=Billing
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
module Billing
|
|
156
|
+
module Services
|
|
157
|
+
class InvoiceService < Railsmith::BaseService
|
|
158
|
+
model(Billing::Invoice)
|
|
159
|
+
service_domain :billing
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Pass context on every call:
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
ctx = Railsmith::DomainContext.new(
|
|
169
|
+
current_domain: :billing,
|
|
170
|
+
meta: { request_id: "req-abc" }
|
|
171
|
+
).to_h
|
|
172
|
+
|
|
173
|
+
Billing::Services::InvoiceService.call(action: :create, params: { ... }, context: ctx)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
When `current_domain` in the context differs from a service's declared `service_domain`, Railsmith emits a `cross_domain.warning.railsmith` instrumentation event.
|
|
177
|
+
|
|
178
|
+
Configure enforcement in `config/initializers/railsmith.rb`:
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
Railsmith.configure do |config|
|
|
182
|
+
config.warn_on_cross_domain_calls = true # default
|
|
183
|
+
config.strict_mode = false
|
|
184
|
+
config.on_cross_domain_violation = ->(payload) { ... }
|
|
185
|
+
config.cross_domain_allowlist = [{ from: :catalog, to: :billing }]
|
|
186
|
+
end
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Error Types
|
|
192
|
+
|
|
193
|
+
| Code | Factory |
|
|
194
|
+
|------|---------|
|
|
195
|
+
| `validation_error` | `Railsmith::Errors.validation_error(message:, details:)` |
|
|
196
|
+
| `not_found` | `Railsmith::Errors.not_found(message:, details:)` |
|
|
197
|
+
| `conflict` | `Railsmith::Errors.conflict(message:, details:)` |
|
|
198
|
+
| `unauthorized` | `Railsmith::Errors.unauthorized(message:, details:)` |
|
|
199
|
+
| `unexpected` | `Railsmith::Errors.unexpected(message:, details:)` |
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Architecture Checks
|
|
204
|
+
|
|
205
|
+
Detect controllers that access models directly (and related service-layer rules). From the shell:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
rake railsmith:arch_check
|
|
209
|
+
RAILSMITH_FORMAT=json rake railsmith:arch_check
|
|
210
|
+
RAILSMITH_FAIL_ON_ARCH_VIOLATIONS=true rake railsmith:arch_check
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
From Ruby (same environment variables and exit codes as the task), after `require "railsmith/arch_checks"`:
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
Railsmith::ArchChecks::Cli.run # => 0 or 1
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
See [Migration](MIGRATION.md#embedding-architecture-checks-from-ruby) for optional `env:`, `output:`, and `warn_proc:` arguments.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Documentation
|
|
224
|
+
|
|
225
|
+
- [Quickstart](docs/quickstart.md) — install, generate, first call
|
|
226
|
+
- [Cookbook](docs/cookbook.md) — CRUD, bulk, domain context, error mapping, observability
|
|
227
|
+
- [Legacy Adoption Guide](docs/legacy-adoption.md) — incremental migration strategy
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Development
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
bin/setup # install dependencies
|
|
235
|
+
bundle exec rake spec # run tests
|
|
236
|
+
bin/console # interactive prompt
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
To install locally: `bundle exec rake install`.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Contributing
|
|
244
|
+
|
|
245
|
+
Bug reports and pull requests are welcome at [github.com/samaswin/railsmith](https://github.com/samaswin/railsmith).
|
|
246
|
+
|
|
247
|
+
## License
|
|
248
|
+
|
|
249
|
+
[MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
|
|
5
|
+
load File.expand_path("lib/tasks/railsmith.rake", __dir__)
|
|
6
|
+
require "rspec/core/rake_task"
|
|
7
|
+
|
|
8
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
9
|
+
|
|
10
|
+
require "rubocop/rake_task"
|
|
11
|
+
|
|
12
|
+
RuboCop::RakeTask.new
|
|
13
|
+
|
|
14
|
+
task default: %i[spec rubocop]
|