light-services 3.0.0 → 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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -1
  3. data/CHANGELOG.md +15 -0
  4. data/CLAUDE.md +1 -1
  5. data/Gemfile.lock +1 -1
  6. data/README.md +11 -11
  7. data/docs/arguments.md +23 -0
  8. data/docs/concepts.md +2 -2
  9. data/docs/configuration.md +36 -0
  10. data/docs/errors.md +31 -1
  11. data/docs/outputs.md +23 -0
  12. data/docs/quickstart.md +1 -1
  13. data/docs/readme.md +12 -11
  14. data/docs/rubocop.md +285 -0
  15. data/docs/ruby-lsp.md +133 -0
  16. data/docs/steps.md +62 -8
  17. data/docs/summary.md +2 -0
  18. data/docs/testing.md +1 -1
  19. data/lib/light/services/base.rb +109 -7
  20. data/lib/light/services/base_with_context.rb +23 -1
  21. data/lib/light/services/callbacks.rb +59 -5
  22. data/lib/light/services/collection.rb +50 -2
  23. data/lib/light/services/concerns/execution.rb +3 -0
  24. data/lib/light/services/config.rb +83 -3
  25. data/lib/light/services/constants.rb +3 -0
  26. data/lib/light/services/dsl/arguments_dsl.rb +1 -0
  27. data/lib/light/services/dsl/outputs_dsl.rb +1 -0
  28. data/lib/light/services/dsl/validation.rb +30 -0
  29. data/lib/light/services/exceptions.rb +19 -1
  30. data/lib/light/services/message.rb +28 -3
  31. data/lib/light/services/messages.rb +74 -2
  32. data/lib/light/services/rubocop/cop/light_services/argument_type_required.rb +52 -0
  33. data/lib/light/services/rubocop/cop/light_services/condition_method_exists.rb +173 -0
  34. data/lib/light/services/rubocop/cop/light_services/deprecated_methods.rb +113 -0
  35. data/lib/light/services/rubocop/cop/light_services/dsl_order.rb +176 -0
  36. data/lib/light/services/rubocop/cop/light_services/missing_private_keyword.rb +102 -0
  37. data/lib/light/services/rubocop/cop/light_services/no_direct_instantiation.rb +66 -0
  38. data/lib/light/services/rubocop/cop/light_services/output_type_required.rb +52 -0
  39. data/lib/light/services/rubocop/cop/light_services/step_method_exists.rb +109 -0
  40. data/lib/light/services/rubocop.rb +12 -0
  41. data/lib/light/services/settings/field.rb +33 -5
  42. data/lib/light/services/settings/step.rb +23 -5
  43. data/lib/light/services/version.rb +1 -1
  44. data/lib/ruby_lsp/light_services/addon.rb +36 -0
  45. data/lib/ruby_lsp/light_services/definition.rb +132 -0
  46. data/lib/ruby_lsp/light_services/indexing_enhancement.rb +263 -0
  47. metadata +15 -1
data/docs/ruby-lsp.md ADDED
@@ -0,0 +1,133 @@
1
+ # Ruby LSP Integration
2
+
3
+ Light Services provides a Ruby LSP add-on that enhances your editor experience by informing the language server about methods generated by the `arg` and `output` DSL keywords.
4
+
5
+ ## Features
6
+
7
+ When you use the `arg` or `output` keywords, Light Services dynamically generates methods at runtime:
8
+
9
+ ```ruby
10
+ class MyService < ApplicationService
11
+ arg :user, type: User
12
+ output :result, type: Hash
13
+ end
14
+ ```
15
+
16
+ This generates the following methods:
17
+ - `user` - getter method (returns `User`)
18
+ - `user?` - predicate method (returns boolean)
19
+ - `user=` - setter method (private, accepts `User`)
20
+ - `result` - getter method (returns `Hash`)
21
+ - `result?` - predicate method (returns boolean)
22
+ - `result=` - setter method (private, accepts `Hash`)
23
+
24
+ The Ruby LSP add-on teaches the language server about these generated methods, enabling:
25
+
26
+ - **Go to Definition** - Navigate to the `arg`/`output` declaration
27
+ - **Completion** - Autocomplete generated method names
28
+ - **Hover** - See information about generated methods, including return types
29
+ - **Signature Help** - Get parameter hints for setter methods
30
+ - **Workspace Symbol** - Find generated methods in symbol search
31
+
32
+ ## Setup
33
+
34
+ The add-on is automatically discovered by Ruby LSP when Light Services is in your project's dependencies. No additional configuration is required.
35
+
36
+ ### Requirements
37
+
38
+ - Ruby LSP `~> 0.26` or later
39
+ - Light Services gem installed in your project
40
+
41
+ ### Verification
42
+
43
+ To verify the add-on is loaded, check the Ruby LSP output in your editor. You should see "Ruby LSP Light Services" listed among the active add-ons.
44
+
45
+ ## How It Works
46
+
47
+ The add-on uses Ruby LSP's **indexing enhancement** system to register generated methods during code indexing. When the indexer encounters an `arg` or `output` call with a symbol argument, it automatically registers the three generated methods (getter, predicate, setter) in the index.
48
+
49
+ This is a static analysis approach - the add-on analyzes your source code without executing it. This means:
50
+
51
+ - Methods are recognized immediately as you type
52
+ - No running application is required
53
+ - Works with any editor that supports Ruby LSP
54
+
55
+ ## Type Inference
56
+
57
+ The add-on extracts type information from the `type:` option and includes it as YARD-style documentation comments. This enables hover information to display return types for generated methods.
58
+
59
+ ### Simple Ruby Types
60
+
61
+ ```ruby
62
+ arg :user, type: User # → User
63
+ arg :items, type: Array # → Array
64
+ arg :name, type: String # → String
65
+ ```
66
+
67
+ ### Namespaced Types
68
+
69
+ ```ruby
70
+ arg :payment, type: Stripe::Charge # → Stripe::Charge
71
+ arg :config, type: MyApp::Configuration # → MyApp::Configuration
72
+ ```
73
+
74
+ ### Dry-Types
75
+
76
+ Common dry-types are mapped to their underlying Ruby types:
77
+
78
+ | Dry-Type | Ruby Type |
79
+ |----------|-----------|
80
+ | `Types::String`, `Types::Strict::String`, `Types::Coercible::String` | `String` |
81
+ | `Types::Integer`, `Types::Strict::Integer`, `Types::Coercible::Integer` | `Integer` |
82
+ | `Types::Float`, `Types::Strict::Float`, `Types::Coercible::Float` | `Float` |
83
+ | `Types::Bool`, `Types::Strict::Bool` | `TrueClass \| FalseClass` |
84
+ | `Types::Array`, `Types::Strict::Array` | `Array` |
85
+ | `Types::Hash`, `Types::Strict::Hash` | `Hash` |
86
+ | `Types::Symbol`, `Types::Strict::Symbol` | `Symbol` |
87
+ | `Types::Date`, `Types::DateTime`, `Types::Time` | `Date`, `DateTime`, `Time` |
88
+
89
+ Constrained and parameterized types extract their base type:
90
+
91
+ ```ruby
92
+ arg :email, type: Types::String.constrained(format: /@/) # → String
93
+ arg :tags, type: Types::Array.of(Types::String) # → Array
94
+ arg :status, type: Types::String.enum("active", "pending") # → String
95
+ ```
96
+
97
+ ### Custom Type Mappings
98
+
99
+ You can add custom type mappings through the Light Services configuration:
100
+
101
+ ```ruby
102
+ # config/initializers/light_services.rb
103
+ Light::Services.configure do |config|
104
+ config.ruby_lsp_type_mappings = {
105
+ "Types::UUID" => "String",
106
+ "Types::Money" => "BigDecimal",
107
+ "Types::JSON" => "Hash",
108
+ "CustomTypes::Email" => "String",
109
+ "MyApp::Types::PhoneNumber" => "String",
110
+ }
111
+ end
112
+ ```
113
+
114
+ Custom mappings take precedence over the default dry-types mappings, allowing you to:
115
+
116
+ - Add mappings for your own custom types
117
+ - Override default mappings if needed
118
+ - Support domain-specific type modules
119
+
120
+ ## Limitations
121
+
122
+ - Only `arg` and `output` declarations with a symbol as the first argument are recognized
123
+ - The add-on cannot detect dynamically computed argument names (e.g., `arg some_variable`)
124
+ - Inherited arguments/outputs from parent classes are not automatically discovered
125
+ - Parameterized dry-types like `Types::Array.of(Types::String)` resolve to the container type (`Array`), not the full generic type
126
+ - Custom dry-type definitions outside the standard `Types::` namespace are not mapped
127
+
128
+ ## What's Next?
129
+
130
+ Learn more about other integrations:
131
+
132
+ - [RuboCop Integration](rubocop.md) - Static analysis cops for services
133
+ - [Testing](testing.md) - Testing your services with RSpec matchers
data/docs/steps.md CHANGED
@@ -187,9 +187,9 @@ class ParsePage < ApplicationService
187
187
  end
188
188
  ```
189
189
 
190
- ## Early Exit with `done!`
190
+ ## Early Exit with `stop!`
191
191
 
192
- Use `done!` 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.
192
+ Use `stop!` to stop executing remaining steps without adding an error. This is useful when you've completed the service's goal early and don't need to run subsequent steps.
193
193
 
194
194
  ```ruby
195
195
  class User::FindOrCreate < ApplicationService
@@ -205,7 +205,7 @@ class User::FindOrCreate < ApplicationService
205
205
 
206
206
  def find_existing_user
207
207
  self.user = User.find_by(email:)
208
- done! if user # Skip remaining steps if user already exists
208
+ stop! if user # Skip remaining steps if user already exists
209
209
  end
210
210
 
211
211
  def create_user
@@ -219,23 +219,77 @@ class User::FindOrCreate < ApplicationService
219
219
  end
220
220
  ```
221
221
 
222
- You can check if `done!` was called using `done?`:
222
+ You can check if `stop!` was called using `stopped?`:
223
223
 
224
224
  ```ruby
225
225
  def some_step
226
- done!
226
+ stop!
227
227
 
228
228
  # This code still runs within the same step
229
- puts "Done? #{done?}" # => "Done? true"
229
+ puts "Stopped? #{stopped?}" # => "Stopped? true"
230
230
  end
231
231
 
232
232
  def next_step
233
- # This step will NOT run because done! was called
233
+ # This step will NOT run because stop! was called
234
234
  end
235
235
  ```
236
236
 
237
237
  {% hint style="info" %}
238
- `done!` stops subsequent steps from running, including steps marked with `always: true`. Code after `done!` within the same step method will still execute.
238
+ `stop!` stops subsequent steps from running, including steps marked with `always: true`. Code after `stop!` within the same step method will still execute.
239
+ {% endhint %}
240
+
241
+ {% hint style="success" %}
242
+ **Database Transactions:** Calling `stop!` does NOT rollback database transactions. All database changes made before `stop!` was called will be committed.
243
+ {% endhint %}
244
+
245
+ {% hint style="info" %}
246
+ **Backward Compatibility:** `done!` and `done?` are still available as aliases for `stop!` and `stopped?`.
247
+ {% endhint %}
248
+
249
+ ## Immediate Exit with `stop_immediately!`
250
+
251
+ Use `stop_immediately!` when you need to halt execution immediately, even within the current step. Unlike `stop!`, code after `stop_immediately!` in the same step method will NOT execute.
252
+
253
+ ```ruby
254
+ class Payment::Process < ApplicationService
255
+ arg :amount, type: Integer
256
+ arg :card_token, type: String
257
+
258
+ step :validate_card
259
+ step :charge_card
260
+ step :send_receipt
261
+
262
+ output :transaction_id, type: String
263
+
264
+ private
265
+
266
+ def validate_card
267
+ unless valid_card?(card_token)
268
+ errors.add(:card, "is invalid")
269
+ stop_immediately! # Exit immediately - don't run any more code
270
+ end
271
+
272
+ # This code won't run if card is invalid
273
+ log_validation_success
274
+ end
275
+
276
+ def charge_card
277
+ # This step won't run if stop_immediately! was called
278
+ self.transaction_id = PaymentGateway.charge(amount, card_token)
279
+ end
280
+
281
+ def send_receipt
282
+ Mailer.receipt(transaction_id).deliver_later
283
+ end
284
+ end
285
+ ```
286
+
287
+ {% hint style="warning" %}
288
+ `stop_immediately!` raises an internal exception to halt execution. Steps marked with `always: true` will NOT run when `stop_immediately!` is called.
289
+ {% endhint %}
290
+
291
+ {% hint style="success" %}
292
+ **Database Transactions:** Calling `stop_immediately!` does NOT rollback database transactions. All database changes made before `stop_immediately!` was called will be committed.
239
293
  {% endhint %}
240
294
 
241
295
  ## Removing Inherited Steps
data/docs/summary.md CHANGED
@@ -12,6 +12,8 @@
12
12
  * [Configuration](configuration.md)
13
13
  * [Testing](testing.md)
14
14
  * [Rails Generators](generators.md)
15
+ * [RuboCop Integration](rubocop.md)
16
+ * [Ruby LSP Integration](ruby-lsp.md)
15
17
  * [Best Practices](best-practices.md)
16
18
  * [Recipes](recipes.md)
17
19
  * [CRUD](crud.md)
data/docs/testing.md CHANGED
@@ -175,7 +175,7 @@ RSpec.describe User::Register do
175
175
  end
176
176
  ```
177
177
 
178
- ## Testing Early Exit with done!
178
+ ## Testing Early Exit with stop!
179
179
 
180
180
  ```ruby
181
181
  RSpec.describe User::FindOrCreate do
@@ -21,6 +21,27 @@ require "light/services/concerns/parent_service"
21
21
  # Base class for all service objects
22
22
  module Light
23
23
  module Services
24
+ # Base class for building service objects with arguments, outputs, and steps.
25
+ #
26
+ # @example Basic service
27
+ # class CreateUser < Light::Services::Base
28
+ # arg :name, type: String
29
+ # arg :email, type: String
30
+ #
31
+ # output :user, type: User
32
+ #
33
+ # step :create_user
34
+ #
35
+ # private
36
+ #
37
+ # def create_user
38
+ # self.user = User.create!(name: name, email: email)
39
+ # end
40
+ # end
41
+ #
42
+ # result = CreateUser.run(name: "John", email: "john@example.com")
43
+ # result.success? # => true
44
+ # result.user # => #<User id: 1, name: "John">
24
45
  class Base
25
46
  include Callbacks
26
47
  include Dsl::ArgumentsDsl
@@ -30,9 +51,23 @@ module Light
30
51
  include Concerns::StateManagement
31
52
  include Concerns::ParentService
32
53
 
33
- # Getters
34
- attr_reader :outputs, :arguments, :errors, :warnings
54
+ # @return [Collection::Base] collection of output values
55
+ attr_reader :outputs
35
56
 
57
+ # @return [Collection::Base] collection of argument values
58
+ attr_reader :arguments
59
+
60
+ # @return [Messages] collection of error messages
61
+ attr_reader :errors
62
+
63
+ # @return [Messages] collection of warning messages
64
+ attr_reader :warnings
65
+
66
+ # Initialize a new service instance.
67
+ #
68
+ # @param args [Hash] arguments to pass to the service
69
+ # @param config [Hash] runtime configuration overrides
70
+ # @param parent_service [Base, nil] parent service for nested calls
36
71
  def initialize(args = {}, config = {}, parent_service = nil)
37
72
  @config = Light::Services.config.merge(self.class.class_config || {}).merge(config)
38
73
  @parent_service = parent_service
@@ -40,37 +75,70 @@ module Light
40
75
  @outputs = Collection::Base.new(self, CollectionTypes::OUTPUTS)
41
76
  @arguments = Collection::Base.new(self, CollectionTypes::ARGUMENTS, args.dup)
42
77
 
43
- @done = false
78
+ @stopped = false
44
79
  @launched_steps = []
45
80
 
46
81
  initialize_errors
47
82
  initialize_warnings
48
83
  end
49
84
 
85
+ # Check if the service completed without errors.
86
+ #
87
+ # @return [Boolean] true if no errors were added
50
88
  def success?
51
89
  !errors?
52
90
  end
53
91
 
92
+ # Check if the service completed with errors.
93
+ #
94
+ # @return [Boolean] true if any errors were added
54
95
  def failed?
55
96
  errors?
56
97
  end
57
98
 
99
+ # Check if the service has any errors.
100
+ #
101
+ # @return [Boolean] true if errors collection is not empty
58
102
  def errors?
59
103
  @errors.any?
60
104
  end
61
105
 
106
+ # Check if the service has any warnings.
107
+ #
108
+ # @return [Boolean] true if warnings collection is not empty
62
109
  def warnings?
63
110
  @warnings.any?
64
111
  end
65
112
 
66
- def done!
67
- @done = true
113
+ # Stop executing remaining steps after the current step completes.
114
+ #
115
+ # @return [Boolean] true
116
+ def stop!
117
+ @stopped = true
68
118
  end
119
+ alias done! stop!
69
120
 
70
- def done?
71
- @done
121
+ # Check if the service has been stopped.
122
+ #
123
+ # @return [Boolean] true if stop! was called
124
+ def stopped?
125
+ @stopped
126
+ end
127
+ alias done? stopped?
128
+
129
+ # Stop execution immediately, skipping any remaining code in the current step.
130
+ #
131
+ # @raise [StopExecution] always raises to halt execution
132
+ # @return [void]
133
+ def stop_immediately!
134
+ @stopped = true
135
+ raise Light::Services::StopExecution
72
136
  end
73
137
 
138
+ # Execute the service steps.
139
+ #
140
+ # @return [void]
141
+ # @raise [StandardError] re-raises any exception after running always steps
74
142
  def call
75
143
  load_defaults_and_validate
76
144
 
@@ -87,20 +155,54 @@ module Light
87
155
  end
88
156
 
89
157
  class << self
158
+ # @return [Hash, nil] class-level configuration options
90
159
  attr_accessor :class_config
91
160
 
161
+ # Set class-level configuration for this service.
162
+ #
163
+ # @param config [Hash] configuration options
164
+ # @return [Hash] the configuration hash
92
165
  def config(config = {})
93
166
  self.class_config = config
94
167
  end
95
168
 
169
+ # Run the service and return the result.
170
+ #
171
+ # @param args [Hash] arguments to pass to the service
172
+ # @param config [Hash] runtime configuration overrides
173
+ # @return [Base] the executed service instance
174
+ #
175
+ # @example
176
+ # result = MyService.run(name: "test")
177
+ # result.success? # => true
96
178
  def run(args = {}, config = {})
97
179
  new(args, config).tap(&:call)
98
180
  end
99
181
 
182
+ # Run the service and raise an error if it fails.
183
+ #
184
+ # @param args [Hash] arguments to pass to the service
185
+ # @param config [Hash] runtime configuration overrides
186
+ # @return [Base] the executed service instance
187
+ # @raise [Error] if the service fails
188
+ #
189
+ # @example
190
+ # MyService.run!(name: "test") # raises if service fails
100
191
  def run!(args = {}, config = {})
101
192
  run(args, config.merge(raise_on_error: true))
102
193
  end
103
194
 
195
+ # Create a context for running the service with a parent service or config.
196
+ #
197
+ # @param service_or_config [Base, Hash] parent service or configuration hash
198
+ # @param config [Hash] configuration hash (when first param is a service)
199
+ # @return [BaseWithContext] context wrapper for running the service
200
+ #
201
+ # @example With parent service
202
+ # ChildService.with(self).run(data: value)
203
+ #
204
+ # @example With configuration
205
+ # MyService.with(use_transactions: false).run(name: "test")
104
206
  def with(service_or_config = {}, config = {})
105
207
  service = service_or_config.is_a?(Hash) ? nil : service_or_config
106
208
  config = service_or_config unless service
@@ -1,9 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # This class allows running a service object with context (parent class and custom config)
4
3
  module Light
5
4
  module Services
5
+ # Wrapper for running a service with a parent context or custom configuration.
6
+ # Created via {Base.with} method.
7
+ #
8
+ # @example Running with parent service context
9
+ # ChildService.with(self).run(data: value)
10
+ #
11
+ # @example Running with custom configuration
12
+ # MyService.with(use_transactions: false).run(name: "test")
6
13
  class BaseWithContext
14
+ # Initialize a new context wrapper.
15
+ #
16
+ # @param service_class [Class] the service class to run
17
+ # @param parent_service [Base, nil] parent service for error/warning propagation
18
+ # @param config [Hash] configuration overrides
19
+ # @raise [ArgTypeError] if parent_service is not a Base subclass
7
20
  def initialize(service_class, parent_service, config)
8
21
  @service_class = service_class
9
22
  @config = config
@@ -14,10 +27,19 @@ module Light
14
27
  raise Light::Services::ArgTypeError, "#{parent_service.class} - must be a subclass of Light::Services::Base"
15
28
  end
16
29
 
30
+ # Run the service with the configured context.
31
+ #
32
+ # @param args [Hash] arguments to pass to the service
33
+ # @return [Base] the executed service instance
17
34
  def run(args = {})
18
35
  @service_class.new(extend_arguments(args), @config, @parent_service).tap(&:call)
19
36
  end
20
37
 
38
+ # Run the service and raise an error if it fails.
39
+ #
40
+ # @param args [Hash] arguments to pass to the service
41
+ # @return [Base] the executed service instance
42
+ # @raise [Error] if the service fails
21
43
  def run!(args = {})
22
44
  @config[:raise_on_error] = true
23
45
  run(args)
@@ -2,7 +2,38 @@
2
2
 
3
3
  module Light
4
4
  module Services
5
+ # Provides callback hooks for service and step lifecycle events.
6
+ #
7
+ # @example Service-level callbacks
8
+ # class MyService < Light::Services::Base
9
+ # before_service_run :log_start
10
+ # after_service_run { |service| Rails.logger.info("Done!") }
11
+ # on_service_success :send_notification
12
+ # on_service_failure :log_error
13
+ # end
14
+ #
15
+ # @example Step-level callbacks
16
+ # class MyService < Light::Services::Base
17
+ # before_step_run :log_step_start
18
+ # after_step_run { |service, step_name| puts "Finished #{step_name}" }
19
+ # on_step_failure :handle_step_error
20
+ # end
21
+ #
22
+ # @example Around callbacks
23
+ # class MyService < Light::Services::Base
24
+ # around_service_run :with_timing
25
+ #
26
+ # private
27
+ #
28
+ # def with_timing(service)
29
+ # start = Time.now
30
+ # yield
31
+ # puts "Took #{Time.now - start}s"
32
+ # end
33
+ # end
5
34
  module Callbacks
35
+ # Available callback events.
36
+ # @return [Array<Symbol>] list of callback event names
6
37
  EVENTS = [
7
38
  :before_step_run,
8
39
  :after_step_run,
@@ -21,6 +52,20 @@ module Light
21
52
  base.extend(ClassMethods)
22
53
  end
23
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
24
69
  module ClassMethods
25
70
  # Define DSL methods for each callback event
26
71
  EVENTS.each do |event|
@@ -37,13 +82,19 @@ module Light
37
82
  end
38
83
  end
39
84
 
40
- # Get all callbacks for a specific event (including inherited ones)
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
41
89
  def callbacks_for(event)
42
90
  @callbacks ||= {}
43
91
  @callbacks[event] ||= []
44
92
  end
45
93
 
46
- # Get all callbacks including inherited ones
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
47
98
  def all_callbacks_for(event)
48
99
  if superclass.respond_to?(:all_callbacks_for)
49
100
  inherited = superclass.all_callbacks_for(event)
@@ -55,9 +106,12 @@ module Light
55
106
  end
56
107
  end
57
108
 
58
- # Run callbacks for a given event
59
- # For around callbacks, yields to the block
60
- # For other callbacks, just executes them in order
109
+ # Run all callbacks for a given event.
110
+ #
111
+ # @param event [Symbol] the callback event name
112
+ # @param args [Array] arguments to pass to callbacks
113
+ # @yield for around callbacks, the block to wrap
114
+ # @return [void]
61
115
  def run_callbacks(event, *args, &block)
62
116
  callbacks = self.class.all_callbacks_for(event)
63
117
 
@@ -2,15 +2,34 @@
2
2
 
3
3
  require_relative "constants"
4
4
 
5
- # Collection to store arguments and outputs values
6
5
  module Light
7
6
  module Services
7
+ # Collection module for storing argument and output values.
8
8
  module Collection
9
+ # Storage for service arguments or outputs with type validation.
10
+ #
11
+ # @example Accessing values
12
+ # service.arguments[:name] # => "John"
13
+ # service.outputs[:user] # => #<User id: 1>
9
14
  class Base
10
15
  extend Forwardable
11
16
 
17
+ # @!method key?(key)
18
+ # Check if a key exists in the collection.
19
+ # @param key [Symbol] the key to check
20
+ # @return [Boolean] true if key exists
21
+
22
+ # @!method to_h
23
+ # Convert collection to a hash.
24
+ # @return [Hash] the stored values
12
25
  def_delegators :@storage, :key?, :to_h
13
26
 
27
+ # Initialize a new collection.
28
+ #
29
+ # @param instance [Base] the service instance
30
+ # @param collection_type [String] "arguments" or "outputs"
31
+ # @param storage [Hash] initial values
32
+ # @raise [ArgTypeError] if storage is not a Hash
14
33
  def initialize(instance, collection_type, storage = {})
15
34
  validate_collection_type!(collection_type)
16
35
 
@@ -23,22 +42,43 @@ module Light
23
42
  raise Light::Services::ArgTypeError, "#{instance.class} - #{collection_type} must be a Hash"
24
43
  end
25
44
 
45
+ # Set a value in the collection.
46
+ #
47
+ # @param key [Symbol] the key to set
48
+ # @param value [Object] the value to store
49
+ # @return [Object] the stored value
26
50
  def set(key, value)
27
51
  @storage[key] = value
28
52
  end
29
53
 
54
+ # Get a value from the collection.
55
+ #
56
+ # @param key [Symbol] the key to retrieve
57
+ # @return [Object, nil] the stored value or nil
30
58
  def get(key)
31
59
  @storage[key]
32
60
  end
33
61
 
62
+ # Get a value using bracket notation.
63
+ #
64
+ # @param key [Symbol] the key to retrieve
65
+ # @return [Object, nil] the stored value or nil
34
66
  def [](key)
35
67
  get(key)
36
68
  end
37
69
 
70
+ # Set a value using bracket notation.
71
+ #
72
+ # @param key [Symbol] the key to set
73
+ # @param value [Object] the value to store
74
+ # @return [Object] the stored value
38
75
  def []=(key, value)
39
76
  set(key, value)
40
77
  end
41
78
 
79
+ # Load default values for fields that haven't been set.
80
+ #
81
+ # @return [void]
42
82
  def load_defaults
43
83
  settings_collection.each do |name, settings|
44
84
  next if !settings.default_exists || key?(name)
@@ -51,6 +91,10 @@ module Light
51
91
  end
52
92
  end
53
93
 
94
+ # Validate all values against their type definitions.
95
+ #
96
+ # @return [void]
97
+ # @raise [ArgTypeError] if a value fails type validation
54
98
  def validate!
55
99
  settings_collection.each do |name, field|
56
100
  next if field.optional && (!key?(name) || get(name).nil?)
@@ -62,7 +106,11 @@ module Light
62
106
  end
63
107
  end
64
108
 
65
- # Extend args with context values (only for arguments)
109
+ # Extend arguments hash with context values from this collection.
110
+ # Only applies to arguments collections.
111
+ #
112
+ # @param args [Hash] arguments hash to extend
113
+ # @return [Hash] the extended arguments hash
66
114
  def extend_with_context(args)
67
115
  return args unless @collection_type == CollectionTypes::ARGUMENTS
68
116
 
@@ -40,6 +40,9 @@ module Light
40
40
 
41
41
  break if @errors.break? || @warnings.break?
42
42
  end
43
+ rescue Light::Services::StopExecution
44
+ # Gracefully handle stop_immediately! inside transaction to prevent rollback
45
+ @stopped = true
43
46
  end
44
47
  end
45
48