light-services 2.2.1 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/config/rubocop_linter_action.yml +4 -4
- data/.github/workflows/ci.yml +12 -12
- data/.gitignore +1 -0
- data/.rubocop.yml +83 -7
- data/CHANGELOG.md +38 -0
- data/CLAUDE.md +139 -0
- data/Gemfile +16 -11
- data/Gemfile.lock +53 -27
- data/README.md +84 -21
- data/docs/arguments.md +290 -0
- data/docs/best-practices.md +153 -0
- data/docs/callbacks.md +476 -0
- data/docs/concepts.md +80 -0
- data/docs/configuration.md +204 -0
- data/docs/context.md +128 -0
- data/docs/crud.md +525 -0
- data/docs/errors.md +280 -0
- data/docs/generators.md +250 -0
- data/docs/outputs.md +158 -0
- data/docs/pundit-authorization.md +320 -0
- data/docs/quickstart.md +134 -0
- data/docs/readme.md +101 -0
- data/docs/recipes.md +14 -0
- data/docs/rubocop.md +285 -0
- data/docs/ruby-lsp.md +133 -0
- data/docs/service-rendering.md +222 -0
- data/docs/steps.md +391 -0
- data/docs/summary.md +21 -0
- data/docs/testing.md +549 -0
- data/lib/generators/light_services/install/USAGE +15 -0
- data/lib/generators/light_services/install/install_generator.rb +41 -0
- data/lib/generators/light_services/install/templates/application_service.rb.tt +8 -0
- data/lib/generators/light_services/install/templates/application_service_spec.rb.tt +7 -0
- data/lib/generators/light_services/install/templates/initializer.rb.tt +30 -0
- data/lib/generators/light_services/service/USAGE +21 -0
- data/lib/generators/light_services/service/service_generator.rb +68 -0
- data/lib/generators/light_services/service/templates/service.rb.tt +48 -0
- data/lib/generators/light_services/service/templates/service_spec.rb.tt +40 -0
- data/lib/light/services/base.rb +134 -122
- data/lib/light/services/base_with_context.rb +23 -1
- data/lib/light/services/callbacks.rb +157 -0
- data/lib/light/services/collection.rb +145 -0
- data/lib/light/services/concerns/execution.rb +79 -0
- data/lib/light/services/concerns/parent_service.rb +34 -0
- data/lib/light/services/concerns/state_management.rb +30 -0
- data/lib/light/services/config.rb +82 -16
- data/lib/light/services/constants.rb +100 -0
- data/lib/light/services/dsl/arguments_dsl.rb +85 -0
- data/lib/light/services/dsl/outputs_dsl.rb +81 -0
- data/lib/light/services/dsl/steps_dsl.rb +205 -0
- data/lib/light/services/dsl/validation.rb +162 -0
- data/lib/light/services/exceptions.rb +25 -2
- data/lib/light/services/message.rb +28 -3
- data/lib/light/services/messages.rb +92 -32
- data/lib/light/services/rspec/matchers/define_argument.rb +174 -0
- data/lib/light/services/rspec/matchers/define_output.rb +147 -0
- data/lib/light/services/rspec/matchers/define_step.rb +225 -0
- data/lib/light/services/rspec/matchers/execute_step.rb +230 -0
- data/lib/light/services/rspec/matchers/have_error_on.rb +148 -0
- data/lib/light/services/rspec/matchers/have_warning_on.rb +148 -0
- data/lib/light/services/rspec/matchers/trigger_callback.rb +138 -0
- data/lib/light/services/rspec.rb +15 -0
- data/lib/light/services/rubocop/cop/light_services/argument_type_required.rb +52 -0
- data/lib/light/services/rubocop/cop/light_services/condition_method_exists.rb +173 -0
- data/lib/light/services/rubocop/cop/light_services/deprecated_methods.rb +113 -0
- data/lib/light/services/rubocop/cop/light_services/dsl_order.rb +176 -0
- data/lib/light/services/rubocop/cop/light_services/missing_private_keyword.rb +102 -0
- data/lib/light/services/rubocop/cop/light_services/no_direct_instantiation.rb +66 -0
- data/lib/light/services/rubocop/cop/light_services/output_type_required.rb +52 -0
- data/lib/light/services/rubocop/cop/light_services/step_method_exists.rb +109 -0
- data/lib/light/services/rubocop.rb +12 -0
- data/lib/light/services/settings/field.rb +114 -0
- data/lib/light/services/settings/step.rb +53 -20
- data/lib/light/services/utils.rb +38 -0
- data/lib/light/services/version.rb +1 -1
- data/lib/light/services.rb +2 -0
- data/lib/ruby_lsp/light_services/addon.rb +36 -0
- data/lib/ruby_lsp/light_services/definition.rb +132 -0
- data/lib/ruby_lsp/light_services/indexing_enhancement.rb +263 -0
- data/light-services.gemspec +6 -8
- metadata +68 -26
- data/lib/light/services/class_based_collection/base.rb +0 -86
- data/lib/light/services/class_based_collection/mount.rb +0 -33
- data/lib/light/services/collection/arguments.rb +0 -34
- data/lib/light/services/collection/base.rb +0 -59
- data/lib/light/services/collection/outputs.rb +0 -16
- data/lib/light/services/settings/argument.rb +0 -68
- data/lib/light/services/settings/output.rb +0 -34
data/docs/steps.md
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
# Steps
|
|
2
|
+
|
|
3
|
+
Steps are the core components of a service, each representing a unit of work executed in sequence when the service is called.
|
|
4
|
+
|
|
5
|
+
## TL;DR
|
|
6
|
+
|
|
7
|
+
- Define steps using the `step` keyword within the service class
|
|
8
|
+
- Use `if` and `unless` options for conditional steps
|
|
9
|
+
- Inherit steps from parent classes
|
|
10
|
+
- Inject steps into the execution flow with `before` and `after` options
|
|
11
|
+
- Ensure cleanup steps run with the `always: true` option (unless `done!` was called)
|
|
12
|
+
- Use a `run` method as a simple alternative for single-step services
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
class GeneralParserService < ApplicationService
|
|
16
|
+
step :create_browser, unless: :browser
|
|
17
|
+
step :parse_content
|
|
18
|
+
step :quit_browser, always: true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class ParsePage < GeneralParserService
|
|
22
|
+
step :parse_additional_content, after: :parse_content
|
|
23
|
+
end
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Define Steps
|
|
27
|
+
|
|
28
|
+
Steps are declared using the `step` keyword in your service class.
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
class User::Charge < ApplicationService
|
|
32
|
+
step :authorize
|
|
33
|
+
step :charge
|
|
34
|
+
step :send_email_receipt
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def authorize
|
|
39
|
+
# ...
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def charge
|
|
43
|
+
# ...
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def send_email_receipt
|
|
47
|
+
# ...
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Conditional Steps
|
|
53
|
+
|
|
54
|
+
Steps can be conditional, executed based on specified conditions using the `if` or `unless` keywords.
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
class User::Charge < ApplicationService
|
|
58
|
+
step :authorize
|
|
59
|
+
step :charge
|
|
60
|
+
step :send_email_receipt, if: :send_receipt?
|
|
61
|
+
|
|
62
|
+
# ...
|
|
63
|
+
|
|
64
|
+
def send_receipt?
|
|
65
|
+
rand(2).zero?
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
This feature works well with argument predicates.
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
class User::Charge < ApplicationService
|
|
74
|
+
arg :send_receipt, type: [TrueClass, FalseClass], default: true
|
|
75
|
+
|
|
76
|
+
step :send_email_receipt, if: :send_receipt?
|
|
77
|
+
|
|
78
|
+
# ...
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Using Procs for Conditions
|
|
83
|
+
|
|
84
|
+
You can also use Procs (lambdas) for inline conditions:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
class User::Charge < ApplicationService
|
|
88
|
+
arg :amount, type: Float
|
|
89
|
+
|
|
90
|
+
step :apply_discount, if: -> { amount > 100 }
|
|
91
|
+
step :charge
|
|
92
|
+
step :send_large_purchase_alert, if: -> { amount > 1000 }
|
|
93
|
+
|
|
94
|
+
# ...
|
|
95
|
+
end
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
{% hint style="info" %}
|
|
99
|
+
Using Procs can make simple conditions more readable, but for complex logic, prefer extracting to a method.
|
|
100
|
+
{% endhint %}
|
|
101
|
+
|
|
102
|
+
## Inheritance
|
|
103
|
+
|
|
104
|
+
Steps are inherited from parent classes, making it easy to build upon existing services.
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
# UpdateRecordService
|
|
108
|
+
class UpdateRecordService < ApplicationService
|
|
109
|
+
arg :record, type: ApplicationRecord
|
|
110
|
+
arg :attributes, type: Hash
|
|
111
|
+
|
|
112
|
+
step :authorize
|
|
113
|
+
step :update_record
|
|
114
|
+
end
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
# User::Update inherited from UpdateRecordService
|
|
119
|
+
class User::Update < UpdateRecordService
|
|
120
|
+
# Arguments and steps are inherited from UpdateRecordService
|
|
121
|
+
end
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Injecting Steps into Execution Flow
|
|
125
|
+
|
|
126
|
+
Steps can be injected at specific points in the execution flow using `before` and `after` options.
|
|
127
|
+
|
|
128
|
+
Let's enhance the previous example by adding a step to send a notification after updating the record.
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
# User::Update inherited from UpdateRecordService
|
|
132
|
+
class User::Update < UpdateRecordService
|
|
133
|
+
step :log_action, before: :authorize
|
|
134
|
+
step :send_notification, after: :update_record
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
def log_action
|
|
139
|
+
# ...
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def send_notification
|
|
143
|
+
# ...
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Combine this with `if` and `unless` options for more control.
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
step :send_notification, after: :update_record, if: :send_notification?
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
{% hint style="info" %}
|
|
155
|
+
By default, if neither `before` nor `after` is specified, the step is added at the end of the execution flow.
|
|
156
|
+
{% endhint %}
|
|
157
|
+
|
|
158
|
+
## Always Running Steps
|
|
159
|
+
|
|
160
|
+
To ensure certain steps run regardless of previous step outcomes (errors, warnings, failed validations), use the `always: true` option. This is particularly useful for cleanup tasks, error logging, etc.
|
|
161
|
+
|
|
162
|
+
Note: if `done!` was called, the service exits early and `always: true` steps will **not** run.
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
class ParsePage < ApplicationService
|
|
166
|
+
arg :url, type: String
|
|
167
|
+
|
|
168
|
+
step :create_browser
|
|
169
|
+
step :parse_content
|
|
170
|
+
step :quit_browser, always: true
|
|
171
|
+
|
|
172
|
+
private
|
|
173
|
+
|
|
174
|
+
attr_accessor :browser
|
|
175
|
+
|
|
176
|
+
def create_browser
|
|
177
|
+
self.browser = Watir::Browser.new
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def parse_content
|
|
181
|
+
# ...
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def quit_browser
|
|
185
|
+
browser&.quit
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Early Exit with `stop!`
|
|
191
|
+
|
|
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
|
+
|
|
194
|
+
```ruby
|
|
195
|
+
class User::FindOrCreate < ApplicationService
|
|
196
|
+
arg :email, type: String
|
|
197
|
+
|
|
198
|
+
step :find_existing_user
|
|
199
|
+
step :create_user
|
|
200
|
+
step :send_welcome_email
|
|
201
|
+
|
|
202
|
+
output :user
|
|
203
|
+
|
|
204
|
+
private
|
|
205
|
+
|
|
206
|
+
def find_existing_user
|
|
207
|
+
self.user = User.find_by(email:)
|
|
208
|
+
stop! if user # Skip remaining steps if user already exists
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def create_user
|
|
212
|
+
self.user = User.create!(email:)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def send_welcome_email
|
|
216
|
+
# Only runs for newly created users
|
|
217
|
+
Mailer.welcome(user).deliver_later
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
You can check if `stop!` was called using `stopped?`:
|
|
223
|
+
|
|
224
|
+
```ruby
|
|
225
|
+
def some_step
|
|
226
|
+
stop!
|
|
227
|
+
|
|
228
|
+
# This code still runs within the same step
|
|
229
|
+
puts "Stopped? #{stopped?}" # => "Stopped? true"
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def next_step
|
|
233
|
+
# This step will NOT run because stop! was called
|
|
234
|
+
end
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
{% hint style="info" %}
|
|
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.
|
|
293
|
+
{% endhint %}
|
|
294
|
+
|
|
295
|
+
## Removing Inherited Steps
|
|
296
|
+
|
|
297
|
+
When inheriting from a parent service, you can remove steps using `remove_step`:
|
|
298
|
+
|
|
299
|
+
```ruby
|
|
300
|
+
class UpdateRecordService < ApplicationService
|
|
301
|
+
step :authorize
|
|
302
|
+
step :validate
|
|
303
|
+
step :update_record
|
|
304
|
+
step :send_notification
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
class InternalUpdate < UpdateRecordService
|
|
308
|
+
# Remove authorization for internal system updates
|
|
309
|
+
remove_step :authorize
|
|
310
|
+
remove_step :send_notification
|
|
311
|
+
end
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Using `run` Method as a Simple Alternative
|
|
315
|
+
|
|
316
|
+
For simple services that don't need multiple steps, you can define a `run` method instead of using the `step` DSL. If no steps are defined, Light Services will automatically use the `run` method as a single step.
|
|
317
|
+
|
|
318
|
+
```ruby
|
|
319
|
+
class User::SendWelcomeEmail < ApplicationService
|
|
320
|
+
arg :user, type: User
|
|
321
|
+
|
|
322
|
+
private
|
|
323
|
+
|
|
324
|
+
def run
|
|
325
|
+
Mailer.welcome(user).deliver_later
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
This is equivalent to:
|
|
331
|
+
|
|
332
|
+
```ruby
|
|
333
|
+
class User::SendWelcomeEmail < ApplicationService
|
|
334
|
+
arg :user, type: User
|
|
335
|
+
|
|
336
|
+
step :run
|
|
337
|
+
|
|
338
|
+
private
|
|
339
|
+
|
|
340
|
+
def run
|
|
341
|
+
Mailer.welcome(user).deliver_later
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Inheritance with `run` Method
|
|
347
|
+
|
|
348
|
+
The `run` method works with inheritance. If a parent service defines a `run` method, child services will inherit it:
|
|
349
|
+
|
|
350
|
+
```ruby
|
|
351
|
+
class BaseNotificationService < ApplicationService
|
|
352
|
+
arg :message, type: String
|
|
353
|
+
|
|
354
|
+
private
|
|
355
|
+
|
|
356
|
+
def run
|
|
357
|
+
send_notification(message)
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def send_notification(msg)
|
|
361
|
+
raise NotImplementedError
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
class SlackNotification < BaseNotificationService
|
|
366
|
+
private
|
|
367
|
+
|
|
368
|
+
def send_notification(msg)
|
|
369
|
+
SlackClient.post(msg)
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
class EmailNotification < BaseNotificationService
|
|
374
|
+
private
|
|
375
|
+
|
|
376
|
+
def send_notification(msg)
|
|
377
|
+
Mailer.notify(msg).deliver_later
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
{% hint style="info" %}
|
|
383
|
+
If a service has no steps defined and no `run` method (including from parent classes), a `Light::Services::NoStepsError` will be raised when the service is executed.
|
|
384
|
+
{% endhint %}
|
|
385
|
+
|
|
386
|
+
# What's Next?
|
|
387
|
+
|
|
388
|
+
Next step is to learn about outputs. Outputs are the results of a service, returned upon completion of service execution.
|
|
389
|
+
|
|
390
|
+
[Next: Outputs](outputs.md)
|
|
391
|
+
|
data/docs/summary.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Table of contents
|
|
2
|
+
|
|
3
|
+
* [Light Services](README.md)
|
|
4
|
+
* [Quickstart](quickstart.md)
|
|
5
|
+
* [Concepts](concepts.md)
|
|
6
|
+
* [Arguments](arguments.md)
|
|
7
|
+
* [Steps](steps.md)
|
|
8
|
+
* [Outputs](outputs.md)
|
|
9
|
+
* [Context](context.md)
|
|
10
|
+
* [Errors](errors.md)
|
|
11
|
+
* [Callbacks](callbacks.md)
|
|
12
|
+
* [Configuration](configuration.md)
|
|
13
|
+
* [Testing](testing.md)
|
|
14
|
+
* [Rails Generators](generators.md)
|
|
15
|
+
* [RuboCop Integration](rubocop.md)
|
|
16
|
+
* [Ruby LSP Integration](ruby-lsp.md)
|
|
17
|
+
* [Best Practices](best-practices.md)
|
|
18
|
+
* [Recipes](recipes.md)
|
|
19
|
+
* [CRUD](crud.md)
|
|
20
|
+
* [Service Rendering](service-rendering.md)
|
|
21
|
+
* [Pundit Authorization](pundit-authorization.md)
|