light-services 2.2.1 → 3.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/.github/config/rubocop_linter_action.yml +4 -4
- data/.github/workflows/ci.yml +12 -12
- data/.gitignore +1 -0
- data/.rubocop.yml +83 -7
- data/CHANGELOG.md +38 -0
- data/CLAUDE.md +139 -0
- data/Gemfile +16 -11
- data/Gemfile.lock +53 -27
- data/README.md +84 -21
- data/docs/arguments.md +290 -0
- data/docs/best-practices.md +153 -0
- data/docs/callbacks.md +476 -0
- data/docs/concepts.md +80 -0
- data/docs/configuration.md +204 -0
- data/docs/context.md +128 -0
- data/docs/crud.md +525 -0
- data/docs/errors.md +280 -0
- data/docs/generators.md +250 -0
- data/docs/outputs.md +158 -0
- data/docs/pundit-authorization.md +320 -0
- data/docs/quickstart.md +134 -0
- data/docs/readme.md +101 -0
- data/docs/recipes.md +14 -0
- data/docs/rubocop.md +285 -0
- data/docs/ruby-lsp.md +133 -0
- data/docs/service-rendering.md +222 -0
- data/docs/steps.md +391 -0
- data/docs/summary.md +21 -0
- data/docs/testing.md +549 -0
- data/lib/generators/light_services/install/USAGE +15 -0
- data/lib/generators/light_services/install/install_generator.rb +41 -0
- data/lib/generators/light_services/install/templates/application_service.rb.tt +8 -0
- data/lib/generators/light_services/install/templates/application_service_spec.rb.tt +7 -0
- data/lib/generators/light_services/install/templates/initializer.rb.tt +30 -0
- data/lib/generators/light_services/service/USAGE +21 -0
- data/lib/generators/light_services/service/service_generator.rb +68 -0
- data/lib/generators/light_services/service/templates/service.rb.tt +48 -0
- data/lib/generators/light_services/service/templates/service_spec.rb.tt +40 -0
- data/lib/light/services/base.rb +134 -122
- data/lib/light/services/base_with_context.rb +23 -1
- data/lib/light/services/callbacks.rb +157 -0
- data/lib/light/services/collection.rb +145 -0
- data/lib/light/services/concerns/execution.rb +79 -0
- data/lib/light/services/concerns/parent_service.rb +34 -0
- data/lib/light/services/concerns/state_management.rb +30 -0
- data/lib/light/services/config.rb +82 -16
- data/lib/light/services/constants.rb +100 -0
- data/lib/light/services/dsl/arguments_dsl.rb +85 -0
- data/lib/light/services/dsl/outputs_dsl.rb +81 -0
- data/lib/light/services/dsl/steps_dsl.rb +205 -0
- data/lib/light/services/dsl/validation.rb +162 -0
- data/lib/light/services/exceptions.rb +25 -2
- data/lib/light/services/message.rb +28 -3
- data/lib/light/services/messages.rb +92 -32
- data/lib/light/services/rspec/matchers/define_argument.rb +174 -0
- data/lib/light/services/rspec/matchers/define_output.rb +147 -0
- data/lib/light/services/rspec/matchers/define_step.rb +225 -0
- data/lib/light/services/rspec/matchers/execute_step.rb +230 -0
- data/lib/light/services/rspec/matchers/have_error_on.rb +148 -0
- data/lib/light/services/rspec/matchers/have_warning_on.rb +148 -0
- data/lib/light/services/rspec/matchers/trigger_callback.rb +138 -0
- data/lib/light/services/rspec.rb +15 -0
- data/lib/light/services/rubocop/cop/light_services/argument_type_required.rb +52 -0
- data/lib/light/services/rubocop/cop/light_services/condition_method_exists.rb +173 -0
- data/lib/light/services/rubocop/cop/light_services/deprecated_methods.rb +113 -0
- data/lib/light/services/rubocop/cop/light_services/dsl_order.rb +176 -0
- data/lib/light/services/rubocop/cop/light_services/missing_private_keyword.rb +102 -0
- data/lib/light/services/rubocop/cop/light_services/no_direct_instantiation.rb +66 -0
- data/lib/light/services/rubocop/cop/light_services/output_type_required.rb +52 -0
- data/lib/light/services/rubocop/cop/light_services/step_method_exists.rb +109 -0
- data/lib/light/services/rubocop.rb +12 -0
- data/lib/light/services/settings/field.rb +114 -0
- data/lib/light/services/settings/step.rb +53 -20
- data/lib/light/services/utils.rb +38 -0
- data/lib/light/services/version.rb +1 -1
- data/lib/light/services.rb +2 -0
- data/lib/ruby_lsp/light_services/addon.rb +36 -0
- data/lib/ruby_lsp/light_services/definition.rb +132 -0
- data/lib/ruby_lsp/light_services/indexing_enhancement.rb +263 -0
- data/light-services.gemspec +6 -8
- metadata +68 -26
- data/lib/light/services/class_based_collection/base.rb +0 -86
- data/lib/light/services/class_based_collection/mount.rb +0 -33
- data/lib/light/services/collection/arguments.rb +0 -34
- data/lib/light/services/collection/base.rb +0 -59
- data/lib/light/services/collection/outputs.rb +0 -16
- data/lib/light/services/settings/argument.rb +0 -68
- data/lib/light/services/settings/output.rb +0 -34
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
Light Services provides a flexible configuration system that allows you to customize behavior at three levels: global, per-service, and per-call.
|
|
4
|
+
|
|
5
|
+
## Global Configuration
|
|
6
|
+
|
|
7
|
+
Configure Light Services globally using an initializer. For Rails applications, create `config/initializers/light_services.rb`:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
Light::Services.configure do |config|
|
|
11
|
+
# Type enforcement
|
|
12
|
+
config.require_type = true # Require type option for all arguments and outputs
|
|
13
|
+
|
|
14
|
+
# Transaction settings
|
|
15
|
+
config.use_transactions = true # Wrap each service in a database transaction
|
|
16
|
+
|
|
17
|
+
# Error behavior
|
|
18
|
+
config.load_errors = true # Copy errors to parent service in context chain
|
|
19
|
+
config.break_on_error = true # Stop step execution when an error is added
|
|
20
|
+
config.raise_on_error = false # Raise an exception when an error is added
|
|
21
|
+
config.rollback_on_error = true # Rollback transaction when an error is added
|
|
22
|
+
|
|
23
|
+
# Warning behavior
|
|
24
|
+
config.load_warnings = true # Copy warnings to parent service in context chain
|
|
25
|
+
config.break_on_warning = false # Stop step execution when a warning is added
|
|
26
|
+
config.raise_on_warning = false # Raise an exception when a warning is added
|
|
27
|
+
config.rollback_on_warning = false # Rollback transaction when a warning is added
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Default Values
|
|
32
|
+
|
|
33
|
+
| Option | Default | Description |
|
|
34
|
+
|--------|---------|-------------|
|
|
35
|
+
| `require_type` | `true` | Raises `Light::Services::MissingTypeError` when defining arguments or outputs without a `type` option |
|
|
36
|
+
| `use_transactions` | `true` | Wraps service execution in `ActiveRecord::Base.transaction` |
|
|
37
|
+
| `load_errors` | `true` | Propagates errors to parent service when using `.with(self)` |
|
|
38
|
+
| `break_on_error` | `true` | Stops executing remaining steps when an error is added |
|
|
39
|
+
| `raise_on_error` | `false` | Raises `Light::Services::Error` when an error is added |
|
|
40
|
+
| `rollback_on_error` | `true` | Rolls back the transaction when an error is added |
|
|
41
|
+
| `load_warnings` | `true` | Propagates warnings to parent service when using `.with(self)` |
|
|
42
|
+
| `break_on_warning` | `false` | Stops executing remaining steps when a warning is added |
|
|
43
|
+
| `raise_on_warning` | `false` | Raises `Light::Services::Error` when a warning is added |
|
|
44
|
+
| `rollback_on_warning` | `false` | Rolls back the transaction when a warning is added |
|
|
45
|
+
|
|
46
|
+
## Per-Service Configuration
|
|
47
|
+
|
|
48
|
+
Override global configuration for a specific service class using the `config` class method:
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
class CriticalPaymentService < ApplicationService
|
|
52
|
+
# This service will raise exceptions instead of collecting errors
|
|
53
|
+
config raise_on_error: true
|
|
54
|
+
|
|
55
|
+
step :process_payment
|
|
56
|
+
step :send_receipt
|
|
57
|
+
|
|
58
|
+
# ...
|
|
59
|
+
end
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
class NonCriticalNotificationService < ApplicationService
|
|
64
|
+
# This service doesn't need transactions and shouldn't stop on errors
|
|
65
|
+
config use_transactions: false, break_on_error: false
|
|
66
|
+
|
|
67
|
+
step :send_push_notification
|
|
68
|
+
step :send_email_notification
|
|
69
|
+
|
|
70
|
+
# ...
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Per-Call Configuration
|
|
75
|
+
|
|
76
|
+
Override configuration for a single service call:
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
# Pass config as second argument to run
|
|
80
|
+
MyService.run({ name: "John" }, { raise_on_error: true })
|
|
81
|
+
|
|
82
|
+
# Or use with() for context-based calls
|
|
83
|
+
MyService.with({ raise_on_error: true }).run(name: "John")
|
|
84
|
+
|
|
85
|
+
# Combine with parent service context
|
|
86
|
+
ChildService
|
|
87
|
+
.with(self, { use_transactions: false })
|
|
88
|
+
.run(data: some_data)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Configuration Precedence
|
|
92
|
+
|
|
93
|
+
Configuration is merged in this order (later overrides earlier):
|
|
94
|
+
|
|
95
|
+
1. Global configuration (from initializer)
|
|
96
|
+
2. Per-service configuration (from `config` class method)
|
|
97
|
+
3. Per-call configuration (from `run` or `with` arguments)
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
# Global: raise_on_error = false
|
|
101
|
+
Light::Services.configure do |config|
|
|
102
|
+
config.raise_on_error = false
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Per-service: raise_on_error = true (overrides global)
|
|
106
|
+
class MyService < ApplicationService
|
|
107
|
+
config raise_on_error: true
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Per-call: raise_on_error = false (overrides per-service)
|
|
111
|
+
MyService.run(args, { raise_on_error: false })
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Common Configuration Patterns
|
|
115
|
+
|
|
116
|
+
### Strict Mode for Critical Services
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
class Payment::Process < ApplicationService
|
|
120
|
+
config raise_on_error: true, rollback_on_error: true
|
|
121
|
+
|
|
122
|
+
# Any error will raise an exception and rollback the transaction
|
|
123
|
+
end
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Fire-and-Forget Services
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
class Analytics::Track < ApplicationService
|
|
130
|
+
config use_transactions: false, break_on_error: false, load_errors: false
|
|
131
|
+
|
|
132
|
+
# Errors won't stop execution or propagate to parent services
|
|
133
|
+
end
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Background Job Services
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
class BackgroundTaskService < ApplicationService
|
|
140
|
+
# Background jobs typically handle their own transactions
|
|
141
|
+
config use_transactions: false
|
|
142
|
+
end
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Type Enforcement (Enabled by Default)
|
|
146
|
+
|
|
147
|
+
By default, all arguments and outputs must have a `type` option. This helps catch type-related bugs early and makes your services self-documenting.
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
class User::Create < ApplicationService
|
|
151
|
+
arg :name, type: String # ✓ Valid
|
|
152
|
+
arg :email # ✗ Raises MissingTypeError
|
|
153
|
+
output :user, type: User # ✓ Valid
|
|
154
|
+
output :token # ✗ Raises MissingTypeError
|
|
155
|
+
end
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
To disable type enforcement globally (not recommended):
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
Light::Services.configure do |config|
|
|
162
|
+
config.require_type = false
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Or disable for specific services:
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
class LegacyService < ApplicationService
|
|
170
|
+
config require_type: false
|
|
171
|
+
|
|
172
|
+
arg :data # Allowed when require_type is disabled
|
|
173
|
+
output :result # Allowed when require_type is disabled
|
|
174
|
+
end
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Disabling Transactions
|
|
178
|
+
|
|
179
|
+
If you're not using ActiveRecord or want to manage transactions yourself:
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
Light::Services.configure do |config|
|
|
183
|
+
config.use_transactions = false
|
|
184
|
+
end
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Or disable for specific services:
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
class MyService < ApplicationService
|
|
191
|
+
config use_transactions: false
|
|
192
|
+
end
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
{% hint style="info" %}
|
|
196
|
+
When `use_transactions` is `true`, Light Services uses `ActiveRecord::Base.transaction(requires_new: true)` to create savepoints, allowing nested services to rollback independently.
|
|
197
|
+
{% endhint %}
|
|
198
|
+
|
|
199
|
+
## What's Next?
|
|
200
|
+
|
|
201
|
+
Now that you understand configuration, learn about the core concepts:
|
|
202
|
+
|
|
203
|
+
[Next: Concepts](concepts.md)
|
|
204
|
+
|
data/docs/context.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Context
|
|
2
|
+
|
|
3
|
+
Context allows services to be run within the same execution scope, enabling shared state and coordinated transactions.
|
|
4
|
+
|
|
5
|
+
## Key Features
|
|
6
|
+
|
|
7
|
+
- Services share arguments marked as `context: true`
|
|
8
|
+
- If any service fails, the entire context fails and rolls back database changes
|
|
9
|
+
|
|
10
|
+
## How to Run Services in the Same Context
|
|
11
|
+
|
|
12
|
+
To run a service in the same context, call `with(self)` before the `#run` method.
|
|
13
|
+
|
|
14
|
+
## Context Rollback
|
|
15
|
+
|
|
16
|
+
### Example:
|
|
17
|
+
|
|
18
|
+
Let's say we have two services: `User::Create` and `Profile::Create`. We want to ensure that if either service fails, all database changes are rolled back.
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
class User::Create < ApplicationService
|
|
22
|
+
# Arguments
|
|
23
|
+
arg :attributes, type: Hash
|
|
24
|
+
|
|
25
|
+
# Steps
|
|
26
|
+
step :create_user
|
|
27
|
+
step :create_profile
|
|
28
|
+
step :send_welcome_email
|
|
29
|
+
|
|
30
|
+
# Outputs
|
|
31
|
+
output :user, type: User
|
|
32
|
+
output :profile, type: Profile
|
|
33
|
+
|
|
34
|
+
def create_user
|
|
35
|
+
self.user = User.create!(attributes)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def create_profile
|
|
39
|
+
service = Profile::Create
|
|
40
|
+
.with(self) # This runs the service in the same context
|
|
41
|
+
.run(user:)
|
|
42
|
+
|
|
43
|
+
self.profile = service.profile
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# If the Profile::Create service fails, this step and any following steps won't execute
|
|
47
|
+
# And all database changes will be rolled back
|
|
48
|
+
def send_welcome_email
|
|
49
|
+
# We don't run this service in the same context
|
|
50
|
+
# Because we don't care too much if it fails
|
|
51
|
+
service = Mailer::SendWelcomeEmail.run(user:)
|
|
52
|
+
|
|
53
|
+
# Handle the failure manually if needed
|
|
54
|
+
if service.failed?
|
|
55
|
+
# Handle the failure
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Context Arguments
|
|
62
|
+
|
|
63
|
+
Context arguments are shared between services running in the same context. This can make them a bit less predictable and harder to test.
|
|
64
|
+
|
|
65
|
+
It's recommended to use context arguments only when necessary and keep them as close to the root service as possible. For example, you can use them to share `current_user` or `current_organization` between services.
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
class ApplicationService < Light::Services::Base
|
|
69
|
+
arg :current_user, type: User, context: true
|
|
70
|
+
end
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
class Comment::Create < ApplicationService
|
|
75
|
+
# Arguments
|
|
76
|
+
# We don't need to specify current_user here
|
|
77
|
+
# as it's automatically inherited from the ApplicationService
|
|
78
|
+
arg :post_id, type: Integer
|
|
79
|
+
arg :text, type: String
|
|
80
|
+
arg :subscribe, type: [TrueClass, FalseClass]
|
|
81
|
+
|
|
82
|
+
# Steps
|
|
83
|
+
step :create_comment
|
|
84
|
+
step :subscribe_to_post, if: :subscribe?
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def create_comment
|
|
89
|
+
# ...
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def subscribe_to_post
|
|
93
|
+
Post::Subscribe
|
|
94
|
+
.with(self) # Run service in the same context
|
|
95
|
+
.run(post_id:) # We omit current_user here as context will handle it for us
|
|
96
|
+
|
|
97
|
+
# If we run Post::Subscribe without `with(self)`
|
|
98
|
+
# It'll fail because it won't have information about the `current_user`
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
class Post::Subscribe < ApplicationService
|
|
105
|
+
# Arguments
|
|
106
|
+
arg :post_id, type: Integer
|
|
107
|
+
|
|
108
|
+
# Steps
|
|
109
|
+
step :subscribe
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def subscribe
|
|
114
|
+
# We have access to current_user here because we run it in the same context
|
|
115
|
+
#
|
|
116
|
+
# Even if we would run this service without context this won't be a problem
|
|
117
|
+
# because we specified this argument in top-level service (ApplicationService)
|
|
118
|
+
current_user.subscriptions.create!(post_id:)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
# What's Next?
|
|
124
|
+
|
|
125
|
+
The next step is to learn about error handling in Light Service.
|
|
126
|
+
|
|
127
|
+
[Next: Errors](errors.md)
|
|
128
|
+
|