light-services 3.1.0 → 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 +17 -1
- data/Gemfile.lock +1 -1
- data/docs/{summary.md → SUMMARY.md} +9 -1
- data/docs/arguments.md +3 -3
- data/docs/concepts.md +18 -18
- 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 +20 -0
- data/lib/light/services/callbacks.rb +256 -58
- 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 +3 -3
- /data/docs/{readme.md → README.md} +0 -0
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,10 +1,26 @@
|
|
|
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
|
+
|
|
13
|
+
## 3.1.1 (2025-12-13)
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Better IDE support for callbacks DSL
|
|
18
|
+
|
|
3
19
|
## 3.1.0 (2025-12-13)
|
|
4
20
|
|
|
5
21
|
### Breaking changes
|
|
6
22
|
|
|
7
|
-
- 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.
|
|
8
24
|
|
|
9
25
|
### Added
|
|
10
26
|
|
data/Gemfile.lock
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Summary
|
|
2
|
+
|
|
3
|
+
## Introduction
|
|
2
4
|
|
|
3
5
|
* [Light Services](README.md)
|
|
4
6
|
* [Quickstart](quickstart.md)
|
|
5
7
|
* [Concepts](concepts.md)
|
|
8
|
+
|
|
9
|
+
## Deep Dive
|
|
10
|
+
|
|
6
11
|
* [Arguments](arguments.md)
|
|
7
12
|
* [Steps](steps.md)
|
|
8
13
|
* [Outputs](outputs.md)
|
|
@@ -14,6 +19,9 @@
|
|
|
14
19
|
* [Rails Generators](generators.md)
|
|
15
20
|
* [RuboCop Integration](rubocop.md)
|
|
16
21
|
* [Ruby LSP Integration](ruby-lsp.md)
|
|
22
|
+
|
|
23
|
+
## Examples
|
|
24
|
+
|
|
17
25
|
* [Best Practices](best-practices.md)
|
|
18
26
|
* [Recipes](recipes.md)
|
|
19
27
|
* [CRUD](crud.md)
|
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/concepts.md
CHANGED
|
@@ -7,39 +7,39 @@ This section covers the core concepts of Light Services: **Arguments**, **Steps*
|
|
|
7
7
|
When you call `MyService.run(args)`, the following happens:
|
|
8
8
|
|
|
9
9
|
```
|
|
10
|
-
|
|
10
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
11
11
|
│ Service.run(args) │
|
|
12
|
-
|
|
12
|
+
├──────────────────────────────────────────────────────────────┤
|
|
13
13
|
│ 1. Load default values for arguments and outputs │
|
|
14
14
|
│ 2. Validate argument types │
|
|
15
15
|
│ 3. Run before_service_run callbacks │
|
|
16
|
-
|
|
16
|
+
├──────────────────────────────────────────────────────────────┤
|
|
17
17
|
│ 4. Begin around_service_run callback │
|
|
18
18
|
│ 5. Begin database transaction (if use_transactions: true) │
|
|
19
|
-
│ ┌─────────────────────────────────────────────────────┐
|
|
20
|
-
│ │ 6. Execute steps in order │
|
|
21
|
-
│ │ - Run before_step_run / around_step_run │
|
|
22
|
-
│ │ - Execute step method │
|
|
23
|
-
│ │ - Run after_step_run / on_step_success │
|
|
24
|
-
│ │ - Skip if condition (if:/unless:) not met │
|
|
25
|
-
│ │ - Stop if errors.break? is true │
|
|
26
|
-
│ │ - Stop if stop! was called │
|
|
27
|
-
│ ├─────────────────────────────────────────────────────┤
|
|
28
|
-
│ │ 7. On error → Rollback transaction │
|
|
29
|
-
│ │ On success → Commit transaction │
|
|
30
|
-
│ └─────────────────────────────────────────────────────┘
|
|
19
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
20
|
+
│ │ 6. Execute steps in order │ │
|
|
21
|
+
│ │ - Run before_step_run / around_step_run │ │
|
|
22
|
+
│ │ - Execute step method │ │
|
|
23
|
+
│ │ - Run after_step_run / on_step_success │ │
|
|
24
|
+
│ │ - Skip if condition (if:/unless:) not met │ │
|
|
25
|
+
│ │ - Stop if errors.break? is true │ │
|
|
26
|
+
│ │ - Stop if stop! was called │ │
|
|
27
|
+
│ ├─────────────────────────────────────────────────────┤ │
|
|
28
|
+
│ │ 7. On error → Rollback transaction │ │
|
|
29
|
+
│ │ On success → Commit transaction │ │
|
|
30
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
31
31
|
│ 8. End around_service_run callback │
|
|
32
|
-
|
|
32
|
+
├──────────────────────────────────────────────────────────────┤
|
|
33
33
|
│ 9. Run steps marked with always: true (unless stop! called) │
|
|
34
34
|
│ 10. Validate output types (if success) │
|
|
35
35
|
│ 11. Copy errors/warnings to parent service (if in context) │
|
|
36
36
|
│ 12. Run after_service_run callback │
|
|
37
37
|
│ 13. Run on_service_success or on_service_failure callback │
|
|
38
|
-
|
|
38
|
+
├──────────────────────────────────────────────────────────────┤
|
|
39
39
|
│ 14. Return service instance │
|
|
40
40
|
│ - service.success? / service.failed? │
|
|
41
41
|
│ - service.outputs / service.errors │
|
|
42
|
-
|
|
42
|
+
└──────────────────────────────────────────────────────────────┘
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
## Arguments
|
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
|
@@ -43,6 +43,7 @@ module Light
|
|
|
43
43
|
# result.success? # => true
|
|
44
44
|
# result.user # => #<User id: 1, name: "John">
|
|
45
45
|
class Base
|
|
46
|
+
extend CallbackDsl
|
|
46
47
|
include Callbacks
|
|
47
48
|
include Dsl::ArgumentsDsl
|
|
48
49
|
include Dsl::OutputsDsl
|
|
@@ -135,6 +136,25 @@ module Light
|
|
|
135
136
|
raise Light::Services::StopExecution
|
|
136
137
|
end
|
|
137
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
|
+
|
|
138
158
|
# Execute the service steps.
|
|
139
159
|
#
|
|
140
160
|
# @return [void]
|
|
@@ -48,64 +48,6 @@ module Light
|
|
|
48
48
|
:on_service_failure,
|
|
49
49
|
].freeze
|
|
50
50
|
|
|
51
|
-
def self.included(base)
|
|
52
|
-
base.extend(ClassMethods)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Class methods for registering callbacks.
|
|
56
|
-
#
|
|
57
|
-
# Each callback event has a corresponding class method:
|
|
58
|
-
# - {before_step_run} - before each step executes
|
|
59
|
-
# - {after_step_run} - after each step executes
|
|
60
|
-
# - {around_step_run} - wraps step execution (must yield)
|
|
61
|
-
# - {on_step_success} - when a step completes without adding errors
|
|
62
|
-
# - {on_step_failure} - when a step adds errors
|
|
63
|
-
# - {on_step_crash} - when a step raises an exception
|
|
64
|
-
# - {before_service_run} - before the service starts
|
|
65
|
-
# - {after_service_run} - after the service completes
|
|
66
|
-
# - {around_service_run} - wraps service execution (must yield)
|
|
67
|
-
# - {on_service_success} - when service completes without errors
|
|
68
|
-
# - {on_service_failure} - when service completes with errors
|
|
69
|
-
module ClassMethods
|
|
70
|
-
# Define DSL methods for each callback event
|
|
71
|
-
EVENTS.each do |event|
|
|
72
|
-
define_method(event) do |method_name = nil, &block|
|
|
73
|
-
callback = method_name || block
|
|
74
|
-
raise ArgumentError, "#{event} requires a method name (symbol) or a block" unless callback
|
|
75
|
-
|
|
76
|
-
unless callback.is_a?(Symbol) || callback.is_a?(Proc)
|
|
77
|
-
raise ArgumentError,
|
|
78
|
-
"#{event} callback must be a Symbol or Proc"
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
callbacks_for(event) << callback
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# Get callbacks defined in this class for a specific event.
|
|
86
|
-
#
|
|
87
|
-
# @param event [Symbol] the callback event name
|
|
88
|
-
# @return [Array<Symbol, Proc>] callbacks for this event
|
|
89
|
-
def callbacks_for(event)
|
|
90
|
-
@callbacks ||= {}
|
|
91
|
-
@callbacks[event] ||= []
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
# Get all callbacks for an event including inherited ones.
|
|
95
|
-
#
|
|
96
|
-
# @param event [Symbol] the callback event name
|
|
97
|
-
# @return [Array<Symbol, Proc>] all callbacks for this event
|
|
98
|
-
def all_callbacks_for(event)
|
|
99
|
-
if superclass.respond_to?(:all_callbacks_for)
|
|
100
|
-
inherited = superclass.all_callbacks_for(event)
|
|
101
|
-
else
|
|
102
|
-
inherited = []
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
inherited + callbacks_for(event)
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
51
|
# Run all callbacks for a given event.
|
|
110
52
|
#
|
|
111
53
|
# @param event [Symbol] the callback event name
|
|
@@ -153,5 +95,261 @@ module Light
|
|
|
153
95
|
end
|
|
154
96
|
end
|
|
155
97
|
end
|
|
98
|
+
|
|
99
|
+
# Class methods for registering callbacks.
|
|
100
|
+
# Extend this module in your service class to get callback DSL methods.
|
|
101
|
+
#
|
|
102
|
+
# @example
|
|
103
|
+
# class MyService < Light::Services::Base
|
|
104
|
+
# before_service_run :setup
|
|
105
|
+
# after_service_run :cleanup
|
|
106
|
+
# end
|
|
107
|
+
module CallbackDsl
|
|
108
|
+
# Registers a callback to run before each step executes.
|
|
109
|
+
#
|
|
110
|
+
# @param method_name [Symbol, nil] name of the instance method to call
|
|
111
|
+
# @yield [service, step_name] block to execute if no method name provided
|
|
112
|
+
# @yieldparam service [Light::Services::Base] the service instance
|
|
113
|
+
# @yieldparam step_name [Symbol] the name of the step about to run
|
|
114
|
+
# @return [void]
|
|
115
|
+
# @raise [ArgumentError] if neither method name nor block is provided
|
|
116
|
+
#
|
|
117
|
+
# @example With method name
|
|
118
|
+
# before_step_run :log_step_start
|
|
119
|
+
#
|
|
120
|
+
# @example With block
|
|
121
|
+
# before_step_run { |service, step_name| puts "Starting #{step_name}" }
|
|
122
|
+
def before_step_run(method_name = nil, &block)
|
|
123
|
+
register_callback(:before_step_run, method_name, &block)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Registers a callback to run after each step executes.
|
|
127
|
+
#
|
|
128
|
+
# @param method_name [Symbol, nil] name of the instance method to call
|
|
129
|
+
# @yield [service, step_name] block to execute if no method name provided
|
|
130
|
+
# @yieldparam service [Light::Services::Base] the service instance
|
|
131
|
+
# @yieldparam step_name [Symbol] the name of the step that just ran
|
|
132
|
+
# @return [void]
|
|
133
|
+
# @raise [ArgumentError] if neither method name nor block is provided
|
|
134
|
+
#
|
|
135
|
+
# @example With method name
|
|
136
|
+
# after_step_run :log_step_complete
|
|
137
|
+
#
|
|
138
|
+
# @example With block
|
|
139
|
+
# after_step_run { |service, step_name| puts "Finished #{step_name}" }
|
|
140
|
+
def after_step_run(method_name = nil, &block)
|
|
141
|
+
register_callback(:after_step_run, method_name, &block)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Registers an around callback that wraps each step execution.
|
|
145
|
+
# The callback must yield to execute the step.
|
|
146
|
+
#
|
|
147
|
+
# @param method_name [Symbol, nil] name of the instance method to call
|
|
148
|
+
# @yield [service, step_name] block to execute if no method name provided
|
|
149
|
+
# @yieldparam service [Light::Services::Base] the service instance
|
|
150
|
+
# @yieldparam step_name [Symbol] the name of the step being wrapped
|
|
151
|
+
# @return [void]
|
|
152
|
+
# @raise [ArgumentError] if neither method name nor block is provided
|
|
153
|
+
#
|
|
154
|
+
# @example With method name
|
|
155
|
+
# around_step_run :with_step_timing
|
|
156
|
+
#
|
|
157
|
+
# def with_step_timing(service, step_name)
|
|
158
|
+
# start = Time.now
|
|
159
|
+
# yield
|
|
160
|
+
# puts "#{step_name} took #{Time.now - start}s"
|
|
161
|
+
# end
|
|
162
|
+
def around_step_run(method_name = nil, &block)
|
|
163
|
+
register_callback(:around_step_run, method_name, &block)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Registers a callback to run when a step completes successfully (without adding errors).
|
|
167
|
+
#
|
|
168
|
+
# @param method_name [Symbol, nil] name of the instance method to call
|
|
169
|
+
# @yield [service, step_name] block to execute if no method name provided
|
|
170
|
+
# @yieldparam service [Light::Services::Base] the service instance
|
|
171
|
+
# @yieldparam step_name [Symbol] the name of the successful step
|
|
172
|
+
# @return [void]
|
|
173
|
+
# @raise [ArgumentError] if neither method name nor block is provided
|
|
174
|
+
#
|
|
175
|
+
# @example With method name
|
|
176
|
+
# on_step_success :track_step_success
|
|
177
|
+
#
|
|
178
|
+
# @example With block
|
|
179
|
+
# on_step_success { |service, step_name| Analytics.track("step.success", step: step_name) }
|
|
180
|
+
def on_step_success(method_name = nil, &block)
|
|
181
|
+
register_callback(:on_step_success, method_name, &block)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Registers a callback to run when a step fails (adds errors).
|
|
185
|
+
#
|
|
186
|
+
# @param method_name [Symbol, nil] name of the instance method to call
|
|
187
|
+
# @yield [service, step_name] block to execute if no method name provided
|
|
188
|
+
# @yieldparam service [Light::Services::Base] the service instance
|
|
189
|
+
# @yieldparam step_name [Symbol] the name of the failed step
|
|
190
|
+
# @return [void]
|
|
191
|
+
# @raise [ArgumentError] if neither method name nor block is provided
|
|
192
|
+
#
|
|
193
|
+
# @example With method name
|
|
194
|
+
# on_step_failure :handle_step_error
|
|
195
|
+
#
|
|
196
|
+
# @example With block
|
|
197
|
+
# on_step_failure { |service, step_name| Rails.logger.error("Step #{step_name} failed") }
|
|
198
|
+
def on_step_failure(method_name = nil, &block)
|
|
199
|
+
register_callback(:on_step_failure, method_name, &block)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Registers a callback to run when a step raises an exception.
|
|
203
|
+
#
|
|
204
|
+
# @param method_name [Symbol, nil] name of the instance method to call
|
|
205
|
+
# @yield [service, step_name, exception] block to execute if no method name provided
|
|
206
|
+
# @yieldparam service [Light::Services::Base] the service instance
|
|
207
|
+
# @yieldparam step_name [Symbol] the name of the crashed step
|
|
208
|
+
# @yieldparam exception [Exception] the exception that was raised
|
|
209
|
+
# @return [void]
|
|
210
|
+
# @raise [ArgumentError] if neither method name nor block is provided
|
|
211
|
+
#
|
|
212
|
+
# @example With method name
|
|
213
|
+
# on_step_crash :report_crash
|
|
214
|
+
#
|
|
215
|
+
# @example With block
|
|
216
|
+
# on_step_crash { |service, step_name, error| Sentry.capture_exception(error) }
|
|
217
|
+
def on_step_crash(method_name = nil, &block)
|
|
218
|
+
register_callback(:on_step_crash, method_name, &block)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Registers a callback to run before the service starts executing.
|
|
222
|
+
#
|
|
223
|
+
# @param method_name [Symbol, nil] name of the instance method to call
|
|
224
|
+
# @yield [service] block to execute if no method name provided
|
|
225
|
+
# @yieldparam service [Light::Services::Base] the service instance
|
|
226
|
+
# @return [void]
|
|
227
|
+
# @raise [ArgumentError] if neither method name nor block is provided
|
|
228
|
+
#
|
|
229
|
+
# @example With method name
|
|
230
|
+
# before_service_run :log_start
|
|
231
|
+
#
|
|
232
|
+
# @example With block
|
|
233
|
+
# before_service_run { |service| Rails.logger.info("Starting #{service.class.name}") }
|
|
234
|
+
def before_service_run(method_name = nil, &block)
|
|
235
|
+
register_callback(:before_service_run, method_name, &block)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Registers a callback to run after the service completes (regardless of success/failure).
|
|
239
|
+
#
|
|
240
|
+
# @param method_name [Symbol, nil] name of the instance method to call
|
|
241
|
+
# @yield [service] block to execute if no method name provided
|
|
242
|
+
# @yieldparam service [Light::Services::Base] the service instance
|
|
243
|
+
# @return [void]
|
|
244
|
+
# @raise [ArgumentError] if neither method name nor block is provided
|
|
245
|
+
#
|
|
246
|
+
# @example With method name
|
|
247
|
+
# after_service_run :cleanup
|
|
248
|
+
#
|
|
249
|
+
# @example With block
|
|
250
|
+
# after_service_run { |service| Rails.logger.info("Done!") }
|
|
251
|
+
def after_service_run(method_name = nil, &block)
|
|
252
|
+
register_callback(:after_service_run, method_name, &block)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Registers an around callback that wraps the entire service execution.
|
|
256
|
+
# The callback must yield to execute the service.
|
|
257
|
+
#
|
|
258
|
+
# @param method_name [Symbol, nil] name of the instance method to call
|
|
259
|
+
# @yield [service] block to execute if no method name provided
|
|
260
|
+
# @yieldparam service [Light::Services::Base] the service instance
|
|
261
|
+
# @return [void]
|
|
262
|
+
# @raise [ArgumentError] if neither method name nor block is provided
|
|
263
|
+
#
|
|
264
|
+
# @example With method name
|
|
265
|
+
# around_service_run :with_timing
|
|
266
|
+
#
|
|
267
|
+
# def with_timing(service)
|
|
268
|
+
# start = Time.now
|
|
269
|
+
# yield
|
|
270
|
+
# puts "Took #{Time.now - start}s"
|
|
271
|
+
# end
|
|
272
|
+
def around_service_run(method_name = nil, &block)
|
|
273
|
+
register_callback(:around_service_run, method_name, &block)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Registers a callback to run when the service completes successfully (without errors).
|
|
277
|
+
#
|
|
278
|
+
# @param method_name [Symbol, nil] name of the instance method to call
|
|
279
|
+
# @yield [service] block to execute if no method name provided
|
|
280
|
+
# @yieldparam service [Light::Services::Base] the service instance
|
|
281
|
+
# @return [void]
|
|
282
|
+
# @raise [ArgumentError] if neither method name nor block is provided
|
|
283
|
+
#
|
|
284
|
+
# @example With method name
|
|
285
|
+
# on_service_success :send_notification
|
|
286
|
+
#
|
|
287
|
+
# @example With block
|
|
288
|
+
# on_service_success { |service| NotificationMailer.success(service.user).deliver_later }
|
|
289
|
+
def on_service_success(method_name = nil, &block)
|
|
290
|
+
register_callback(:on_service_success, method_name, &block)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Registers a callback to run when the service completes with errors.
|
|
294
|
+
#
|
|
295
|
+
# @param method_name [Symbol, nil] name of the instance method to call
|
|
296
|
+
# @yield [service] block to execute if no method name provided
|
|
297
|
+
# @yieldparam service [Light::Services::Base] the service instance
|
|
298
|
+
# @return [void]
|
|
299
|
+
# @raise [ArgumentError] if neither method name nor block is provided
|
|
300
|
+
#
|
|
301
|
+
# @example With method name
|
|
302
|
+
# on_service_failure :log_error
|
|
303
|
+
#
|
|
304
|
+
# @example With block
|
|
305
|
+
# on_service_failure { |service| Rails.logger.error(service.errors.full_messages) }
|
|
306
|
+
def on_service_failure(method_name = nil, &block)
|
|
307
|
+
register_callback(:on_service_failure, method_name, &block)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Get callbacks defined in this class for a specific event.
|
|
311
|
+
#
|
|
312
|
+
# @param event [Symbol] the callback event name
|
|
313
|
+
# @return [Array<Symbol, Proc>] callbacks for this event
|
|
314
|
+
def callbacks_for(event)
|
|
315
|
+
@callbacks ||= {}
|
|
316
|
+
@callbacks[event] ||= []
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Get all callbacks for an event including inherited ones.
|
|
320
|
+
#
|
|
321
|
+
# @param event [Symbol] the callback event name
|
|
322
|
+
# @return [Array<Symbol, Proc>] all callbacks for this event
|
|
323
|
+
def all_callbacks_for(event)
|
|
324
|
+
if superclass.respond_to?(:all_callbacks_for)
|
|
325
|
+
inherited = superclass.all_callbacks_for(event)
|
|
326
|
+
else
|
|
327
|
+
inherited = []
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
inherited + callbacks_for(event)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
private
|
|
334
|
+
|
|
335
|
+
# Registers a callback for a given event.
|
|
336
|
+
#
|
|
337
|
+
# @param event [Symbol] the callback event name
|
|
338
|
+
# @param method_name [Symbol, nil] name of the instance method to call
|
|
339
|
+
# @yield block to execute if no method name provided
|
|
340
|
+
# @return [void]
|
|
341
|
+
# @raise [ArgumentError] if neither method name nor block is provided
|
|
342
|
+
# @api private
|
|
343
|
+
def register_callback(event, method_name = nil, &block)
|
|
344
|
+
callback = method_name || block
|
|
345
|
+
raise ArgumentError, "#{event} requires a method name (symbol) or a block" unless callback
|
|
346
|
+
|
|
347
|
+
unless callback.is_a?(Symbol) || callback.is_a?(Proc)
|
|
348
|
+
raise ArgumentError, "#{event} callback must be a Symbol or Proc"
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
callbacks_for(event) << callback
|
|
352
|
+
end
|
|
353
|
+
end
|
|
156
354
|
end
|
|
157
355
|
end
|
|
@@ -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
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: light-services
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.1.
|
|
4
|
+
version: 3.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Kodkod
|
|
@@ -34,6 +34,8 @@ files:
|
|
|
34
34
|
- Rakefile
|
|
35
35
|
- bin/console
|
|
36
36
|
- bin/setup
|
|
37
|
+
- docs/README.md
|
|
38
|
+
- docs/SUMMARY.md
|
|
37
39
|
- docs/arguments.md
|
|
38
40
|
- docs/best-practices.md
|
|
39
41
|
- docs/callbacks.md
|
|
@@ -46,13 +48,11 @@ files:
|
|
|
46
48
|
- docs/outputs.md
|
|
47
49
|
- docs/pundit-authorization.md
|
|
48
50
|
- docs/quickstart.md
|
|
49
|
-
- docs/readme.md
|
|
50
51
|
- docs/recipes.md
|
|
51
52
|
- docs/rubocop.md
|
|
52
53
|
- docs/ruby-lsp.md
|
|
53
54
|
- docs/service-rendering.md
|
|
54
55
|
- docs/steps.md
|
|
55
|
-
- docs/summary.md
|
|
56
56
|
- docs/testing.md
|
|
57
57
|
- lib/generators/light_services/install/USAGE
|
|
58
58
|
- lib/generators/light_services/install/install_generator.rb
|
|
File without changes
|