light-services 3.1.0 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 14b80318915db3dc20bd40365dc9ad00160938bb6836c1cda14bfba5301c7d7a
4
- data.tar.gz: 80515890786ed055972e5b52bbc6cdee8e79b85d4fa1227b217f4b746c9fb899
3
+ metadata.gz: 1c9112f92c81696cc02f0d3440b848abca3a501f503b9bf95e645296d0a122dc
4
+ data.tar.gz: 22c0672f290fc82f843d55f31060941dceedae0a991eb25697010ffd12b5fae9
5
5
  SHA512:
6
- metadata.gz: 454b6f2cd8fdf8615bc29c6b69e16fe704a3b19abd8f7e48fbd1e848505038db013e41396399dd5caa721d7179ae1bc3a6a99989a3adfc0f6868e9fe49a63c85
7
- data.tar.gz: 50dbf071f72a242c0c36daeb83db94db5826dca4e3e2bbe7a47f9c81006b4d2c2266731715e1948b6528df86633735839f6eee525abde302a76b7a046e484b72
6
+ metadata.gz: 4b851d34f2c6a89a63a2d2f747db12b75f50741aabcbb3cbda4d8154a1ba65560894c2cabe4ac94f7f50f4ffc8fcdeb6280b83aa61a53888f76dd691c3cf1770
7
+ data.tar.gz: 1e2ca16aaf58aee4d8adcda2d83ea881761a3f1c09543461edf5ad2516d91ee76b639eae20477c66c94619437b51a7e717932d5cc9750d18a0d6b2bd9e0e4036
data/CHANGELOG.md CHANGED
@@ -1,10 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.1.2 (2025-12-13)
4
+
5
+ ### Added
6
+
7
+ - Add `fail!` and `fail_immediately!` helpers
8
+
9
+ ### Changed
10
+
11
+ - Split `config.require_type` into `config.require_arg_type` and `config.require_output_type`
12
+
13
+ ## 3.1.1 (2025-12-13)
14
+
15
+ ### Added
16
+
17
+ - Better IDE support for callbacks DSL
18
+
3
19
  ## 3.1.0 (2025-12-13)
4
20
 
5
21
  ### Breaking changes
6
22
 
7
- - Enforce arguments and output types by default. Add `config.require_type = false` to your config to disable this behavior.
23
+ - Enforce arguments and output types by default. Use `config.require_arg_type = false` and `config.require_output_type = false` to disable this behavior. The convenience setter `config.require_type = false` sets both options at once for backward compatibility.
8
24
 
9
25
  ### Added
10
26
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- light-services (3.1.0)
4
+ light-services (3.1.1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -1,8 +1,13 @@
1
- # Table of contents
1
+ # Summary​
2
+
3
+ ## Introduction
2
4
 
3
5
  * [Light Services](README.md)
4
6
  * [Quickstart](quickstart.md)
5
7
  * [Concepts](concepts.md)
8
+
9
+ ## Deep Dive
10
+
6
11
  * [Arguments](arguments.md)
7
12
  * [Steps](steps.md)
8
13
  * [Outputs](outputs.md)
@@ -14,6 +19,9 @@
14
19
  * [Rails Generators](generators.md)
15
20
  * [RuboCop Integration](rubocop.md)
16
21
  * [Ruby LSP Integration](ruby-lsp.md)
22
+
23
+ ## Examples
24
+
17
25
  * [Best Practices](best-practices.md)
18
26
  * [Recipes](recipes.md)
19
27
  * [CRUD](crud.md)
data/docs/arguments.md CHANGED
@@ -66,13 +66,13 @@ class MyService < ApplicationService
66
66
  end
67
67
  ```
68
68
 
69
- To disable type enforcement for a specific service:
69
+ To disable type enforcement for arguments in a specific service:
70
70
 
71
71
  ```ruby
72
72
  class LegacyService < ApplicationService
73
- config require_type: false
73
+ config require_arg_type: false
74
74
 
75
- arg :name # Allowed when require_type is disabled
75
+ arg :name # Allowed when require_arg_type is disabled
76
76
  end
77
77
  ```
78
78
 
data/docs/concepts.md CHANGED
@@ -7,39 +7,39 @@ This section covers the core concepts of Light Services: **Arguments**, **Steps*
7
7
  When you call `MyService.run(args)`, the following happens:
8
8
 
9
9
  ```
10
- ┌─────────────────────────────────────────────────────────────┐
10
+ ┌──────────────────────────────────────────────────────────────┐
11
11
  │ Service.run(args) │
12
- ├─────────────────────────────────────────────────────────────┤
12
+ ├──────────────────────────────────────────────────────────────┤
13
13
  │ 1. Load default values for arguments and outputs │
14
14
  │ 2. Validate argument types │
15
15
  │ 3. Run before_service_run callbacks │
16
- ├─────────────────────────────────────────────────────────────┤
16
+ ├──────────────────────────────────────────────────────────────┤
17
17
  │ 4. Begin around_service_run callback │
18
18
  │ 5. Begin database transaction (if use_transactions: true) │
19
- │ ┌─────────────────────────────────────────────────────┐
20
- │ │ 6. Execute steps in order │
21
- │ │ - Run before_step_run / around_step_run │
22
- │ │ - Execute step method │
23
- │ │ - Run after_step_run / on_step_success │
24
- │ │ - Skip if condition (if:/unless:) not met │
25
- │ │ - Stop if errors.break? is true │
26
- │ │ - Stop if stop! was called │
27
- │ ├─────────────────────────────────────────────────────┤
28
- │ │ 7. On error → Rollback transaction │
29
- │ │ On success → Commit transaction │
30
- │ └─────────────────────────────────────────────────────┘
19
+ │ ┌─────────────────────────────────────────────────────┐
20
+ │ │ 6. Execute steps in order │
21
+ │ │ - Run before_step_run / around_step_run │
22
+ │ │ - Execute step method │
23
+ │ │ - Run after_step_run / on_step_success │
24
+ │ │ - Skip if condition (if:/unless:) not met │
25
+ │ │ - Stop if errors.break? is true │
26
+ │ │ - Stop if stop! was called │
27
+ │ ├─────────────────────────────────────────────────────┤
28
+ │ │ 7. On error → Rollback transaction │
29
+ │ │ On success → Commit transaction │
30
+ │ └─────────────────────────────────────────────────────┘
31
31
  │ 8. End around_service_run callback │
32
- ├─────────────────────────────────────────────────────────────┤
32
+ ├──────────────────────────────────────────────────────────────┤
33
33
  │ 9. Run steps marked with always: true (unless stop! called) │
34
34
  │ 10. Validate output types (if success) │
35
35
  │ 11. Copy errors/warnings to parent service (if in context) │
36
36
  │ 12. Run after_service_run callback │
37
37
  │ 13. Run on_service_success or on_service_failure callback │
38
- ├─────────────────────────────────────────────────────────────┤
38
+ ├──────────────────────────────────────────────────────────────┤
39
39
  │ 14. Return service instance │
40
40
  │ - service.success? / service.failed? │
41
41
  │ - service.outputs / service.errors │
42
- └─────────────────────────────────────────────────────────────┘
42
+ └──────────────────────────────────────────────────────────────┘
43
43
  ```
44
44
 
45
45
  ## Arguments
@@ -8,8 +8,9 @@ Configure Light Services globally using an initializer. For Rails applications,
8
8
 
9
9
  ```ruby
10
10
  Light::Services.configure do |config|
11
- # Type enforcement
12
- config.require_type = true # Require type option for all arguments and outputs
11
+ # Type enforcement
12
+ config.require_arg_type = true # Require type option for all arguments
13
+ config.require_output_type = true # Require type option for all outputs
13
14
 
14
15
  # Transaction settings
15
16
  config.use_transactions = true # Wrap each service in a database transaction
@@ -32,7 +33,8 @@ end
32
33
 
33
34
  | Option | Default | Description |
34
35
  |--------|---------|-------------|
35
- | `require_type` | `true` | Raises `Light::Services::MissingTypeError` when defining arguments or outputs without a `type` option |
36
+ | `require_arg_type` | `true` | Raises `Light::Services::MissingTypeError` when defining arguments without a `type` option |
37
+ | `require_output_type` | `true` | Raises `Light::Services::MissingTypeError` when defining outputs without a `type` option |
36
38
  | `use_transactions` | `true` | Wraps service execution in `ActiveRecord::Base.transaction` |
37
39
  | `load_errors` | `true` | Propagates errors to parent service when using `.with(self)` |
38
40
  | `break_on_error` | `true` | Stops executing remaining steps when an error is added |
@@ -159,7 +161,8 @@ To disable type enforcement globally (not recommended):
159
161
 
160
162
  ```ruby
161
163
  Light::Services.configure do |config|
162
- config.require_type = false
164
+ config.require_arg_type = false # Disable for arguments
165
+ config.require_output_type = false # Disable for outputs
163
166
  end
164
167
  ```
165
168
 
@@ -167,10 +170,22 @@ Or disable for specific services:
167
170
 
168
171
  ```ruby
169
172
  class LegacyService < ApplicationService
170
- config require_type: false
173
+ config require_arg_type: false, require_output_type: false
171
174
 
172
- arg :data # Allowed when require_type is disabled
173
- output :result # Allowed when require_type is disabled
175
+ arg :data # Allowed when require_arg_type is disabled
176
+ output :result # Allowed when require_output_type is disabled
177
+ end
178
+ ```
179
+
180
+ You can also control them independently:
181
+
182
+ ```ruby
183
+ class StrictInputService < ApplicationService
184
+ # Require types for arguments but not outputs
185
+ config require_arg_type: true, require_output_type: false
186
+
187
+ arg :data, type: Hash # Type required
188
+ output :result # Type not required
174
189
  end
175
190
  ```
176
191
 
data/docs/errors.md CHANGED
@@ -44,6 +44,24 @@ class ParsePage < ApplicationService
44
44
  end
45
45
  ```
46
46
 
47
+ ## Quick Error with `fail!`
48
+
49
+ The `fail!` method is a shortcut for adding an error to the `:base` key:
50
+
51
+ ```ruby
52
+ class ParsePage < ApplicationService
53
+ def validate
54
+ fail!("URL is required") if url.blank?
55
+ end
56
+ end
57
+ ```
58
+
59
+ This is equivalent to:
60
+
61
+ ```ruby
62
+ errors.add(:base, "URL is required")
63
+ ```
64
+
47
65
  ## Reading Errors
48
66
 
49
67
  To check if a service has errors, you can use the `#failed?` method. You can also use methods like `errors.any?` to inspect errors.
@@ -205,11 +223,13 @@ Light Services defines several exception classes for different error scenarios:
205
223
  | `Light::Services::ReservedNameError` | Raised when using a reserved name for arguments, outputs, or steps |
206
224
  | `Light::Services::InvalidNameError` | Raised when using an invalid name format |
207
225
  | `Light::Services::NoStepsError` | Raised when a service has no steps defined and no `run` method |
208
- | `Light::Services::MissingTypeError` | Raised when defining an argument or output without a `type` option when `require_type` is enabled |
226
+ | `Light::Services::MissingTypeError` | Raised when defining an argument or output without a `type` option when `require_arg_type` or `require_output_type` is enabled |
227
+ | `Light::Services::StopExecution` | Control flow exception raised by `stop_immediately!` to halt execution without rollback |
228
+ | `Light::Services::FailExecution` | Control flow exception raised by `fail_immediately!` to halt execution and rollback transactions |
209
229
 
210
230
  ### MissingTypeError
211
231
 
212
- This exception is raised when you define an argument or output without a `type` option. Since `require_type` is enabled by default, all arguments and outputs must have a type.
232
+ This exception is raised when you define an argument or output without a `type` option. Since `require_arg_type` and `require_output_type` are enabled by default, all arguments and outputs must have a type.
213
233
 
214
234
  ```ruby
215
235
  class MyService < ApplicationService
@@ -230,9 +250,10 @@ If you need to disable type enforcement for legacy services, you can use the `co
230
250
 
231
251
  ```ruby
232
252
  class LegacyService < ApplicationService
233
- config require_type: false
253
+ config require_arg_type: false, require_output_type: false
234
254
 
235
- arg :data # Allowed when require_type is disabled
255
+ arg :data # Allowed when require_arg_type is disabled
256
+ output :result # Allowed when require_output_type is disabled
236
257
  end
237
258
  ```
238
259
 
data/docs/outputs.md CHANGED
@@ -89,13 +89,13 @@ class MyService < ApplicationService
89
89
  end
90
90
  ```
91
91
 
92
- To disable type enforcement for a specific service:
92
+ To disable type enforcement for outputs in a specific service:
93
93
 
94
94
  ```ruby
95
95
  class LegacyService < ApplicationService
96
- config require_type: false
96
+ config require_output_type: false
97
97
 
98
- output :data # Allowed when require_type is disabled
98
+ output :data # Allowed when require_output_type is disabled
99
99
  end
100
100
  ```
101
101
 
data/docs/steps.md CHANGED
@@ -292,6 +292,58 @@ end
292
292
  **Database Transactions:** Calling `stop_immediately!` does NOT rollback database transactions. All database changes made before `stop_immediately!` was called will be committed.
293
293
  {% endhint %}
294
294
 
295
+ ## Immediate Failure with `fail_immediately!`
296
+
297
+ Use `fail_immediately!` when you need to halt execution immediately AND rollback any database transactions. Unlike `stop_immediately!`, this method adds an error and causes transaction rollback.
298
+
299
+ ```ruby
300
+ class Payment::Process < ApplicationService
301
+ arg :amount, type: Integer
302
+ arg :card_token, type: String
303
+
304
+ step :validate_card
305
+ step :charge_card
306
+ step :send_receipt
307
+
308
+ output :transaction_id, type: String
309
+
310
+ private
311
+
312
+ def validate_card
313
+ unless valid_card?(card_token)
314
+ fail_immediately!("Card validation failed")
315
+ end
316
+
317
+ # This code won't run if card is invalid
318
+ log_validation_success
319
+ end
320
+
321
+ def charge_card
322
+ # This step won't run if fail_immediately! was called
323
+ self.transaction_id = PaymentGateway.charge(amount, card_token)
324
+ end
325
+ end
326
+ ```
327
+
328
+ {% hint style="warning" %}
329
+ `fail_immediately!` raises an internal exception to halt execution. Steps marked with `always: true` will NOT run when `fail_immediately!` is called.
330
+ {% endhint %}
331
+
332
+ {% hint style="danger" %}
333
+ **Database Transactions:** Calling `fail_immediately!` DOES rollback database transactions. All database changes made before `fail_immediately!` was called will be rolled back.
334
+ {% endhint %}
335
+
336
+ ### Comparison Table
337
+
338
+ | Method | Adds Error | Stops Execution | Transaction Rollback |
339
+ |--------|------------|-----------------|---------------------|
340
+ | `stop!` | No | After current step | No |
341
+ | `stop_immediately!` | No | Immediately | No |
342
+ | `fail!(msg)` | Yes (:base) | After current step* | No |
343
+ | `fail_immediately!(msg)` | Yes (:base) | Immediately | Yes |
344
+
345
+ *By default, adding an error stops subsequent steps from running due to `break_on_add` configuration.
346
+
295
347
  ## Removing Inherited Steps
296
348
 
297
349
  When inheriting from a parent service, you can remove steps using `remove_step`:
@@ -43,6 +43,7 @@ module Light
43
43
  # result.success? # => true
44
44
  # result.user # => #<User id: 1, name: "John">
45
45
  class Base
46
+ extend CallbackDsl
46
47
  include Callbacks
47
48
  include Dsl::ArgumentsDsl
48
49
  include Dsl::OutputsDsl
@@ -135,6 +136,25 @@ module Light
135
136
  raise Light::Services::StopExecution
136
137
  end
137
138
 
139
+ # Add an error to the :base key.
140
+ #
141
+ # @param message [String] the error message
142
+ # @return [void]
143
+ def fail!(message)
144
+ errors.add(:base, message)
145
+ end
146
+
147
+ # Add an error and stop execution immediately, causing transaction rollback.
148
+ #
149
+ # @param message [String] the error message
150
+ # @raise [FailExecution] always raises to halt execution and rollback
151
+ # @return [void]
152
+ def fail_immediately!(message)
153
+ @stopped = true
154
+ errors.add(:base, message, rollback: false)
155
+ raise Light::Services::FailExecution
156
+ end
157
+
138
158
  # Execute the service steps.
139
159
  #
140
160
  # @return [void]
@@ -48,64 +48,6 @@ module Light
48
48
  :on_service_failure,
49
49
  ].freeze
50
50
 
51
- def self.included(base)
52
- base.extend(ClassMethods)
53
- end
54
-
55
- # Class methods for registering callbacks.
56
- #
57
- # Each callback event has a corresponding class method:
58
- # - {before_step_run} - before each step executes
59
- # - {after_step_run} - after each step executes
60
- # - {around_step_run} - wraps step execution (must yield)
61
- # - {on_step_success} - when a step completes without adding errors
62
- # - {on_step_failure} - when a step adds errors
63
- # - {on_step_crash} - when a step raises an exception
64
- # - {before_service_run} - before the service starts
65
- # - {after_service_run} - after the service completes
66
- # - {around_service_run} - wraps service execution (must yield)
67
- # - {on_service_success} - when service completes without errors
68
- # - {on_service_failure} - when service completes with errors
69
- module ClassMethods
70
- # Define DSL methods for each callback event
71
- EVENTS.each do |event|
72
- define_method(event) do |method_name = nil, &block|
73
- callback = method_name || block
74
- raise ArgumentError, "#{event} requires a method name (symbol) or a block" unless callback
75
-
76
- unless callback.is_a?(Symbol) || callback.is_a?(Proc)
77
- raise ArgumentError,
78
- "#{event} callback must be a Symbol or Proc"
79
- end
80
-
81
- callbacks_for(event) << callback
82
- end
83
- end
84
-
85
- # Get callbacks defined in this class for a specific event.
86
- #
87
- # @param event [Symbol] the callback event name
88
- # @return [Array<Symbol, Proc>] callbacks for this event
89
- def callbacks_for(event)
90
- @callbacks ||= {}
91
- @callbacks[event] ||= []
92
- end
93
-
94
- # Get all callbacks for an event including inherited ones.
95
- #
96
- # @param event [Symbol] the callback event name
97
- # @return [Array<Symbol, Proc>] all callbacks for this event
98
- def all_callbacks_for(event)
99
- if superclass.respond_to?(:all_callbacks_for)
100
- inherited = superclass.all_callbacks_for(event)
101
- else
102
- inherited = []
103
- end
104
-
105
- inherited + callbacks_for(event)
106
- end
107
- end
108
-
109
51
  # Run all callbacks for a given event.
110
52
  #
111
53
  # @param event [Symbol] the callback event name
@@ -153,5 +95,261 @@ module Light
153
95
  end
154
96
  end
155
97
  end
98
+
99
+ # Class methods for registering callbacks.
100
+ # Extend this module in your service class to get callback DSL methods.
101
+ #
102
+ # @example
103
+ # class MyService < Light::Services::Base
104
+ # before_service_run :setup
105
+ # after_service_run :cleanup
106
+ # end
107
+ module CallbackDsl
108
+ # Registers a callback to run before each step executes.
109
+ #
110
+ # @param method_name [Symbol, nil] name of the instance method to call
111
+ # @yield [service, step_name] block to execute if no method name provided
112
+ # @yieldparam service [Light::Services::Base] the service instance
113
+ # @yieldparam step_name [Symbol] the name of the step about to run
114
+ # @return [void]
115
+ # @raise [ArgumentError] if neither method name nor block is provided
116
+ #
117
+ # @example With method name
118
+ # before_step_run :log_step_start
119
+ #
120
+ # @example With block
121
+ # before_step_run { |service, step_name| puts "Starting #{step_name}" }
122
+ def before_step_run(method_name = nil, &block)
123
+ register_callback(:before_step_run, method_name, &block)
124
+ end
125
+
126
+ # Registers a callback to run after each step executes.
127
+ #
128
+ # @param method_name [Symbol, nil] name of the instance method to call
129
+ # @yield [service, step_name] block to execute if no method name provided
130
+ # @yieldparam service [Light::Services::Base] the service instance
131
+ # @yieldparam step_name [Symbol] the name of the step that just ran
132
+ # @return [void]
133
+ # @raise [ArgumentError] if neither method name nor block is provided
134
+ #
135
+ # @example With method name
136
+ # after_step_run :log_step_complete
137
+ #
138
+ # @example With block
139
+ # after_step_run { |service, step_name| puts "Finished #{step_name}" }
140
+ def after_step_run(method_name = nil, &block)
141
+ register_callback(:after_step_run, method_name, &block)
142
+ end
143
+
144
+ # Registers an around callback that wraps each step execution.
145
+ # The callback must yield to execute the step.
146
+ #
147
+ # @param method_name [Symbol, nil] name of the instance method to call
148
+ # @yield [service, step_name] block to execute if no method name provided
149
+ # @yieldparam service [Light::Services::Base] the service instance
150
+ # @yieldparam step_name [Symbol] the name of the step being wrapped
151
+ # @return [void]
152
+ # @raise [ArgumentError] if neither method name nor block is provided
153
+ #
154
+ # @example With method name
155
+ # around_step_run :with_step_timing
156
+ #
157
+ # def with_step_timing(service, step_name)
158
+ # start = Time.now
159
+ # yield
160
+ # puts "#{step_name} took #{Time.now - start}s"
161
+ # end
162
+ def around_step_run(method_name = nil, &block)
163
+ register_callback(:around_step_run, method_name, &block)
164
+ end
165
+
166
+ # Registers a callback to run when a step completes successfully (without adding errors).
167
+ #
168
+ # @param method_name [Symbol, nil] name of the instance method to call
169
+ # @yield [service, step_name] block to execute if no method name provided
170
+ # @yieldparam service [Light::Services::Base] the service instance
171
+ # @yieldparam step_name [Symbol] the name of the successful step
172
+ # @return [void]
173
+ # @raise [ArgumentError] if neither method name nor block is provided
174
+ #
175
+ # @example With method name
176
+ # on_step_success :track_step_success
177
+ #
178
+ # @example With block
179
+ # on_step_success { |service, step_name| Analytics.track("step.success", step: step_name) }
180
+ def on_step_success(method_name = nil, &block)
181
+ register_callback(:on_step_success, method_name, &block)
182
+ end
183
+
184
+ # Registers a callback to run when a step fails (adds errors).
185
+ #
186
+ # @param method_name [Symbol, nil] name of the instance method to call
187
+ # @yield [service, step_name] block to execute if no method name provided
188
+ # @yieldparam service [Light::Services::Base] the service instance
189
+ # @yieldparam step_name [Symbol] the name of the failed step
190
+ # @return [void]
191
+ # @raise [ArgumentError] if neither method name nor block is provided
192
+ #
193
+ # @example With method name
194
+ # on_step_failure :handle_step_error
195
+ #
196
+ # @example With block
197
+ # on_step_failure { |service, step_name| Rails.logger.error("Step #{step_name} failed") }
198
+ def on_step_failure(method_name = nil, &block)
199
+ register_callback(:on_step_failure, method_name, &block)
200
+ end
201
+
202
+ # Registers a callback to run when a step raises an exception.
203
+ #
204
+ # @param method_name [Symbol, nil] name of the instance method to call
205
+ # @yield [service, step_name, exception] block to execute if no method name provided
206
+ # @yieldparam service [Light::Services::Base] the service instance
207
+ # @yieldparam step_name [Symbol] the name of the crashed step
208
+ # @yieldparam exception [Exception] the exception that was raised
209
+ # @return [void]
210
+ # @raise [ArgumentError] if neither method name nor block is provided
211
+ #
212
+ # @example With method name
213
+ # on_step_crash :report_crash
214
+ #
215
+ # @example With block
216
+ # on_step_crash { |service, step_name, error| Sentry.capture_exception(error) }
217
+ def on_step_crash(method_name = nil, &block)
218
+ register_callback(:on_step_crash, method_name, &block)
219
+ end
220
+
221
+ # Registers a callback to run before the service starts executing.
222
+ #
223
+ # @param method_name [Symbol, nil] name of the instance method to call
224
+ # @yield [service] block to execute if no method name provided
225
+ # @yieldparam service [Light::Services::Base] the service instance
226
+ # @return [void]
227
+ # @raise [ArgumentError] if neither method name nor block is provided
228
+ #
229
+ # @example With method name
230
+ # before_service_run :log_start
231
+ #
232
+ # @example With block
233
+ # before_service_run { |service| Rails.logger.info("Starting #{service.class.name}") }
234
+ def before_service_run(method_name = nil, &block)
235
+ register_callback(:before_service_run, method_name, &block)
236
+ end
237
+
238
+ # Registers a callback to run after the service completes (regardless of success/failure).
239
+ #
240
+ # @param method_name [Symbol, nil] name of the instance method to call
241
+ # @yield [service] block to execute if no method name provided
242
+ # @yieldparam service [Light::Services::Base] the service instance
243
+ # @return [void]
244
+ # @raise [ArgumentError] if neither method name nor block is provided
245
+ #
246
+ # @example With method name
247
+ # after_service_run :cleanup
248
+ #
249
+ # @example With block
250
+ # after_service_run { |service| Rails.logger.info("Done!") }
251
+ def after_service_run(method_name = nil, &block)
252
+ register_callback(:after_service_run, method_name, &block)
253
+ end
254
+
255
+ # Registers an around callback that wraps the entire service execution.
256
+ # The callback must yield to execute the service.
257
+ #
258
+ # @param method_name [Symbol, nil] name of the instance method to call
259
+ # @yield [service] block to execute if no method name provided
260
+ # @yieldparam service [Light::Services::Base] the service instance
261
+ # @return [void]
262
+ # @raise [ArgumentError] if neither method name nor block is provided
263
+ #
264
+ # @example With method name
265
+ # around_service_run :with_timing
266
+ #
267
+ # def with_timing(service)
268
+ # start = Time.now
269
+ # yield
270
+ # puts "Took #{Time.now - start}s"
271
+ # end
272
+ def around_service_run(method_name = nil, &block)
273
+ register_callback(:around_service_run, method_name, &block)
274
+ end
275
+
276
+ # Registers a callback to run when the service completes successfully (without errors).
277
+ #
278
+ # @param method_name [Symbol, nil] name of the instance method to call
279
+ # @yield [service] block to execute if no method name provided
280
+ # @yieldparam service [Light::Services::Base] the service instance
281
+ # @return [void]
282
+ # @raise [ArgumentError] if neither method name nor block is provided
283
+ #
284
+ # @example With method name
285
+ # on_service_success :send_notification
286
+ #
287
+ # @example With block
288
+ # on_service_success { |service| NotificationMailer.success(service.user).deliver_later }
289
+ def on_service_success(method_name = nil, &block)
290
+ register_callback(:on_service_success, method_name, &block)
291
+ end
292
+
293
+ # Registers a callback to run when the service completes with errors.
294
+ #
295
+ # @param method_name [Symbol, nil] name of the instance method to call
296
+ # @yield [service] block to execute if no method name provided
297
+ # @yieldparam service [Light::Services::Base] the service instance
298
+ # @return [void]
299
+ # @raise [ArgumentError] if neither method name nor block is provided
300
+ #
301
+ # @example With method name
302
+ # on_service_failure :log_error
303
+ #
304
+ # @example With block
305
+ # on_service_failure { |service| Rails.logger.error(service.errors.full_messages) }
306
+ def on_service_failure(method_name = nil, &block)
307
+ register_callback(:on_service_failure, method_name, &block)
308
+ end
309
+
310
+ # Get callbacks defined in this class for a specific event.
311
+ #
312
+ # @param event [Symbol] the callback event name
313
+ # @return [Array<Symbol, Proc>] callbacks for this event
314
+ def callbacks_for(event)
315
+ @callbacks ||= {}
316
+ @callbacks[event] ||= []
317
+ end
318
+
319
+ # Get all callbacks for an event including inherited ones.
320
+ #
321
+ # @param event [Symbol] the callback event name
322
+ # @return [Array<Symbol, Proc>] all callbacks for this event
323
+ def all_callbacks_for(event)
324
+ if superclass.respond_to?(:all_callbacks_for)
325
+ inherited = superclass.all_callbacks_for(event)
326
+ else
327
+ inherited = []
328
+ end
329
+
330
+ inherited + callbacks_for(event)
331
+ end
332
+
333
+ private
334
+
335
+ # Registers a callback for a given event.
336
+ #
337
+ # @param event [Symbol] the callback event name
338
+ # @param method_name [Symbol, nil] name of the instance method to call
339
+ # @yield block to execute if no method name provided
340
+ # @return [void]
341
+ # @raise [ArgumentError] if neither method name nor block is provided
342
+ # @api private
343
+ def register_callback(event, method_name = nil, &block)
344
+ callback = method_name || block
345
+ raise ArgumentError, "#{event} requires a method name (symbol) or a block" unless callback
346
+
347
+ unless callback.is_a?(Symbol) || callback.is_a?(Proc)
348
+ raise ArgumentError, "#{event} callback must be a Symbol or Proc"
349
+ end
350
+
351
+ callbacks_for(event) << callback
352
+ end
353
+ end
156
354
  end
157
355
  end
@@ -44,6 +44,10 @@ module Light
44
44
  # Gracefully handle stop_immediately! inside transaction to prevent rollback
45
45
  @stopped = true
46
46
  end
47
+ rescue Light::Services::FailExecution
48
+ # FailExecution bubbles out of transaction (causing rollback) but is caught here
49
+ # @stopped is already set by fail_immediately!
50
+ nil
47
51
  end
48
52
 
49
53
  # Run steps with parameter `always` if they weren't launched because of errors/warnings
@@ -10,7 +10,8 @@ module Light
10
10
  #
11
11
  # @example
12
12
  # Light::Services.configure do |config|
13
- # config.require_type = true
13
+ # config.require_arg_type = true
14
+ # config.require_output_type = true
14
15
  # config.use_transactions = false
15
16
  # end
16
17
  def configure
@@ -28,13 +29,16 @@ module Light
28
29
  # Configuration class for Light::Services global settings.
29
30
  #
30
31
  # @example Accessing configuration
31
- # Light::Services.config.require_type # => true
32
+ # Light::Services.config.require_arg_type # => true
32
33
  #
33
34
  # @example Modifying configuration
34
35
  # Light::Services.config.use_transactions = false
35
36
  class Config
36
- # @return [Boolean] whether arguments and outputs must have a type specified
37
- attr_reader :require_type
37
+ # @return [Boolean] whether arguments must have a type specified
38
+ attr_reader :require_arg_type
39
+
40
+ # @return [Boolean] whether outputs must have a type specified
41
+ attr_reader :require_output_type
38
42
 
39
43
  # @return [Boolean] whether to wrap service execution in a database transaction
40
44
  attr_reader :use_transactions
@@ -69,7 +73,8 @@ module Light
69
73
  attr_reader :ruby_lsp_type_mappings
70
74
 
71
75
  DEFAULTS = {
72
- require_type: true,
76
+ require_arg_type: true,
77
+ require_output_type: true,
73
78
  use_transactions: true,
74
79
 
75
80
  load_errors: true,
@@ -92,6 +97,16 @@ module Light
92
97
  end
93
98
  end
94
99
 
100
+ # Convenience setter for backward compatibility.
101
+ # Sets both require_arg_type and require_output_type.
102
+ #
103
+ # @param value [Boolean] whether to require types for arguments and outputs
104
+ # @return [void]
105
+ def require_type=(value)
106
+ self.require_arg_type = value
107
+ self.require_output_type = value
108
+ end
109
+
95
110
  # Initialize configuration with default values.
96
111
  def initialize
97
112
  reset_to_defaults!
@@ -135,26 +135,37 @@ module Light
135
135
  # @param opts [Hash] the options hash to check for type
136
136
  def self.validate_type_required!(name, field_type, service_class, opts)
137
137
  return if opts.key?(:type)
138
- return unless require_type_enabled?(service_class)
138
+ return unless require_type_enabled_for?(field_type, service_class)
139
139
 
140
+ config_name = field_type == :argument ? "require_arg_type" : "require_output_type"
140
141
  raise Light::Services::MissingTypeError,
141
142
  "#{field_type.to_s.capitalize} `#{name}` in #{service_class} must have a type specified " \
142
- "(require_type is enabled)"
143
+ "(#{config_name} is enabled)"
143
144
  end
144
145
 
145
- # Check if require_type is enabled for the service class
146
- def self.require_type_enabled?(service_class)
146
+ # Check if require_type is enabled for the given field type and service class
147
+ #
148
+ # @param field_type [Symbol] the type of field (:argument, :output)
149
+ # @param service_class [Class] the service class to check
150
+ # @return [Boolean] whether type is required for the field type
151
+ def self.require_type_enabled_for?(field_type, service_class)
152
+ config_key = field_type == :argument ? :require_arg_type : :require_output_type
153
+
147
154
  # Check class-level config in the inheritance chain, then fall back to global config
148
155
  klass = service_class
149
156
  while klass.respond_to?(:class_config)
150
157
  class_config = klass.class_config
151
158
 
159
+ # Check specific config first (require_arg_type or require_output_type)
160
+ return class_config[config_key] if class_config&.key?(config_key)
161
+
162
+ # Check convenience config (require_type) for backward compatibility
152
163
  return class_config[:require_type] if class_config&.key?(:require_type)
153
164
 
154
165
  klass = klass.superclass
155
166
  end
156
167
 
157
- Light::Services.config.require_type
168
+ Light::Services.config.public_send(config_key)
158
169
  end
159
170
  end
160
171
  end
@@ -24,6 +24,10 @@ module Light
24
24
  # Not an error - used to halt execution gracefully.
25
25
  class StopExecution < StandardError; end
26
26
 
27
+ # Control flow exception for fail_immediately!
28
+ # Unlike StopExecution, this exception causes transaction rollback.
29
+ class FailExecution < StandardError; end
30
+
27
31
  # @deprecated Use {Error} instead
28
32
  NoStepError = Error
29
33
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Light
4
4
  module Services
5
- VERSION = "3.1.0"
5
+ VERSION = "3.1.2"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: light-services
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.0
4
+ version: 3.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kodkod
@@ -34,6 +34,8 @@ files:
34
34
  - Rakefile
35
35
  - bin/console
36
36
  - bin/setup
37
+ - docs/README.md
38
+ - docs/SUMMARY.md
37
39
  - docs/arguments.md
38
40
  - docs/best-practices.md
39
41
  - docs/callbacks.md
@@ -46,13 +48,11 @@ files:
46
48
  - docs/outputs.md
47
49
  - docs/pundit-authorization.md
48
50
  - docs/quickstart.md
49
- - docs/readme.md
50
51
  - docs/recipes.md
51
52
  - docs/rubocop.md
52
53
  - docs/ruby-lsp.md
53
54
  - docs/service-rendering.md
54
55
  - docs/steps.md
55
- - docs/summary.md
56
56
  - docs/testing.md
57
57
  - lib/generators/light_services/install/USAGE
58
58
  - lib/generators/light_services/install/install_generator.rb
File without changes