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.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/.github/config/rubocop_linter_action.yml +4 -4
  3. data/.github/workflows/ci.yml +12 -12
  4. data/.gitignore +1 -0
  5. data/.rubocop.yml +83 -7
  6. data/CHANGELOG.md +38 -0
  7. data/CLAUDE.md +139 -0
  8. data/Gemfile +16 -11
  9. data/Gemfile.lock +53 -27
  10. data/README.md +84 -21
  11. data/docs/arguments.md +290 -0
  12. data/docs/best-practices.md +153 -0
  13. data/docs/callbacks.md +476 -0
  14. data/docs/concepts.md +80 -0
  15. data/docs/configuration.md +204 -0
  16. data/docs/context.md +128 -0
  17. data/docs/crud.md +525 -0
  18. data/docs/errors.md +280 -0
  19. data/docs/generators.md +250 -0
  20. data/docs/outputs.md +158 -0
  21. data/docs/pundit-authorization.md +320 -0
  22. data/docs/quickstart.md +134 -0
  23. data/docs/readme.md +101 -0
  24. data/docs/recipes.md +14 -0
  25. data/docs/rubocop.md +285 -0
  26. data/docs/ruby-lsp.md +133 -0
  27. data/docs/service-rendering.md +222 -0
  28. data/docs/steps.md +391 -0
  29. data/docs/summary.md +21 -0
  30. data/docs/testing.md +549 -0
  31. data/lib/generators/light_services/install/USAGE +15 -0
  32. data/lib/generators/light_services/install/install_generator.rb +41 -0
  33. data/lib/generators/light_services/install/templates/application_service.rb.tt +8 -0
  34. data/lib/generators/light_services/install/templates/application_service_spec.rb.tt +7 -0
  35. data/lib/generators/light_services/install/templates/initializer.rb.tt +30 -0
  36. data/lib/generators/light_services/service/USAGE +21 -0
  37. data/lib/generators/light_services/service/service_generator.rb +68 -0
  38. data/lib/generators/light_services/service/templates/service.rb.tt +48 -0
  39. data/lib/generators/light_services/service/templates/service_spec.rb.tt +40 -0
  40. data/lib/light/services/base.rb +134 -122
  41. data/lib/light/services/base_with_context.rb +23 -1
  42. data/lib/light/services/callbacks.rb +157 -0
  43. data/lib/light/services/collection.rb +145 -0
  44. data/lib/light/services/concerns/execution.rb +79 -0
  45. data/lib/light/services/concerns/parent_service.rb +34 -0
  46. data/lib/light/services/concerns/state_management.rb +30 -0
  47. data/lib/light/services/config.rb +82 -16
  48. data/lib/light/services/constants.rb +100 -0
  49. data/lib/light/services/dsl/arguments_dsl.rb +85 -0
  50. data/lib/light/services/dsl/outputs_dsl.rb +81 -0
  51. data/lib/light/services/dsl/steps_dsl.rb +205 -0
  52. data/lib/light/services/dsl/validation.rb +162 -0
  53. data/lib/light/services/exceptions.rb +25 -2
  54. data/lib/light/services/message.rb +28 -3
  55. data/lib/light/services/messages.rb +92 -32
  56. data/lib/light/services/rspec/matchers/define_argument.rb +174 -0
  57. data/lib/light/services/rspec/matchers/define_output.rb +147 -0
  58. data/lib/light/services/rspec/matchers/define_step.rb +225 -0
  59. data/lib/light/services/rspec/matchers/execute_step.rb +230 -0
  60. data/lib/light/services/rspec/matchers/have_error_on.rb +148 -0
  61. data/lib/light/services/rspec/matchers/have_warning_on.rb +148 -0
  62. data/lib/light/services/rspec/matchers/trigger_callback.rb +138 -0
  63. data/lib/light/services/rspec.rb +15 -0
  64. data/lib/light/services/rubocop/cop/light_services/argument_type_required.rb +52 -0
  65. data/lib/light/services/rubocop/cop/light_services/condition_method_exists.rb +173 -0
  66. data/lib/light/services/rubocop/cop/light_services/deprecated_methods.rb +113 -0
  67. data/lib/light/services/rubocop/cop/light_services/dsl_order.rb +176 -0
  68. data/lib/light/services/rubocop/cop/light_services/missing_private_keyword.rb +102 -0
  69. data/lib/light/services/rubocop/cop/light_services/no_direct_instantiation.rb +66 -0
  70. data/lib/light/services/rubocop/cop/light_services/output_type_required.rb +52 -0
  71. data/lib/light/services/rubocop/cop/light_services/step_method_exists.rb +109 -0
  72. data/lib/light/services/rubocop.rb +12 -0
  73. data/lib/light/services/settings/field.rb +114 -0
  74. data/lib/light/services/settings/step.rb +53 -20
  75. data/lib/light/services/utils.rb +38 -0
  76. data/lib/light/services/version.rb +1 -1
  77. data/lib/light/services.rb +2 -0
  78. data/lib/ruby_lsp/light_services/addon.rb +36 -0
  79. data/lib/ruby_lsp/light_services/definition.rb +132 -0
  80. data/lib/ruby_lsp/light_services/indexing_enhancement.rb +263 -0
  81. data/light-services.gemspec +6 -8
  82. metadata +68 -26
  83. data/lib/light/services/class_based_collection/base.rb +0 -86
  84. data/lib/light/services/class_based_collection/mount.rb +0 -33
  85. data/lib/light/services/collection/arguments.rb +0 -34
  86. data/lib/light/services/collection/base.rb +0 -59
  87. data/lib/light/services/collection/outputs.rb +0 -16
  88. data/lib/light/services/settings/argument.rb +0 -68
  89. 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)