light-services 3.0.0 → 3.1.1
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/.rubocop.yml +7 -1
- data/CHANGELOG.md +21 -0
- data/CLAUDE.md +1 -1
- data/Gemfile.lock +1 -1
- data/README.md +11 -11
- data/docs/{readme.md → README.md} +12 -11
- data/docs/{summary.md → SUMMARY.md} +11 -1
- data/docs/arguments.md +23 -0
- data/docs/concepts.md +19 -19
- data/docs/configuration.md +36 -0
- data/docs/errors.md +31 -1
- data/docs/outputs.md +23 -0
- data/docs/quickstart.md +1 -1
- data/docs/rubocop.md +285 -0
- data/docs/ruby-lsp.md +133 -0
- data/docs/steps.md +62 -8
- data/docs/testing.md +1 -1
- data/lib/light/services/base.rb +110 -7
- data/lib/light/services/base_with_context.rb +23 -1
- data/lib/light/services/callbacks.rb +293 -41
- data/lib/light/services/collection.rb +50 -2
- data/lib/light/services/concerns/execution.rb +3 -0
- data/lib/light/services/config.rb +83 -3
- data/lib/light/services/constants.rb +3 -0
- data/lib/light/services/dsl/arguments_dsl.rb +1 -0
- data/lib/light/services/dsl/outputs_dsl.rb +1 -0
- data/lib/light/services/dsl/validation.rb +30 -0
- data/lib/light/services/exceptions.rb +19 -1
- data/lib/light/services/message.rb +28 -3
- data/lib/light/services/messages.rb +74 -2
- 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 +33 -5
- data/lib/light/services/settings/step.rb +23 -5
- data/lib/light/services/version.rb +1 -1
- 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
- metadata +17 -3
data/docs/rubocop.md
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# RuboCop Integration
|
|
2
|
+
|
|
3
|
+
Light Services provides custom RuboCop cops to help enforce best practices in your service definitions.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
Add this to your `.rubocop.yml`:
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
require:
|
|
11
|
+
- light/services/rubocop
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Available Cops
|
|
15
|
+
|
|
16
|
+
### LightServices/ArgumentTypeRequired
|
|
17
|
+
|
|
18
|
+
Ensures all `arg` declarations include a `type:` option.
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
# bad
|
|
22
|
+
arg :user_id
|
|
23
|
+
arg :params, default: {}
|
|
24
|
+
|
|
25
|
+
# good
|
|
26
|
+
arg :user_id, type: Integer
|
|
27
|
+
arg :params, type: Hash, default: {}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### LightServices/OutputTypeRequired
|
|
31
|
+
|
|
32
|
+
Ensures all `output` declarations include a `type:` option.
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
# bad
|
|
36
|
+
output :result
|
|
37
|
+
output :data, optional: true
|
|
38
|
+
|
|
39
|
+
# good
|
|
40
|
+
output :result, type: Hash
|
|
41
|
+
output :data, type: Hash, optional: true
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### LightServices/StepMethodExists
|
|
45
|
+
|
|
46
|
+
Ensures all `step` declarations have a corresponding method defined.
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
# bad
|
|
50
|
+
class MyService < ApplicationService
|
|
51
|
+
step :validate
|
|
52
|
+
step :process # missing method
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def validate; end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# good
|
|
60
|
+
class MyService < ApplicationService
|
|
61
|
+
step :validate
|
|
62
|
+
step :process
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def validate; end
|
|
67
|
+
def process; end
|
|
68
|
+
end
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Configuration:** Use `ExcludedSteps` for inherited steps:
|
|
72
|
+
|
|
73
|
+
```yaml
|
|
74
|
+
LightServices/StepMethodExists:
|
|
75
|
+
ExcludedSteps:
|
|
76
|
+
- initialize_entity
|
|
77
|
+
- assign_attributes
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### LightServices/ConditionMethodExists
|
|
81
|
+
|
|
82
|
+
Ensures symbol conditions (`:if`, `:unless`) have corresponding methods defined.
|
|
83
|
+
|
|
84
|
+
This cop automatically recognizes predicate methods generated by `arg` and `output` declarations (e.g., `arg :user` creates `user?`).
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
# bad
|
|
88
|
+
class MyService < ApplicationService
|
|
89
|
+
step :notify, if: :should_notify? # missing method
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def notify; end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# good - explicit method
|
|
97
|
+
class MyService < ApplicationService
|
|
98
|
+
step :notify, if: :should_notify?
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def notify; end
|
|
103
|
+
def should_notify?; true; end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# good - predicate from arg/output
|
|
107
|
+
class MyService < ApplicationService
|
|
108
|
+
arg :user, type: User, optional: true
|
|
109
|
+
|
|
110
|
+
step :greet, if: :user? # user? is auto-generated
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def greet; end
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Configuration:** Use `ExcludedMethods` for inherited condition methods:
|
|
119
|
+
|
|
120
|
+
```yaml
|
|
121
|
+
LightServices/ConditionMethodExists:
|
|
122
|
+
ExcludedMethods:
|
|
123
|
+
- admin?
|
|
124
|
+
- guest?
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### LightServices/DslOrder
|
|
128
|
+
|
|
129
|
+
Enforces consistent ordering of DSL declarations: `config` → `arg` → `step` → `output`
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
# bad
|
|
133
|
+
class MyService < ApplicationService
|
|
134
|
+
step :process
|
|
135
|
+
arg :name, type: String
|
|
136
|
+
config raise_on_error: true
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# good
|
|
140
|
+
class MyService < ApplicationService
|
|
141
|
+
config raise_on_error: true
|
|
142
|
+
|
|
143
|
+
arg :name, type: String
|
|
144
|
+
|
|
145
|
+
step :process
|
|
146
|
+
|
|
147
|
+
output :result, type: Hash
|
|
148
|
+
end
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### LightServices/MissingPrivateKeyword
|
|
152
|
+
|
|
153
|
+
Ensures step methods are defined as private.
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
# bad
|
|
157
|
+
class MyService < ApplicationService
|
|
158
|
+
step :process
|
|
159
|
+
|
|
160
|
+
def process # should be private
|
|
161
|
+
# implementation
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# good
|
|
166
|
+
class MyService < ApplicationService
|
|
167
|
+
step :process
|
|
168
|
+
|
|
169
|
+
private
|
|
170
|
+
|
|
171
|
+
def process
|
|
172
|
+
# implementation
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### LightServices/NoDirectInstantiation
|
|
178
|
+
|
|
179
|
+
Prevents direct instantiation of service classes with `.new`.
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
# bad
|
|
183
|
+
UserService.new(name: "John")
|
|
184
|
+
|
|
185
|
+
# good
|
|
186
|
+
UserService.run(name: "John")
|
|
187
|
+
UserService.run!(name: "John")
|
|
188
|
+
UserService.call(name: "John")
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**Configuration:** Customize the pattern for service class detection:
|
|
192
|
+
|
|
193
|
+
```yaml
|
|
194
|
+
LightServices/NoDirectInstantiation:
|
|
195
|
+
ServicePattern: 'Service$' # default: matches classes ending with "Service"
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### LightServices/DeprecatedMethods
|
|
199
|
+
|
|
200
|
+
Detects deprecated `done!` and `done?` method calls and suggests using `stop!` and `stopped?` instead. Includes autocorrection.
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
# bad
|
|
204
|
+
class MyService < ApplicationService
|
|
205
|
+
step :process
|
|
206
|
+
|
|
207
|
+
private
|
|
208
|
+
|
|
209
|
+
def process
|
|
210
|
+
done! if condition_met?
|
|
211
|
+
return if done?
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# good
|
|
216
|
+
class MyService < ApplicationService
|
|
217
|
+
step :process
|
|
218
|
+
|
|
219
|
+
private
|
|
220
|
+
|
|
221
|
+
def process
|
|
222
|
+
stop! if condition_met?
|
|
223
|
+
return if stopped?
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**Configuration:** Customize the pattern for service class detection:
|
|
229
|
+
|
|
230
|
+
```yaml
|
|
231
|
+
LightServices/DeprecatedMethods:
|
|
232
|
+
ServicePattern: 'Service$' # default: matches classes ending with "Service"
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Configuration
|
|
236
|
+
|
|
237
|
+
Full configuration example:
|
|
238
|
+
|
|
239
|
+
```yaml
|
|
240
|
+
require:
|
|
241
|
+
- light/services/rubocop
|
|
242
|
+
|
|
243
|
+
LightServices/ArgumentTypeRequired:
|
|
244
|
+
Enabled: true
|
|
245
|
+
|
|
246
|
+
LightServices/OutputTypeRequired:
|
|
247
|
+
Enabled: true
|
|
248
|
+
|
|
249
|
+
LightServices/StepMethodExists:
|
|
250
|
+
Enabled: true
|
|
251
|
+
ExcludedSteps: []
|
|
252
|
+
|
|
253
|
+
LightServices/ConditionMethodExists:
|
|
254
|
+
Enabled: true
|
|
255
|
+
ExcludedMethods: []
|
|
256
|
+
|
|
257
|
+
LightServices/DslOrder:
|
|
258
|
+
Enabled: true
|
|
259
|
+
|
|
260
|
+
LightServices/MissingPrivateKeyword:
|
|
261
|
+
Enabled: true
|
|
262
|
+
|
|
263
|
+
LightServices/NoDirectInstantiation:
|
|
264
|
+
Enabled: true
|
|
265
|
+
ServicePattern: 'Service$'
|
|
266
|
+
|
|
267
|
+
LightServices/DeprecatedMethods:
|
|
268
|
+
Enabled: true
|
|
269
|
+
ServicePattern: 'Service$'
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
To disable a cop for specific files:
|
|
273
|
+
|
|
274
|
+
```yaml
|
|
275
|
+
LightServices/ArgumentTypeRequired:
|
|
276
|
+
Exclude:
|
|
277
|
+
- 'spec/**/*'
|
|
278
|
+
- 'test/**/*'
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## What's Next?
|
|
282
|
+
|
|
283
|
+
Learn more about testing your services:
|
|
284
|
+
|
|
285
|
+
[Next: Testing](testing.md)
|
data/docs/ruby-lsp.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Ruby LSP Integration
|
|
2
|
+
|
|
3
|
+
Light Services provides a Ruby LSP add-on that enhances your editor experience by informing the language server about methods generated by the `arg` and `output` DSL keywords.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
When you use the `arg` or `output` keywords, Light Services dynamically generates methods at runtime:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
class MyService < ApplicationService
|
|
11
|
+
arg :user, type: User
|
|
12
|
+
output :result, type: Hash
|
|
13
|
+
end
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
This generates the following methods:
|
|
17
|
+
- `user` - getter method (returns `User`)
|
|
18
|
+
- `user?` - predicate method (returns boolean)
|
|
19
|
+
- `user=` - setter method (private, accepts `User`)
|
|
20
|
+
- `result` - getter method (returns `Hash`)
|
|
21
|
+
- `result?` - predicate method (returns boolean)
|
|
22
|
+
- `result=` - setter method (private, accepts `Hash`)
|
|
23
|
+
|
|
24
|
+
The Ruby LSP add-on teaches the language server about these generated methods, enabling:
|
|
25
|
+
|
|
26
|
+
- **Go to Definition** - Navigate to the `arg`/`output` declaration
|
|
27
|
+
- **Completion** - Autocomplete generated method names
|
|
28
|
+
- **Hover** - See information about generated methods, including return types
|
|
29
|
+
- **Signature Help** - Get parameter hints for setter methods
|
|
30
|
+
- **Workspace Symbol** - Find generated methods in symbol search
|
|
31
|
+
|
|
32
|
+
## Setup
|
|
33
|
+
|
|
34
|
+
The add-on is automatically discovered by Ruby LSP when Light Services is in your project's dependencies. No additional configuration is required.
|
|
35
|
+
|
|
36
|
+
### Requirements
|
|
37
|
+
|
|
38
|
+
- Ruby LSP `~> 0.26` or later
|
|
39
|
+
- Light Services gem installed in your project
|
|
40
|
+
|
|
41
|
+
### Verification
|
|
42
|
+
|
|
43
|
+
To verify the add-on is loaded, check the Ruby LSP output in your editor. You should see "Ruby LSP Light Services" listed among the active add-ons.
|
|
44
|
+
|
|
45
|
+
## How It Works
|
|
46
|
+
|
|
47
|
+
The add-on uses Ruby LSP's **indexing enhancement** system to register generated methods during code indexing. When the indexer encounters an `arg` or `output` call with a symbol argument, it automatically registers the three generated methods (getter, predicate, setter) in the index.
|
|
48
|
+
|
|
49
|
+
This is a static analysis approach - the add-on analyzes your source code without executing it. This means:
|
|
50
|
+
|
|
51
|
+
- Methods are recognized immediately as you type
|
|
52
|
+
- No running application is required
|
|
53
|
+
- Works with any editor that supports Ruby LSP
|
|
54
|
+
|
|
55
|
+
## Type Inference
|
|
56
|
+
|
|
57
|
+
The add-on extracts type information from the `type:` option and includes it as YARD-style documentation comments. This enables hover information to display return types for generated methods.
|
|
58
|
+
|
|
59
|
+
### Simple Ruby Types
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
arg :user, type: User # → User
|
|
63
|
+
arg :items, type: Array # → Array
|
|
64
|
+
arg :name, type: String # → String
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Namespaced Types
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
arg :payment, type: Stripe::Charge # → Stripe::Charge
|
|
71
|
+
arg :config, type: MyApp::Configuration # → MyApp::Configuration
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Dry-Types
|
|
75
|
+
|
|
76
|
+
Common dry-types are mapped to their underlying Ruby types:
|
|
77
|
+
|
|
78
|
+
| Dry-Type | Ruby Type |
|
|
79
|
+
|----------|-----------|
|
|
80
|
+
| `Types::String`, `Types::Strict::String`, `Types::Coercible::String` | `String` |
|
|
81
|
+
| `Types::Integer`, `Types::Strict::Integer`, `Types::Coercible::Integer` | `Integer` |
|
|
82
|
+
| `Types::Float`, `Types::Strict::Float`, `Types::Coercible::Float` | `Float` |
|
|
83
|
+
| `Types::Bool`, `Types::Strict::Bool` | `TrueClass \| FalseClass` |
|
|
84
|
+
| `Types::Array`, `Types::Strict::Array` | `Array` |
|
|
85
|
+
| `Types::Hash`, `Types::Strict::Hash` | `Hash` |
|
|
86
|
+
| `Types::Symbol`, `Types::Strict::Symbol` | `Symbol` |
|
|
87
|
+
| `Types::Date`, `Types::DateTime`, `Types::Time` | `Date`, `DateTime`, `Time` |
|
|
88
|
+
|
|
89
|
+
Constrained and parameterized types extract their base type:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
arg :email, type: Types::String.constrained(format: /@/) # → String
|
|
93
|
+
arg :tags, type: Types::Array.of(Types::String) # → Array
|
|
94
|
+
arg :status, type: Types::String.enum("active", "pending") # → String
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Custom Type Mappings
|
|
98
|
+
|
|
99
|
+
You can add custom type mappings through the Light Services configuration:
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
# config/initializers/light_services.rb
|
|
103
|
+
Light::Services.configure do |config|
|
|
104
|
+
config.ruby_lsp_type_mappings = {
|
|
105
|
+
"Types::UUID" => "String",
|
|
106
|
+
"Types::Money" => "BigDecimal",
|
|
107
|
+
"Types::JSON" => "Hash",
|
|
108
|
+
"CustomTypes::Email" => "String",
|
|
109
|
+
"MyApp::Types::PhoneNumber" => "String",
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Custom mappings take precedence over the default dry-types mappings, allowing you to:
|
|
115
|
+
|
|
116
|
+
- Add mappings for your own custom types
|
|
117
|
+
- Override default mappings if needed
|
|
118
|
+
- Support domain-specific type modules
|
|
119
|
+
|
|
120
|
+
## Limitations
|
|
121
|
+
|
|
122
|
+
- Only `arg` and `output` declarations with a symbol as the first argument are recognized
|
|
123
|
+
- The add-on cannot detect dynamically computed argument names (e.g., `arg some_variable`)
|
|
124
|
+
- Inherited arguments/outputs from parent classes are not automatically discovered
|
|
125
|
+
- Parameterized dry-types like `Types::Array.of(Types::String)` resolve to the container type (`Array`), not the full generic type
|
|
126
|
+
- Custom dry-type definitions outside the standard `Types::` namespace are not mapped
|
|
127
|
+
|
|
128
|
+
## What's Next?
|
|
129
|
+
|
|
130
|
+
Learn more about other integrations:
|
|
131
|
+
|
|
132
|
+
- [RuboCop Integration](rubocop.md) - Static analysis cops for services
|
|
133
|
+
- [Testing](testing.md) - Testing your services with RSpec matchers
|
data/docs/steps.md
CHANGED
|
@@ -187,9 +187,9 @@ class ParsePage < ApplicationService
|
|
|
187
187
|
end
|
|
188
188
|
```
|
|
189
189
|
|
|
190
|
-
## Early Exit with `
|
|
190
|
+
## Early Exit with `stop!`
|
|
191
191
|
|
|
192
|
-
Use `
|
|
192
|
+
Use `stop!` to stop executing remaining steps without adding an error. This is useful when you've completed the service's goal early and don't need to run subsequent steps.
|
|
193
193
|
|
|
194
194
|
```ruby
|
|
195
195
|
class User::FindOrCreate < ApplicationService
|
|
@@ -205,7 +205,7 @@ class User::FindOrCreate < ApplicationService
|
|
|
205
205
|
|
|
206
206
|
def find_existing_user
|
|
207
207
|
self.user = User.find_by(email:)
|
|
208
|
-
|
|
208
|
+
stop! if user # Skip remaining steps if user already exists
|
|
209
209
|
end
|
|
210
210
|
|
|
211
211
|
def create_user
|
|
@@ -219,23 +219,77 @@ class User::FindOrCreate < ApplicationService
|
|
|
219
219
|
end
|
|
220
220
|
```
|
|
221
221
|
|
|
222
|
-
You can check if `
|
|
222
|
+
You can check if `stop!` was called using `stopped?`:
|
|
223
223
|
|
|
224
224
|
```ruby
|
|
225
225
|
def some_step
|
|
226
|
-
|
|
226
|
+
stop!
|
|
227
227
|
|
|
228
228
|
# This code still runs within the same step
|
|
229
|
-
puts "
|
|
229
|
+
puts "Stopped? #{stopped?}" # => "Stopped? true"
|
|
230
230
|
end
|
|
231
231
|
|
|
232
232
|
def next_step
|
|
233
|
-
# This step will NOT run because
|
|
233
|
+
# This step will NOT run because stop! was called
|
|
234
234
|
end
|
|
235
235
|
```
|
|
236
236
|
|
|
237
237
|
{% hint style="info" %}
|
|
238
|
-
`
|
|
238
|
+
`stop!` stops subsequent steps from running, including steps marked with `always: true`. Code after `stop!` within the same step method will still execute.
|
|
239
|
+
{% endhint %}
|
|
240
|
+
|
|
241
|
+
{% hint style="success" %}
|
|
242
|
+
**Database Transactions:** Calling `stop!` does NOT rollback database transactions. All database changes made before `stop!` was called will be committed.
|
|
243
|
+
{% endhint %}
|
|
244
|
+
|
|
245
|
+
{% hint style="info" %}
|
|
246
|
+
**Backward Compatibility:** `done!` and `done?` are still available as aliases for `stop!` and `stopped?`.
|
|
247
|
+
{% endhint %}
|
|
248
|
+
|
|
249
|
+
## Immediate Exit with `stop_immediately!`
|
|
250
|
+
|
|
251
|
+
Use `stop_immediately!` when you need to halt execution immediately, even within the current step. Unlike `stop!`, code after `stop_immediately!` in the same step method will NOT execute.
|
|
252
|
+
|
|
253
|
+
```ruby
|
|
254
|
+
class Payment::Process < ApplicationService
|
|
255
|
+
arg :amount, type: Integer
|
|
256
|
+
arg :card_token, type: String
|
|
257
|
+
|
|
258
|
+
step :validate_card
|
|
259
|
+
step :charge_card
|
|
260
|
+
step :send_receipt
|
|
261
|
+
|
|
262
|
+
output :transaction_id, type: String
|
|
263
|
+
|
|
264
|
+
private
|
|
265
|
+
|
|
266
|
+
def validate_card
|
|
267
|
+
unless valid_card?(card_token)
|
|
268
|
+
errors.add(:card, "is invalid")
|
|
269
|
+
stop_immediately! # Exit immediately - don't run any more code
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# This code won't run if card is invalid
|
|
273
|
+
log_validation_success
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def charge_card
|
|
277
|
+
# This step won't run if stop_immediately! was called
|
|
278
|
+
self.transaction_id = PaymentGateway.charge(amount, card_token)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def send_receipt
|
|
282
|
+
Mailer.receipt(transaction_id).deliver_later
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
{% hint style="warning" %}
|
|
288
|
+
`stop_immediately!` raises an internal exception to halt execution. Steps marked with `always: true` will NOT run when `stop_immediately!` is called.
|
|
289
|
+
{% endhint %}
|
|
290
|
+
|
|
291
|
+
{% hint style="success" %}
|
|
292
|
+
**Database Transactions:** Calling `stop_immediately!` does NOT rollback database transactions. All database changes made before `stop_immediately!` was called will be committed.
|
|
239
293
|
{% endhint %}
|
|
240
294
|
|
|
241
295
|
## Removing Inherited Steps
|
data/docs/testing.md
CHANGED
data/lib/light/services/base.rb
CHANGED
|
@@ -21,7 +21,29 @@ require "light/services/concerns/parent_service"
|
|
|
21
21
|
# Base class for all service objects
|
|
22
22
|
module Light
|
|
23
23
|
module Services
|
|
24
|
+
# Base class for building service objects with arguments, outputs, and steps.
|
|
25
|
+
#
|
|
26
|
+
# @example Basic service
|
|
27
|
+
# class CreateUser < Light::Services::Base
|
|
28
|
+
# arg :name, type: String
|
|
29
|
+
# arg :email, type: String
|
|
30
|
+
#
|
|
31
|
+
# output :user, type: User
|
|
32
|
+
#
|
|
33
|
+
# step :create_user
|
|
34
|
+
#
|
|
35
|
+
# private
|
|
36
|
+
#
|
|
37
|
+
# def create_user
|
|
38
|
+
# self.user = User.create!(name: name, email: email)
|
|
39
|
+
# end
|
|
40
|
+
# end
|
|
41
|
+
#
|
|
42
|
+
# result = CreateUser.run(name: "John", email: "john@example.com")
|
|
43
|
+
# result.success? # => true
|
|
44
|
+
# result.user # => #<User id: 1, name: "John">
|
|
24
45
|
class Base
|
|
46
|
+
extend CallbackDsl
|
|
25
47
|
include Callbacks
|
|
26
48
|
include Dsl::ArgumentsDsl
|
|
27
49
|
include Dsl::OutputsDsl
|
|
@@ -30,9 +52,23 @@ module Light
|
|
|
30
52
|
include Concerns::StateManagement
|
|
31
53
|
include Concerns::ParentService
|
|
32
54
|
|
|
33
|
-
#
|
|
34
|
-
attr_reader :outputs
|
|
55
|
+
# @return [Collection::Base] collection of output values
|
|
56
|
+
attr_reader :outputs
|
|
35
57
|
|
|
58
|
+
# @return [Collection::Base] collection of argument values
|
|
59
|
+
attr_reader :arguments
|
|
60
|
+
|
|
61
|
+
# @return [Messages] collection of error messages
|
|
62
|
+
attr_reader :errors
|
|
63
|
+
|
|
64
|
+
# @return [Messages] collection of warning messages
|
|
65
|
+
attr_reader :warnings
|
|
66
|
+
|
|
67
|
+
# Initialize a new service instance.
|
|
68
|
+
#
|
|
69
|
+
# @param args [Hash] arguments to pass to the service
|
|
70
|
+
# @param config [Hash] runtime configuration overrides
|
|
71
|
+
# @param parent_service [Base, nil] parent service for nested calls
|
|
36
72
|
def initialize(args = {}, config = {}, parent_service = nil)
|
|
37
73
|
@config = Light::Services.config.merge(self.class.class_config || {}).merge(config)
|
|
38
74
|
@parent_service = parent_service
|
|
@@ -40,37 +76,70 @@ module Light
|
|
|
40
76
|
@outputs = Collection::Base.new(self, CollectionTypes::OUTPUTS)
|
|
41
77
|
@arguments = Collection::Base.new(self, CollectionTypes::ARGUMENTS, args.dup)
|
|
42
78
|
|
|
43
|
-
@
|
|
79
|
+
@stopped = false
|
|
44
80
|
@launched_steps = []
|
|
45
81
|
|
|
46
82
|
initialize_errors
|
|
47
83
|
initialize_warnings
|
|
48
84
|
end
|
|
49
85
|
|
|
86
|
+
# Check if the service completed without errors.
|
|
87
|
+
#
|
|
88
|
+
# @return [Boolean] true if no errors were added
|
|
50
89
|
def success?
|
|
51
90
|
!errors?
|
|
52
91
|
end
|
|
53
92
|
|
|
93
|
+
# Check if the service completed with errors.
|
|
94
|
+
#
|
|
95
|
+
# @return [Boolean] true if any errors were added
|
|
54
96
|
def failed?
|
|
55
97
|
errors?
|
|
56
98
|
end
|
|
57
99
|
|
|
100
|
+
# Check if the service has any errors.
|
|
101
|
+
#
|
|
102
|
+
# @return [Boolean] true if errors collection is not empty
|
|
58
103
|
def errors?
|
|
59
104
|
@errors.any?
|
|
60
105
|
end
|
|
61
106
|
|
|
107
|
+
# Check if the service has any warnings.
|
|
108
|
+
#
|
|
109
|
+
# @return [Boolean] true if warnings collection is not empty
|
|
62
110
|
def warnings?
|
|
63
111
|
@warnings.any?
|
|
64
112
|
end
|
|
65
113
|
|
|
66
|
-
|
|
67
|
-
|
|
114
|
+
# Stop executing remaining steps after the current step completes.
|
|
115
|
+
#
|
|
116
|
+
# @return [Boolean] true
|
|
117
|
+
def stop!
|
|
118
|
+
@stopped = true
|
|
68
119
|
end
|
|
120
|
+
alias done! stop!
|
|
69
121
|
|
|
70
|
-
|
|
71
|
-
|
|
122
|
+
# Check if the service has been stopped.
|
|
123
|
+
#
|
|
124
|
+
# @return [Boolean] true if stop! was called
|
|
125
|
+
def stopped?
|
|
126
|
+
@stopped
|
|
127
|
+
end
|
|
128
|
+
alias done? stopped?
|
|
129
|
+
|
|
130
|
+
# Stop execution immediately, skipping any remaining code in the current step.
|
|
131
|
+
#
|
|
132
|
+
# @raise [StopExecution] always raises to halt execution
|
|
133
|
+
# @return [void]
|
|
134
|
+
def stop_immediately!
|
|
135
|
+
@stopped = true
|
|
136
|
+
raise Light::Services::StopExecution
|
|
72
137
|
end
|
|
73
138
|
|
|
139
|
+
# Execute the service steps.
|
|
140
|
+
#
|
|
141
|
+
# @return [void]
|
|
142
|
+
# @raise [StandardError] re-raises any exception after running always steps
|
|
74
143
|
def call
|
|
75
144
|
load_defaults_and_validate
|
|
76
145
|
|
|
@@ -87,20 +156,54 @@ module Light
|
|
|
87
156
|
end
|
|
88
157
|
|
|
89
158
|
class << self
|
|
159
|
+
# @return [Hash, nil] class-level configuration options
|
|
90
160
|
attr_accessor :class_config
|
|
91
161
|
|
|
162
|
+
# Set class-level configuration for this service.
|
|
163
|
+
#
|
|
164
|
+
# @param config [Hash] configuration options
|
|
165
|
+
# @return [Hash] the configuration hash
|
|
92
166
|
def config(config = {})
|
|
93
167
|
self.class_config = config
|
|
94
168
|
end
|
|
95
169
|
|
|
170
|
+
# Run the service and return the result.
|
|
171
|
+
#
|
|
172
|
+
# @param args [Hash] arguments to pass to the service
|
|
173
|
+
# @param config [Hash] runtime configuration overrides
|
|
174
|
+
# @return [Base] the executed service instance
|
|
175
|
+
#
|
|
176
|
+
# @example
|
|
177
|
+
# result = MyService.run(name: "test")
|
|
178
|
+
# result.success? # => true
|
|
96
179
|
def run(args = {}, config = {})
|
|
97
180
|
new(args, config).tap(&:call)
|
|
98
181
|
end
|
|
99
182
|
|
|
183
|
+
# Run the service and raise an error if it fails.
|
|
184
|
+
#
|
|
185
|
+
# @param args [Hash] arguments to pass to the service
|
|
186
|
+
# @param config [Hash] runtime configuration overrides
|
|
187
|
+
# @return [Base] the executed service instance
|
|
188
|
+
# @raise [Error] if the service fails
|
|
189
|
+
#
|
|
190
|
+
# @example
|
|
191
|
+
# MyService.run!(name: "test") # raises if service fails
|
|
100
192
|
def run!(args = {}, config = {})
|
|
101
193
|
run(args, config.merge(raise_on_error: true))
|
|
102
194
|
end
|
|
103
195
|
|
|
196
|
+
# Create a context for running the service with a parent service or config.
|
|
197
|
+
#
|
|
198
|
+
# @param service_or_config [Base, Hash] parent service or configuration hash
|
|
199
|
+
# @param config [Hash] configuration hash (when first param is a service)
|
|
200
|
+
# @return [BaseWithContext] context wrapper for running the service
|
|
201
|
+
#
|
|
202
|
+
# @example With parent service
|
|
203
|
+
# ChildService.with(self).run(data: value)
|
|
204
|
+
#
|
|
205
|
+
# @example With configuration
|
|
206
|
+
# MyService.with(use_transactions: false).run(name: "test")
|
|
104
207
|
def with(service_or_config = {}, config = {})
|
|
105
208
|
service = service_or_config.is_a?(Hash) ? nil : service_or_config
|
|
106
209
|
config = service_or_config unless service
|