light-services 3.1.1 → 3.1.2
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 +11 -1
- data/Gemfile.lock +1 -1
- data/docs/arguments.md +3 -3
- data/docs/configuration.md +22 -7
- data/docs/errors.md +25 -4
- data/docs/outputs.md +3 -3
- data/docs/steps.md +52 -0
- data/lib/light/services/base.rb +19 -0
- data/lib/light/services/concerns/execution.rb +4 -0
- data/lib/light/services/config.rb +20 -5
- data/lib/light/services/dsl/validation.rb +16 -5
- data/lib/light/services/exceptions.rb +4 -0
- data/lib/light/services/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1c9112f92c81696cc02f0d3440b848abca3a501f503b9bf95e645296d0a122dc
|
|
4
|
+
data.tar.gz: 22c0672f290fc82f843d55f31060941dceedae0a991eb25697010ffd12b5fae9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4b851d34f2c6a89a63a2d2f747db12b75f50741aabcbb3cbda4d8154a1ba65560894c2cabe4ac94f7f50f4ffc8fcdeb6280b83aa61a53888f76dd691c3cf1770
|
|
7
|
+
data.tar.gz: 1e2ca16aaf58aee4d8adcda2d83ea881761a3f1c09543461edf5ad2516d91ee76b639eae20477c66c94619437b51a7e717932d5cc9750d18a0d6b2bd9e0e4036
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.1.2 (2025-12-13)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Add `fail!` and `fail_immediately!` helpers
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Split `config.require_type` into `config.require_arg_type` and `config.require_output_type`
|
|
12
|
+
|
|
3
13
|
## 3.1.1 (2025-12-13)
|
|
4
14
|
|
|
5
15
|
### Added
|
|
@@ -10,7 +20,7 @@
|
|
|
10
20
|
|
|
11
21
|
### Breaking changes
|
|
12
22
|
|
|
13
|
-
- Enforce arguments and output types by default.
|
|
23
|
+
- Enforce arguments and output types by default. Use `config.require_arg_type = false` and `config.require_output_type = false` to disable this behavior. The convenience setter `config.require_type = false` sets both options at once for backward compatibility.
|
|
14
24
|
|
|
15
25
|
### Added
|
|
16
26
|
|
data/Gemfile.lock
CHANGED
data/docs/arguments.md
CHANGED
|
@@ -66,13 +66,13 @@ class MyService < ApplicationService
|
|
|
66
66
|
end
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
To disable type enforcement for a specific service:
|
|
69
|
+
To disable type enforcement for arguments in a specific service:
|
|
70
70
|
|
|
71
71
|
```ruby
|
|
72
72
|
class LegacyService < ApplicationService
|
|
73
|
-
config
|
|
73
|
+
config require_arg_type: false
|
|
74
74
|
|
|
75
|
-
arg :name # Allowed when
|
|
75
|
+
arg :name # Allowed when require_arg_type is disabled
|
|
76
76
|
end
|
|
77
77
|
```
|
|
78
78
|
|
data/docs/configuration.md
CHANGED
|
@@ -8,8 +8,9 @@ Configure Light Services globally using an initializer. For Rails applications,
|
|
|
8
8
|
|
|
9
9
|
```ruby
|
|
10
10
|
Light::Services.configure do |config|
|
|
11
|
-
# Type enforcement
|
|
12
|
-
config.
|
|
11
|
+
# Type enforcement
|
|
12
|
+
config.require_arg_type = true # Require type option for all arguments
|
|
13
|
+
config.require_output_type = true # Require type option for all outputs
|
|
13
14
|
|
|
14
15
|
# Transaction settings
|
|
15
16
|
config.use_transactions = true # Wrap each service in a database transaction
|
|
@@ -32,7 +33,8 @@ end
|
|
|
32
33
|
|
|
33
34
|
| Option | Default | Description |
|
|
34
35
|
|--------|---------|-------------|
|
|
35
|
-
| `
|
|
36
|
+
| `require_arg_type` | `true` | Raises `Light::Services::MissingTypeError` when defining arguments without a `type` option |
|
|
37
|
+
| `require_output_type` | `true` | Raises `Light::Services::MissingTypeError` when defining outputs without a `type` option |
|
|
36
38
|
| `use_transactions` | `true` | Wraps service execution in `ActiveRecord::Base.transaction` |
|
|
37
39
|
| `load_errors` | `true` | Propagates errors to parent service when using `.with(self)` |
|
|
38
40
|
| `break_on_error` | `true` | Stops executing remaining steps when an error is added |
|
|
@@ -159,7 +161,8 @@ To disable type enforcement globally (not recommended):
|
|
|
159
161
|
|
|
160
162
|
```ruby
|
|
161
163
|
Light::Services.configure do |config|
|
|
162
|
-
config.
|
|
164
|
+
config.require_arg_type = false # Disable for arguments
|
|
165
|
+
config.require_output_type = false # Disable for outputs
|
|
163
166
|
end
|
|
164
167
|
```
|
|
165
168
|
|
|
@@ -167,10 +170,22 @@ Or disable for specific services:
|
|
|
167
170
|
|
|
168
171
|
```ruby
|
|
169
172
|
class LegacyService < ApplicationService
|
|
170
|
-
config
|
|
173
|
+
config require_arg_type: false, require_output_type: false
|
|
171
174
|
|
|
172
|
-
arg :data # Allowed when
|
|
173
|
-
output :result # Allowed when
|
|
175
|
+
arg :data # Allowed when require_arg_type is disabled
|
|
176
|
+
output :result # Allowed when require_output_type is disabled
|
|
177
|
+
end
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
You can also control them independently:
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
class StrictInputService < ApplicationService
|
|
184
|
+
# Require types for arguments but not outputs
|
|
185
|
+
config require_arg_type: true, require_output_type: false
|
|
186
|
+
|
|
187
|
+
arg :data, type: Hash # Type required
|
|
188
|
+
output :result # Type not required
|
|
174
189
|
end
|
|
175
190
|
```
|
|
176
191
|
|
data/docs/errors.md
CHANGED
|
@@ -44,6 +44,24 @@ class ParsePage < ApplicationService
|
|
|
44
44
|
end
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
## Quick Error with `fail!`
|
|
48
|
+
|
|
49
|
+
The `fail!` method is a shortcut for adding an error to the `:base` key:
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
class ParsePage < ApplicationService
|
|
53
|
+
def validate
|
|
54
|
+
fail!("URL is required") if url.blank?
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This is equivalent to:
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
errors.add(:base, "URL is required")
|
|
63
|
+
```
|
|
64
|
+
|
|
47
65
|
## Reading Errors
|
|
48
66
|
|
|
49
67
|
To check if a service has errors, you can use the `#failed?` method. You can also use methods like `errors.any?` to inspect errors.
|
|
@@ -205,11 +223,13 @@ Light Services defines several exception classes for different error scenarios:
|
|
|
205
223
|
| `Light::Services::ReservedNameError` | Raised when using a reserved name for arguments, outputs, or steps |
|
|
206
224
|
| `Light::Services::InvalidNameError` | Raised when using an invalid name format |
|
|
207
225
|
| `Light::Services::NoStepsError` | Raised when a service has no steps defined and no `run` method |
|
|
208
|
-
| `Light::Services::MissingTypeError` | Raised when defining an argument or output without a `type` option when `
|
|
226
|
+
| `Light::Services::MissingTypeError` | Raised when defining an argument or output without a `type` option when `require_arg_type` or `require_output_type` is enabled |
|
|
227
|
+
| `Light::Services::StopExecution` | Control flow exception raised by `stop_immediately!` to halt execution without rollback |
|
|
228
|
+
| `Light::Services::FailExecution` | Control flow exception raised by `fail_immediately!` to halt execution and rollback transactions |
|
|
209
229
|
|
|
210
230
|
### MissingTypeError
|
|
211
231
|
|
|
212
|
-
This exception is raised when you define an argument or output without a `type` option. Since `
|
|
232
|
+
This exception is raised when you define an argument or output without a `type` option. Since `require_arg_type` and `require_output_type` are enabled by default, all arguments and outputs must have a type.
|
|
213
233
|
|
|
214
234
|
```ruby
|
|
215
235
|
class MyService < ApplicationService
|
|
@@ -230,9 +250,10 @@ If you need to disable type enforcement for legacy services, you can use the `co
|
|
|
230
250
|
|
|
231
251
|
```ruby
|
|
232
252
|
class LegacyService < ApplicationService
|
|
233
|
-
config
|
|
253
|
+
config require_arg_type: false, require_output_type: false
|
|
234
254
|
|
|
235
|
-
arg :data # Allowed when
|
|
255
|
+
arg :data # Allowed when require_arg_type is disabled
|
|
256
|
+
output :result # Allowed when require_output_type is disabled
|
|
236
257
|
end
|
|
237
258
|
```
|
|
238
259
|
|
data/docs/outputs.md
CHANGED
|
@@ -89,13 +89,13 @@ class MyService < ApplicationService
|
|
|
89
89
|
end
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
To disable type enforcement for a specific service:
|
|
92
|
+
To disable type enforcement for outputs in a specific service:
|
|
93
93
|
|
|
94
94
|
```ruby
|
|
95
95
|
class LegacyService < ApplicationService
|
|
96
|
-
config
|
|
96
|
+
config require_output_type: false
|
|
97
97
|
|
|
98
|
-
output :data # Allowed when
|
|
98
|
+
output :data # Allowed when require_output_type is disabled
|
|
99
99
|
end
|
|
100
100
|
```
|
|
101
101
|
|
data/docs/steps.md
CHANGED
|
@@ -292,6 +292,58 @@ end
|
|
|
292
292
|
**Database Transactions:** Calling `stop_immediately!` does NOT rollback database transactions. All database changes made before `stop_immediately!` was called will be committed.
|
|
293
293
|
{% endhint %}
|
|
294
294
|
|
|
295
|
+
## Immediate Failure with `fail_immediately!`
|
|
296
|
+
|
|
297
|
+
Use `fail_immediately!` when you need to halt execution immediately AND rollback any database transactions. Unlike `stop_immediately!`, this method adds an error and causes transaction rollback.
|
|
298
|
+
|
|
299
|
+
```ruby
|
|
300
|
+
class Payment::Process < ApplicationService
|
|
301
|
+
arg :amount, type: Integer
|
|
302
|
+
arg :card_token, type: String
|
|
303
|
+
|
|
304
|
+
step :validate_card
|
|
305
|
+
step :charge_card
|
|
306
|
+
step :send_receipt
|
|
307
|
+
|
|
308
|
+
output :transaction_id, type: String
|
|
309
|
+
|
|
310
|
+
private
|
|
311
|
+
|
|
312
|
+
def validate_card
|
|
313
|
+
unless valid_card?(card_token)
|
|
314
|
+
fail_immediately!("Card validation failed")
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# This code won't run if card is invalid
|
|
318
|
+
log_validation_success
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def charge_card
|
|
322
|
+
# This step won't run if fail_immediately! was called
|
|
323
|
+
self.transaction_id = PaymentGateway.charge(amount, card_token)
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
{% hint style="warning" %}
|
|
329
|
+
`fail_immediately!` raises an internal exception to halt execution. Steps marked with `always: true` will NOT run when `fail_immediately!` is called.
|
|
330
|
+
{% endhint %}
|
|
331
|
+
|
|
332
|
+
{% hint style="danger" %}
|
|
333
|
+
**Database Transactions:** Calling `fail_immediately!` DOES rollback database transactions. All database changes made before `fail_immediately!` was called will be rolled back.
|
|
334
|
+
{% endhint %}
|
|
335
|
+
|
|
336
|
+
### Comparison Table
|
|
337
|
+
|
|
338
|
+
| Method | Adds Error | Stops Execution | Transaction Rollback |
|
|
339
|
+
|--------|------------|-----------------|---------------------|
|
|
340
|
+
| `stop!` | No | After current step | No |
|
|
341
|
+
| `stop_immediately!` | No | Immediately | No |
|
|
342
|
+
| `fail!(msg)` | Yes (:base) | After current step* | No |
|
|
343
|
+
| `fail_immediately!(msg)` | Yes (:base) | Immediately | Yes |
|
|
344
|
+
|
|
345
|
+
*By default, adding an error stops subsequent steps from running due to `break_on_add` configuration.
|
|
346
|
+
|
|
295
347
|
## Removing Inherited Steps
|
|
296
348
|
|
|
297
349
|
When inheriting from a parent service, you can remove steps using `remove_step`:
|
data/lib/light/services/base.rb
CHANGED
|
@@ -136,6 +136,25 @@ module Light
|
|
|
136
136
|
raise Light::Services::StopExecution
|
|
137
137
|
end
|
|
138
138
|
|
|
139
|
+
# Add an error to the :base key.
|
|
140
|
+
#
|
|
141
|
+
# @param message [String] the error message
|
|
142
|
+
# @return [void]
|
|
143
|
+
def fail!(message)
|
|
144
|
+
errors.add(:base, message)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Add an error and stop execution immediately, causing transaction rollback.
|
|
148
|
+
#
|
|
149
|
+
# @param message [String] the error message
|
|
150
|
+
# @raise [FailExecution] always raises to halt execution and rollback
|
|
151
|
+
# @return [void]
|
|
152
|
+
def fail_immediately!(message)
|
|
153
|
+
@stopped = true
|
|
154
|
+
errors.add(:base, message, rollback: false)
|
|
155
|
+
raise Light::Services::FailExecution
|
|
156
|
+
end
|
|
157
|
+
|
|
139
158
|
# Execute the service steps.
|
|
140
159
|
#
|
|
141
160
|
# @return [void]
|
|
@@ -44,6 +44,10 @@ module Light
|
|
|
44
44
|
# Gracefully handle stop_immediately! inside transaction to prevent rollback
|
|
45
45
|
@stopped = true
|
|
46
46
|
end
|
|
47
|
+
rescue Light::Services::FailExecution
|
|
48
|
+
# FailExecution bubbles out of transaction (causing rollback) but is caught here
|
|
49
|
+
# @stopped is already set by fail_immediately!
|
|
50
|
+
nil
|
|
47
51
|
end
|
|
48
52
|
|
|
49
53
|
# Run steps with parameter `always` if they weren't launched because of errors/warnings
|
|
@@ -10,7 +10,8 @@ module Light
|
|
|
10
10
|
#
|
|
11
11
|
# @example
|
|
12
12
|
# Light::Services.configure do |config|
|
|
13
|
-
# config.
|
|
13
|
+
# config.require_arg_type = true
|
|
14
|
+
# config.require_output_type = true
|
|
14
15
|
# config.use_transactions = false
|
|
15
16
|
# end
|
|
16
17
|
def configure
|
|
@@ -28,13 +29,16 @@ module Light
|
|
|
28
29
|
# Configuration class for Light::Services global settings.
|
|
29
30
|
#
|
|
30
31
|
# @example Accessing configuration
|
|
31
|
-
# Light::Services.config.
|
|
32
|
+
# Light::Services.config.require_arg_type # => true
|
|
32
33
|
#
|
|
33
34
|
# @example Modifying configuration
|
|
34
35
|
# Light::Services.config.use_transactions = false
|
|
35
36
|
class Config
|
|
36
|
-
# @return [Boolean] whether arguments
|
|
37
|
-
attr_reader :
|
|
37
|
+
# @return [Boolean] whether arguments must have a type specified
|
|
38
|
+
attr_reader :require_arg_type
|
|
39
|
+
|
|
40
|
+
# @return [Boolean] whether outputs must have a type specified
|
|
41
|
+
attr_reader :require_output_type
|
|
38
42
|
|
|
39
43
|
# @return [Boolean] whether to wrap service execution in a database transaction
|
|
40
44
|
attr_reader :use_transactions
|
|
@@ -69,7 +73,8 @@ module Light
|
|
|
69
73
|
attr_reader :ruby_lsp_type_mappings
|
|
70
74
|
|
|
71
75
|
DEFAULTS = {
|
|
72
|
-
|
|
76
|
+
require_arg_type: true,
|
|
77
|
+
require_output_type: true,
|
|
73
78
|
use_transactions: true,
|
|
74
79
|
|
|
75
80
|
load_errors: true,
|
|
@@ -92,6 +97,16 @@ module Light
|
|
|
92
97
|
end
|
|
93
98
|
end
|
|
94
99
|
|
|
100
|
+
# Convenience setter for backward compatibility.
|
|
101
|
+
# Sets both require_arg_type and require_output_type.
|
|
102
|
+
#
|
|
103
|
+
# @param value [Boolean] whether to require types for arguments and outputs
|
|
104
|
+
# @return [void]
|
|
105
|
+
def require_type=(value)
|
|
106
|
+
self.require_arg_type = value
|
|
107
|
+
self.require_output_type = value
|
|
108
|
+
end
|
|
109
|
+
|
|
95
110
|
# Initialize configuration with default values.
|
|
96
111
|
def initialize
|
|
97
112
|
reset_to_defaults!
|
|
@@ -135,26 +135,37 @@ module Light
|
|
|
135
135
|
# @param opts [Hash] the options hash to check for type
|
|
136
136
|
def self.validate_type_required!(name, field_type, service_class, opts)
|
|
137
137
|
return if opts.key?(:type)
|
|
138
|
-
return unless
|
|
138
|
+
return unless require_type_enabled_for?(field_type, service_class)
|
|
139
139
|
|
|
140
|
+
config_name = field_type == :argument ? "require_arg_type" : "require_output_type"
|
|
140
141
|
raise Light::Services::MissingTypeError,
|
|
141
142
|
"#{field_type.to_s.capitalize} `#{name}` in #{service_class} must have a type specified " \
|
|
142
|
-
"(
|
|
143
|
+
"(#{config_name} is enabled)"
|
|
143
144
|
end
|
|
144
145
|
|
|
145
|
-
# Check if require_type is enabled for the service class
|
|
146
|
-
|
|
146
|
+
# Check if require_type is enabled for the given field type and service class
|
|
147
|
+
#
|
|
148
|
+
# @param field_type [Symbol] the type of field (:argument, :output)
|
|
149
|
+
# @param service_class [Class] the service class to check
|
|
150
|
+
# @return [Boolean] whether type is required for the field type
|
|
151
|
+
def self.require_type_enabled_for?(field_type, service_class)
|
|
152
|
+
config_key = field_type == :argument ? :require_arg_type : :require_output_type
|
|
153
|
+
|
|
147
154
|
# Check class-level config in the inheritance chain, then fall back to global config
|
|
148
155
|
klass = service_class
|
|
149
156
|
while klass.respond_to?(:class_config)
|
|
150
157
|
class_config = klass.class_config
|
|
151
158
|
|
|
159
|
+
# Check specific config first (require_arg_type or require_output_type)
|
|
160
|
+
return class_config[config_key] if class_config&.key?(config_key)
|
|
161
|
+
|
|
162
|
+
# Check convenience config (require_type) for backward compatibility
|
|
152
163
|
return class_config[:require_type] if class_config&.key?(:require_type)
|
|
153
164
|
|
|
154
165
|
klass = klass.superclass
|
|
155
166
|
end
|
|
156
167
|
|
|
157
|
-
Light::Services.config.
|
|
168
|
+
Light::Services.config.public_send(config_key)
|
|
158
169
|
end
|
|
159
170
|
end
|
|
160
171
|
end
|
|
@@ -24,6 +24,10 @@ module Light
|
|
|
24
24
|
# Not an error - used to halt execution gracefully.
|
|
25
25
|
class StopExecution < StandardError; end
|
|
26
26
|
|
|
27
|
+
# Control flow exception for fail_immediately!
|
|
28
|
+
# Unlike StopExecution, this exception causes transaction rollback.
|
|
29
|
+
class FailExecution < StandardError; end
|
|
30
|
+
|
|
27
31
|
# @deprecated Use {Error} instead
|
|
28
32
|
NoStepError = Error
|
|
29
33
|
|