light-services 2.2.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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 +77 -7
- data/CHANGELOG.md +23 -0
- data/CLAUDE.md +139 -0
- data/Gemfile +16 -11
- data/Gemfile.lock +53 -27
- data/README.md +76 -13
- data/docs/arguments.md +267 -0
- data/docs/best-practices.md +153 -0
- data/docs/callbacks.md +476 -0
- data/docs/concepts.md +80 -0
- data/docs/configuration.md +168 -0
- data/docs/context.md +128 -0
- data/docs/crud.md +525 -0
- data/docs/errors.md +250 -0
- data/docs/generators.md +250 -0
- data/docs/outputs.md +135 -0
- data/docs/pundit-authorization.md +320 -0
- data/docs/quickstart.md +134 -0
- data/docs/readme.md +100 -0
- data/docs/recipes.md +14 -0
- data/docs/service-rendering.md +222 -0
- data/docs/steps.md +337 -0
- data/docs/summary.md +19 -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 +23 -113
- data/lib/light/services/callbacks.rb +103 -0
- data/lib/light/services/collection.rb +97 -0
- data/lib/light/services/concerns/execution.rb +76 -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 +4 -18
- data/lib/light/services/constants.rb +97 -0
- data/lib/light/services/dsl/arguments_dsl.rb +84 -0
- data/lib/light/services/dsl/outputs_dsl.rb +80 -0
- data/lib/light/services/dsl/steps_dsl.rb +205 -0
- data/lib/light/services/dsl/validation.rb +132 -0
- data/lib/light/services/exceptions.rb +7 -2
- data/lib/light/services/messages.rb +19 -31
- 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/settings/field.rb +86 -0
- data/lib/light/services/settings/step.rb +31 -16
- data/lib/light/services/utils.rb +38 -0
- data/lib/light/services/version.rb +1 -1
- data/lib/light/services.rb +2 -0
- data/light-services.gemspec +6 -8
- metadata +54 -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
data/docs/callbacks.md
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
# Callbacks
|
|
2
|
+
|
|
3
|
+
Callbacks are hooks that allow you to run custom code at specific points during service and step execution. They're perfect for logging, benchmarking, auditing, and other cross-cutting concerns.
|
|
4
|
+
|
|
5
|
+
## TL;DR
|
|
6
|
+
|
|
7
|
+
- Define callbacks using DSL methods like `before_service_run`, `after_step_run`, etc.
|
|
8
|
+
- Use symbols (method names) or procs/lambdas
|
|
9
|
+
- Callbacks are inherited from parent classes
|
|
10
|
+
- Around callbacks wrap execution and must yield
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
class User::Charge < ApplicationService
|
|
14
|
+
before_service_run :log_start
|
|
15
|
+
after_service_run :log_end
|
|
16
|
+
on_service_failure :notify_admin
|
|
17
|
+
|
|
18
|
+
around_step_run :benchmark_step
|
|
19
|
+
|
|
20
|
+
step :authorize
|
|
21
|
+
step :charge
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def log_start(service)
|
|
26
|
+
Rails.logger.info "Starting #{service.class.name}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def log_end(service)
|
|
30
|
+
Rails.logger.info "Finished #{service.class.name}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def notify_admin(service)
|
|
34
|
+
AdminMailer.service_failed(service).deliver_later
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def benchmark_step(service, step_name)
|
|
38
|
+
start = Time.current
|
|
39
|
+
yield
|
|
40
|
+
duration = Time.current - start
|
|
41
|
+
Rails.logger.info "Step #{step_name} took #{duration}s"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Available Callbacks
|
|
47
|
+
|
|
48
|
+
### Service Callbacks
|
|
49
|
+
|
|
50
|
+
| Callback | When it runs | Arguments |
|
|
51
|
+
|----------|--------------|-----------|
|
|
52
|
+
| `before_service_run` | Before the service starts executing steps | `(service)` |
|
|
53
|
+
| `after_service_run` | After the service completes (success or failure) | `(service)` |
|
|
54
|
+
| `around_service_run` | Wraps the entire service execution | `(service, &block)` |
|
|
55
|
+
| `on_service_success` | After service completes without errors | `(service)` |
|
|
56
|
+
| `on_service_failure` | After service completes with errors | `(service)` |
|
|
57
|
+
|
|
58
|
+
### Step Callbacks
|
|
59
|
+
|
|
60
|
+
| Callback | When it runs | Arguments |
|
|
61
|
+
|----------|--------------|-----------|
|
|
62
|
+
| `before_step_run` | Before each step executes | `(service, step_name)` |
|
|
63
|
+
| `after_step_run` | After each step completes (success or failure) | `(service, step_name)` |
|
|
64
|
+
| `around_step_run` | Wraps each step execution | `(service, step_name, &block)` |
|
|
65
|
+
| `on_step_success` | After step completes without errors | `(service, step_name)` |
|
|
66
|
+
| `on_step_failure` | When step produces errors | `(service, step_name)` |
|
|
67
|
+
| `on_step_crash` | When step raises an exception | `(service, step_name, exception)` |
|
|
68
|
+
|
|
69
|
+
{% hint style="info" %}
|
|
70
|
+
Note the difference between `on_step_failure` and `on_step_crash`:
|
|
71
|
+
- `on_step_failure` is called when a step adds errors (similar to `on_service_failure`)
|
|
72
|
+
- `on_step_crash` is called when a step raises an exception
|
|
73
|
+
|
|
74
|
+
When a step crashes (raises an exception), `after_step_run` is NOT called.
|
|
75
|
+
{% endhint %}
|
|
76
|
+
|
|
77
|
+
## Defining Callbacks
|
|
78
|
+
|
|
79
|
+
### Using Symbols (Method Names)
|
|
80
|
+
|
|
81
|
+
The most common way to define callbacks is using symbols that reference instance methods:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
class Order::Process < ApplicationService
|
|
85
|
+
before_service_run :log_start
|
|
86
|
+
after_service_run :log_end
|
|
87
|
+
|
|
88
|
+
step :validate
|
|
89
|
+
step :process
|
|
90
|
+
step :notify
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def log_start(service)
|
|
95
|
+
Rails.logger.info "Processing order started"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def log_end(service)
|
|
99
|
+
Rails.logger.info "Processing order finished, success: #{service.success?}"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Using Procs/Lambdas
|
|
105
|
+
|
|
106
|
+
For simple callbacks, you can use inline procs:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
class Order::Process < ApplicationService
|
|
110
|
+
before_service_run do |service|
|
|
111
|
+
Rails.logger.info "Starting #{service.class.name}"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
after_service_run do |service|
|
|
115
|
+
Rails.logger.info "Completed with #{service.errors.count} errors"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
on_step_failure do |service, step_name|
|
|
119
|
+
Rails.logger.warn "Step #{step_name} produced errors"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
on_step_crash do |service, step_name, exception|
|
|
123
|
+
Bugsnag.notify(exception, step: step_name)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
step :validate
|
|
127
|
+
step :process
|
|
128
|
+
end
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Around Callbacks
|
|
132
|
+
|
|
133
|
+
Around callbacks wrap execution and must call `yield` to continue:
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
class Order::Process < ApplicationService
|
|
137
|
+
around_service_run :with_logging
|
|
138
|
+
around_step_run :with_timing
|
|
139
|
+
|
|
140
|
+
step :validate
|
|
141
|
+
step :process
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def with_logging(service)
|
|
146
|
+
Rails.logger.info "=== Starting #{service.class.name} ==="
|
|
147
|
+
yield
|
|
148
|
+
Rails.logger.info "=== Finished #{service.class.name} ==="
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def with_timing(service, step_name)
|
|
152
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
153
|
+
yield
|
|
154
|
+
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
|
|
155
|
+
Rails.logger.info "Step :#{step_name} completed in #{duration.round(3)}s"
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Around Callbacks with Procs
|
|
161
|
+
|
|
162
|
+
When using procs for around callbacks, the block is passed as the last argument:
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
class Order::Process < ApplicationService
|
|
166
|
+
around_service_run do |service, block|
|
|
167
|
+
Rails.logger.info "Starting..."
|
|
168
|
+
block.call
|
|
169
|
+
Rails.logger.info "Finished!"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
around_step_run do |service, step_name, block|
|
|
173
|
+
Rails.logger.info "Running step :#{step_name}"
|
|
174
|
+
block.call
|
|
175
|
+
Rails.logger.info "Completed step :#{step_name}"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
step :process
|
|
179
|
+
end
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
{% hint style="warning" %}
|
|
183
|
+
Forgetting to call `yield` (or `block.call` for procs) in around callbacks will prevent the service/step from executing!
|
|
184
|
+
{% endhint %}
|
|
185
|
+
|
|
186
|
+
## Multiple Callbacks
|
|
187
|
+
|
|
188
|
+
You can define multiple callbacks of the same type. They execute in the order they're defined:
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
class Order::Process < ApplicationService
|
|
192
|
+
before_service_run :log_start
|
|
193
|
+
before_service_run :validate_environment
|
|
194
|
+
before_service_run :check_permissions
|
|
195
|
+
|
|
196
|
+
step :process
|
|
197
|
+
|
|
198
|
+
private
|
|
199
|
+
|
|
200
|
+
def log_start(service)
|
|
201
|
+
# Runs first
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def validate_environment(service)
|
|
205
|
+
# Runs second
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def check_permissions(service)
|
|
209
|
+
# Runs third
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Multiple Around Callbacks
|
|
215
|
+
|
|
216
|
+
Multiple around callbacks are nested, with the first one wrapping the second:
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
class Order::Process < ApplicationService
|
|
220
|
+
around_service_run :outer_wrapper
|
|
221
|
+
around_service_run :inner_wrapper
|
|
222
|
+
|
|
223
|
+
step :process
|
|
224
|
+
|
|
225
|
+
private
|
|
226
|
+
|
|
227
|
+
def outer_wrapper(service)
|
|
228
|
+
puts "outer before"
|
|
229
|
+
yield
|
|
230
|
+
puts "outer after"
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def inner_wrapper(service)
|
|
234
|
+
puts "inner before"
|
|
235
|
+
yield
|
|
236
|
+
puts "inner after"
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Output:
|
|
240
|
+
# outer before
|
|
241
|
+
# inner before
|
|
242
|
+
# (service executes)
|
|
243
|
+
# inner after
|
|
244
|
+
# outer after
|
|
245
|
+
end
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Callback Inheritance
|
|
249
|
+
|
|
250
|
+
Callbacks are inherited from parent classes. Child class callbacks run after parent callbacks:
|
|
251
|
+
|
|
252
|
+
```ruby
|
|
253
|
+
class ApplicationService < Light::Services::Base
|
|
254
|
+
before_service_run :log_service_start
|
|
255
|
+
|
|
256
|
+
private
|
|
257
|
+
|
|
258
|
+
def log_service_start(service)
|
|
259
|
+
Rails.logger.info "[#{service.class.name}] Starting"
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
class Order::Process < ApplicationService
|
|
264
|
+
before_service_run :validate_order
|
|
265
|
+
|
|
266
|
+
step :process
|
|
267
|
+
|
|
268
|
+
private
|
|
269
|
+
|
|
270
|
+
def validate_order(service)
|
|
271
|
+
# Runs after log_service_start
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Deep Inheritance
|
|
277
|
+
|
|
278
|
+
Callbacks accumulate through the inheritance chain:
|
|
279
|
+
|
|
280
|
+
```ruby
|
|
281
|
+
class BaseService < Light::Services::Base
|
|
282
|
+
before_service_run :base_callback
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
class MiddleService < BaseService
|
|
286
|
+
before_service_run :middle_callback
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
class ConcreteService < MiddleService
|
|
290
|
+
before_service_run :concrete_callback
|
|
291
|
+
|
|
292
|
+
# Execution order:
|
|
293
|
+
# 1. base_callback
|
|
294
|
+
# 2. middle_callback
|
|
295
|
+
# 3. concrete_callback
|
|
296
|
+
end
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Execution Order
|
|
300
|
+
|
|
301
|
+
### Service Callbacks Order
|
|
302
|
+
|
|
303
|
+
```
|
|
304
|
+
before_service_run
|
|
305
|
+
└── around_service_run (before yield)
|
|
306
|
+
└── [steps execute]
|
|
307
|
+
around_service_run (after yield)
|
|
308
|
+
after_service_run
|
|
309
|
+
on_service_success OR on_service_failure
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Step Callbacks Order (for each step)
|
|
313
|
+
|
|
314
|
+
**Normal execution (no exception):**
|
|
315
|
+
```
|
|
316
|
+
before_step_run
|
|
317
|
+
└── around_step_run (before yield)
|
|
318
|
+
└── [step executes]
|
|
319
|
+
around_step_run (after yield)
|
|
320
|
+
after_step_run
|
|
321
|
+
on_step_success OR on_step_failure (if errors were added)
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
**Exception during step:**
|
|
325
|
+
```
|
|
326
|
+
before_step_run
|
|
327
|
+
└── around_step_run (before yield)
|
|
328
|
+
└── [step raises exception]
|
|
329
|
+
on_step_crash
|
|
330
|
+
[exception propagates]
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## Use Cases
|
|
334
|
+
|
|
335
|
+
### Logging
|
|
336
|
+
|
|
337
|
+
```ruby
|
|
338
|
+
class ApplicationService < Light::Services::Base
|
|
339
|
+
before_service_run :log_start
|
|
340
|
+
after_service_run :log_finish
|
|
341
|
+
|
|
342
|
+
private
|
|
343
|
+
|
|
344
|
+
def log_start(service)
|
|
345
|
+
Rails.logger.tagged(service.class.name) do
|
|
346
|
+
Rails.logger.info "Started with arguments: #{service.arguments.to_h}"
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def log_finish(service)
|
|
351
|
+
Rails.logger.tagged(service.class.name) do
|
|
352
|
+
if service.success?
|
|
353
|
+
Rails.logger.info "Completed successfully"
|
|
354
|
+
else
|
|
355
|
+
Rails.logger.warn "Failed with errors: #{service.errors.full_messages}"
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Benchmarking
|
|
363
|
+
|
|
364
|
+
```ruby
|
|
365
|
+
class ApplicationService < Light::Services::Base
|
|
366
|
+
around_service_run :benchmark
|
|
367
|
+
|
|
368
|
+
private
|
|
369
|
+
|
|
370
|
+
def benchmark(service)
|
|
371
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
372
|
+
yield
|
|
373
|
+
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
|
|
374
|
+
|
|
375
|
+
if duration > 1.0
|
|
376
|
+
Rails.logger.warn "#{service.class.name} took #{duration.round(2)}s"
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Error Tracking
|
|
383
|
+
|
|
384
|
+
```ruby
|
|
385
|
+
class ApplicationService < Light::Services::Base
|
|
386
|
+
on_service_failure :track_failure
|
|
387
|
+
on_step_failure :track_step_error
|
|
388
|
+
on_step_crash :track_step_crash
|
|
389
|
+
|
|
390
|
+
private
|
|
391
|
+
|
|
392
|
+
def track_failure(service)
|
|
393
|
+
Bugsnag.notify("Service failed") do |report|
|
|
394
|
+
report.add_metadata(:service, {
|
|
395
|
+
class: service.class.name,
|
|
396
|
+
errors: service.errors.full_messages,
|
|
397
|
+
arguments: service.arguments.to_h
|
|
398
|
+
})
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def track_step_error(service, step_name)
|
|
403
|
+
Rails.logger.warn "Step :#{step_name} produced errors in #{service.class.name}"
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def track_step_crash(service, step_name, exception)
|
|
407
|
+
Bugsnag.notify(exception) do |report|
|
|
408
|
+
report.add_metadata(:service, {
|
|
409
|
+
class: service.class.name,
|
|
410
|
+
step: step_name
|
|
411
|
+
})
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### Audit Trail
|
|
418
|
+
|
|
419
|
+
```ruby
|
|
420
|
+
class Order::Process < ApplicationService
|
|
421
|
+
after_service_run :create_audit_log
|
|
422
|
+
|
|
423
|
+
arg :order, type: Order
|
|
424
|
+
arg :current_user, type: User
|
|
425
|
+
|
|
426
|
+
step :process
|
|
427
|
+
|
|
428
|
+
private
|
|
429
|
+
|
|
430
|
+
def create_audit_log(service)
|
|
431
|
+
AuditLog.create!(
|
|
432
|
+
user: current_user,
|
|
433
|
+
action: "order.process",
|
|
434
|
+
resource: order,
|
|
435
|
+
success: service.success?,
|
|
436
|
+
metadata: {
|
|
437
|
+
errors: service.errors.full_messages
|
|
438
|
+
}
|
|
439
|
+
)
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### Database Instrumentation
|
|
445
|
+
|
|
446
|
+
```ruby
|
|
447
|
+
class ApplicationService < Light::Services::Base
|
|
448
|
+
around_step_run :track_queries
|
|
449
|
+
|
|
450
|
+
private
|
|
451
|
+
|
|
452
|
+
def track_queries(service, step_name)
|
|
453
|
+
query_count = 0
|
|
454
|
+
|
|
455
|
+
subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do
|
|
456
|
+
query_count += 1
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
yield
|
|
460
|
+
|
|
461
|
+
ActiveSupport::Notifications.unsubscribe(subscriber)
|
|
462
|
+
|
|
463
|
+
if query_count > 10
|
|
464
|
+
Rails.logger.warn "Step :#{step_name} executed #{query_count} queries"
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
## What's Next?
|
|
471
|
+
|
|
472
|
+
Learn about testing your services, including how to test callbacks.
|
|
473
|
+
|
|
474
|
+
[Next: Testing](testing.md)
|
|
475
|
+
|
|
476
|
+
|
data/docs/concepts.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Concepts
|
|
2
|
+
|
|
3
|
+
This section covers the core concepts of Light Services: **Arguments**, **Steps**, **Outputs**, **Context**, **Errors**, and **Callbacks**.
|
|
4
|
+
|
|
5
|
+
## Service Execution Flow
|
|
6
|
+
|
|
7
|
+
When you call `MyService.run(args)`, the following happens:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
11
|
+
│ Service.run(args) │
|
|
12
|
+
├─────────────────────────────────────────────────────────────┤
|
|
13
|
+
│ 1. Load default values for arguments and outputs │
|
|
14
|
+
│ 2. Validate argument types │
|
|
15
|
+
│ 3. Run before_service_run callbacks │
|
|
16
|
+
├─────────────────────────────────────────────────────────────┤
|
|
17
|
+
│ 4. Begin around_service_run callback │
|
|
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 done! was called │ │
|
|
27
|
+
│ ├─────────────────────────────────────────────────────┤ │
|
|
28
|
+
│ │ 7. On error → Rollback transaction │ │
|
|
29
|
+
│ │ On success → Commit transaction │ │
|
|
30
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
31
|
+
│ 8. End around_service_run callback │
|
|
32
|
+
├─────────────────────────────────────────────────────────────┤
|
|
33
|
+
│ 9. Run steps marked with always: true (unless done! called) │
|
|
34
|
+
│ 10. Validate output types (if success) │
|
|
35
|
+
│ 11. Copy errors/warnings to parent service (if in context) │
|
|
36
|
+
│ 12. Run after_service_run callback │
|
|
37
|
+
│ 13. Run on_service_success or on_service_failure callback │
|
|
38
|
+
├─────────────────────────────────────────────────────────────┤
|
|
39
|
+
│ 14. Return service instance │
|
|
40
|
+
│ - service.success? / service.failed? │
|
|
41
|
+
│ - service.outputs / service.errors │
|
|
42
|
+
└─────────────────────────────────────────────────────────────┘
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Arguments
|
|
46
|
+
|
|
47
|
+
Arguments are the inputs provided to a service when it is invoked. They can be validated by type, assigned default values, and be designated as optional or required.
|
|
48
|
+
|
|
49
|
+
[Read more about arguments](arguments.md)
|
|
50
|
+
|
|
51
|
+
## Steps
|
|
52
|
+
|
|
53
|
+
Steps are the fundamental units of work within a service, representing each individual task a service performs. They can be executed conditionally or skipped.
|
|
54
|
+
|
|
55
|
+
[Read more about steps](steps.md)
|
|
56
|
+
|
|
57
|
+
## Outputs
|
|
58
|
+
|
|
59
|
+
Outputs are the results produced by a service upon its completion. They can have default values and be validated by type.
|
|
60
|
+
|
|
61
|
+
[Read more about outputs](outputs.md)
|
|
62
|
+
|
|
63
|
+
## Context
|
|
64
|
+
|
|
65
|
+
Context refers to the shared state that passes between services in a service chain, enabling the transfer of arguments and error states from one service to another.
|
|
66
|
+
|
|
67
|
+
[Read more about context](context.md)
|
|
68
|
+
|
|
69
|
+
## Errors
|
|
70
|
+
|
|
71
|
+
Errors occur during service execution and cause execution to halt. When an error occurs, all services in the same context chain stop, and database transactions are rolled back (configurable).
|
|
72
|
+
|
|
73
|
+
[Read more about errors](errors.md)
|
|
74
|
+
|
|
75
|
+
## Callbacks
|
|
76
|
+
|
|
77
|
+
Callbacks allow you to run custom code at specific points during service and step execution. They're perfect for logging, benchmarking, auditing, and other cross-cutting concerns.
|
|
78
|
+
|
|
79
|
+
[Read more about callbacks](callbacks.md)
|
|
80
|
+
|
|
@@ -0,0 +1,168 @@
|
|
|
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
|
+
# Transaction settings
|
|
12
|
+
config.use_transactions = true # Wrap each service in a database transaction
|
|
13
|
+
|
|
14
|
+
# Error behavior
|
|
15
|
+
config.load_errors = true # Copy errors to parent service in context chain
|
|
16
|
+
config.break_on_error = true # Stop step execution when an error is added
|
|
17
|
+
config.raise_on_error = false # Raise an exception when an error is added
|
|
18
|
+
config.rollback_on_error = true # Rollback transaction when an error is added
|
|
19
|
+
|
|
20
|
+
# Warning behavior
|
|
21
|
+
config.load_warnings = true # Copy warnings to parent service in context chain
|
|
22
|
+
config.break_on_warning = false # Stop step execution when a warning is added
|
|
23
|
+
config.raise_on_warning = false # Raise an exception when a warning is added
|
|
24
|
+
config.rollback_on_warning = false # Rollback transaction when a warning is added
|
|
25
|
+
end
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Default Values
|
|
29
|
+
|
|
30
|
+
| Option | Default | Description |
|
|
31
|
+
|--------|---------|-------------|
|
|
32
|
+
| `use_transactions` | `true` | Wraps service execution in `ActiveRecord::Base.transaction` |
|
|
33
|
+
| `load_errors` | `true` | Propagates errors to parent service when using `.with(self)` |
|
|
34
|
+
| `break_on_error` | `true` | Stops executing remaining steps when an error is added |
|
|
35
|
+
| `raise_on_error` | `false` | Raises `Light::Services::Error` when an error is added |
|
|
36
|
+
| `rollback_on_error` | `true` | Rolls back the transaction when an error is added |
|
|
37
|
+
| `load_warnings` | `true` | Propagates warnings to parent service when using `.with(self)` |
|
|
38
|
+
| `break_on_warning` | `false` | Stops executing remaining steps when a warning is added |
|
|
39
|
+
| `raise_on_warning` | `false` | Raises `Light::Services::Error` when a warning is added |
|
|
40
|
+
| `rollback_on_warning` | `false` | Rolls back the transaction when a warning is added |
|
|
41
|
+
|
|
42
|
+
## Per-Service Configuration
|
|
43
|
+
|
|
44
|
+
Override global configuration for a specific service class using the `config` class method:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
class CriticalPaymentService < ApplicationService
|
|
48
|
+
# This service will raise exceptions instead of collecting errors
|
|
49
|
+
config raise_on_error: true
|
|
50
|
+
|
|
51
|
+
step :process_payment
|
|
52
|
+
step :send_receipt
|
|
53
|
+
|
|
54
|
+
# ...
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
class NonCriticalNotificationService < ApplicationService
|
|
60
|
+
# This service doesn't need transactions and shouldn't stop on errors
|
|
61
|
+
config use_transactions: false, break_on_error: false
|
|
62
|
+
|
|
63
|
+
step :send_push_notification
|
|
64
|
+
step :send_email_notification
|
|
65
|
+
|
|
66
|
+
# ...
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Per-Call Configuration
|
|
71
|
+
|
|
72
|
+
Override configuration for a single service call:
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
# Pass config as second argument to run
|
|
76
|
+
MyService.run({ name: "John" }, { raise_on_error: true })
|
|
77
|
+
|
|
78
|
+
# Or use with() for context-based calls
|
|
79
|
+
MyService.with({ raise_on_error: true }).run(name: "John")
|
|
80
|
+
|
|
81
|
+
# Combine with parent service context
|
|
82
|
+
ChildService
|
|
83
|
+
.with(self, { use_transactions: false })
|
|
84
|
+
.run(data: some_data)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Configuration Precedence
|
|
88
|
+
|
|
89
|
+
Configuration is merged in this order (later overrides earlier):
|
|
90
|
+
|
|
91
|
+
1. Global configuration (from initializer)
|
|
92
|
+
2. Per-service configuration (from `config` class method)
|
|
93
|
+
3. Per-call configuration (from `run` or `with` arguments)
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
# Global: raise_on_error = false
|
|
97
|
+
Light::Services.configure do |config|
|
|
98
|
+
config.raise_on_error = false
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Per-service: raise_on_error = true (overrides global)
|
|
102
|
+
class MyService < ApplicationService
|
|
103
|
+
config raise_on_error: true
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Per-call: raise_on_error = false (overrides per-service)
|
|
107
|
+
MyService.run(args, { raise_on_error: false })
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Common Configuration Patterns
|
|
111
|
+
|
|
112
|
+
### Strict Mode for Critical Services
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
class Payment::Process < ApplicationService
|
|
116
|
+
config raise_on_error: true, rollback_on_error: true
|
|
117
|
+
|
|
118
|
+
# Any error will raise an exception and rollback the transaction
|
|
119
|
+
end
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Fire-and-Forget Services
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
class Analytics::Track < ApplicationService
|
|
126
|
+
config use_transactions: false, break_on_error: false, load_errors: false
|
|
127
|
+
|
|
128
|
+
# Errors won't stop execution or propagate to parent services
|
|
129
|
+
end
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Background Job Services
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
class BackgroundTaskService < ApplicationService
|
|
136
|
+
# Background jobs typically handle their own transactions
|
|
137
|
+
config use_transactions: false
|
|
138
|
+
end
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Disabling Transactions
|
|
142
|
+
|
|
143
|
+
If you're not using ActiveRecord or want to manage transactions yourself:
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
Light::Services.configure do |config|
|
|
147
|
+
config.use_transactions = false
|
|
148
|
+
end
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Or disable for specific services:
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
class MyService < ApplicationService
|
|
155
|
+
config use_transactions: false
|
|
156
|
+
end
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
{% hint style="info" %}
|
|
160
|
+
When `use_transactions` is `true`, Light Services uses `ActiveRecord::Base.transaction(requires_new: true)` to create savepoints, allowing nested services to rollback independently.
|
|
161
|
+
{% endhint %}
|
|
162
|
+
|
|
163
|
+
## What's Next?
|
|
164
|
+
|
|
165
|
+
Now that you understand configuration, learn about the core concepts:
|
|
166
|
+
|
|
167
|
+
[Next: Concepts](concepts.md)
|
|
168
|
+
|