light-services 3.0.0 → 3.1.1

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 +21 -0
  4. data/CLAUDE.md +1 -1
  5. data/Gemfile.lock +1 -1
  6. data/README.md +11 -11
  7. data/docs/{readme.md → README.md} +12 -11
  8. data/docs/{summary.md → SUMMARY.md} +11 -1
  9. data/docs/arguments.md +23 -0
  10. data/docs/concepts.md +19 -19
  11. data/docs/configuration.md +36 -0
  12. data/docs/errors.md +31 -1
  13. data/docs/outputs.md +23 -0
  14. data/docs/quickstart.md +1 -1
  15. data/docs/rubocop.md +285 -0
  16. data/docs/ruby-lsp.md +133 -0
  17. data/docs/steps.md +62 -8
  18. data/docs/testing.md +1 -1
  19. data/lib/light/services/base.rb +110 -7
  20. data/lib/light/services/base_with_context.rb +23 -1
  21. data/lib/light/services/callbacks.rb +293 -41
  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 +17 -3
data/docs/rubocop.md ADDED
@@ -0,0 +1,285 @@
1
+ # RuboCop Integration
2
+
3
+ Light Services provides custom RuboCop cops to help enforce best practices in your service definitions.
4
+
5
+ ## Setup
6
+
7
+ Add this to your `.rubocop.yml`:
8
+
9
+ ```yaml
10
+ require:
11
+ - light/services/rubocop
12
+ ```
13
+
14
+ ## Available Cops
15
+
16
+ ### LightServices/ArgumentTypeRequired
17
+
18
+ Ensures all `arg` declarations include a `type:` option.
19
+
20
+ ```ruby
21
+ # bad
22
+ arg :user_id
23
+ arg :params, default: {}
24
+
25
+ # good
26
+ arg :user_id, type: Integer
27
+ arg :params, type: Hash, default: {}
28
+ ```
29
+
30
+ ### LightServices/OutputTypeRequired
31
+
32
+ Ensures all `output` declarations include a `type:` option.
33
+
34
+ ```ruby
35
+ # bad
36
+ output :result
37
+ output :data, optional: true
38
+
39
+ # good
40
+ output :result, type: Hash
41
+ output :data, type: Hash, optional: true
42
+ ```
43
+
44
+ ### LightServices/StepMethodExists
45
+
46
+ Ensures all `step` declarations have a corresponding method defined.
47
+
48
+ ```ruby
49
+ # bad
50
+ class MyService < ApplicationService
51
+ step :validate
52
+ step :process # missing method
53
+
54
+ private
55
+
56
+ def validate; end
57
+ end
58
+
59
+ # good
60
+ class MyService < ApplicationService
61
+ step :validate
62
+ step :process
63
+
64
+ private
65
+
66
+ def validate; end
67
+ def process; end
68
+ end
69
+ ```
70
+
71
+ **Configuration:** Use `ExcludedSteps` for inherited steps:
72
+
73
+ ```yaml
74
+ LightServices/StepMethodExists:
75
+ ExcludedSteps:
76
+ - initialize_entity
77
+ - assign_attributes
78
+ ```
79
+
80
+ ### LightServices/ConditionMethodExists
81
+
82
+ Ensures symbol conditions (`:if`, `:unless`) have corresponding methods defined.
83
+
84
+ This cop automatically recognizes predicate methods generated by `arg` and `output` declarations (e.g., `arg :user` creates `user?`).
85
+
86
+ ```ruby
87
+ # bad
88
+ class MyService < ApplicationService
89
+ step :notify, if: :should_notify? # missing method
90
+
91
+ private
92
+
93
+ def notify; end
94
+ end
95
+
96
+ # good - explicit method
97
+ class MyService < ApplicationService
98
+ step :notify, if: :should_notify?
99
+
100
+ private
101
+
102
+ def notify; end
103
+ def should_notify?; true; end
104
+ end
105
+
106
+ # good - predicate from arg/output
107
+ class MyService < ApplicationService
108
+ arg :user, type: User, optional: true
109
+
110
+ step :greet, if: :user? # user? is auto-generated
111
+
112
+ private
113
+
114
+ def greet; end
115
+ end
116
+ ```
117
+
118
+ **Configuration:** Use `ExcludedMethods` for inherited condition methods:
119
+
120
+ ```yaml
121
+ LightServices/ConditionMethodExists:
122
+ ExcludedMethods:
123
+ - admin?
124
+ - guest?
125
+ ```
126
+
127
+ ### LightServices/DslOrder
128
+
129
+ Enforces consistent ordering of DSL declarations: `config` → `arg` → `step` → `output`
130
+
131
+ ```ruby
132
+ # bad
133
+ class MyService < ApplicationService
134
+ step :process
135
+ arg :name, type: String
136
+ config raise_on_error: true
137
+ end
138
+
139
+ # good
140
+ class MyService < ApplicationService
141
+ config raise_on_error: true
142
+
143
+ arg :name, type: String
144
+
145
+ step :process
146
+
147
+ output :result, type: Hash
148
+ end
149
+ ```
150
+
151
+ ### LightServices/MissingPrivateKeyword
152
+
153
+ Ensures step methods are defined as private.
154
+
155
+ ```ruby
156
+ # bad
157
+ class MyService < ApplicationService
158
+ step :process
159
+
160
+ def process # should be private
161
+ # implementation
162
+ end
163
+ end
164
+
165
+ # good
166
+ class MyService < ApplicationService
167
+ step :process
168
+
169
+ private
170
+
171
+ def process
172
+ # implementation
173
+ end
174
+ end
175
+ ```
176
+
177
+ ### LightServices/NoDirectInstantiation
178
+
179
+ Prevents direct instantiation of service classes with `.new`.
180
+
181
+ ```ruby
182
+ # bad
183
+ UserService.new(name: "John")
184
+
185
+ # good
186
+ UserService.run(name: "John")
187
+ UserService.run!(name: "John")
188
+ UserService.call(name: "John")
189
+ ```
190
+
191
+ **Configuration:** Customize the pattern for service class detection:
192
+
193
+ ```yaml
194
+ LightServices/NoDirectInstantiation:
195
+ ServicePattern: 'Service$' # default: matches classes ending with "Service"
196
+ ```
197
+
198
+ ### LightServices/DeprecatedMethods
199
+
200
+ Detects deprecated `done!` and `done?` method calls and suggests using `stop!` and `stopped?` instead. Includes autocorrection.
201
+
202
+ ```ruby
203
+ # bad
204
+ class MyService < ApplicationService
205
+ step :process
206
+
207
+ private
208
+
209
+ def process
210
+ done! if condition_met?
211
+ return if done?
212
+ end
213
+ end
214
+
215
+ # good
216
+ class MyService < ApplicationService
217
+ step :process
218
+
219
+ private
220
+
221
+ def process
222
+ stop! if condition_met?
223
+ return if stopped?
224
+ end
225
+ end
226
+ ```
227
+
228
+ **Configuration:** Customize the pattern for service class detection:
229
+
230
+ ```yaml
231
+ LightServices/DeprecatedMethods:
232
+ ServicePattern: 'Service$' # default: matches classes ending with "Service"
233
+ ```
234
+
235
+ ## Configuration
236
+
237
+ Full configuration example:
238
+
239
+ ```yaml
240
+ require:
241
+ - light/services/rubocop
242
+
243
+ LightServices/ArgumentTypeRequired:
244
+ Enabled: true
245
+
246
+ LightServices/OutputTypeRequired:
247
+ Enabled: true
248
+
249
+ LightServices/StepMethodExists:
250
+ Enabled: true
251
+ ExcludedSteps: []
252
+
253
+ LightServices/ConditionMethodExists:
254
+ Enabled: true
255
+ ExcludedMethods: []
256
+
257
+ LightServices/DslOrder:
258
+ Enabled: true
259
+
260
+ LightServices/MissingPrivateKeyword:
261
+ Enabled: true
262
+
263
+ LightServices/NoDirectInstantiation:
264
+ Enabled: true
265
+ ServicePattern: 'Service$'
266
+
267
+ LightServices/DeprecatedMethods:
268
+ Enabled: true
269
+ ServicePattern: 'Service$'
270
+ ```
271
+
272
+ To disable a cop for specific files:
273
+
274
+ ```yaml
275
+ LightServices/ArgumentTypeRequired:
276
+ Exclude:
277
+ - 'spec/**/*'
278
+ - 'test/**/*'
279
+ ```
280
+
281
+ ## What's Next?
282
+
283
+ Learn more about testing your services:
284
+
285
+ [Next: Testing](testing.md)
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/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,7 +21,29 @@ 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
46
+ extend CallbackDsl
25
47
  include Callbacks
26
48
  include Dsl::ArgumentsDsl
27
49
  include Dsl::OutputsDsl
@@ -30,9 +52,23 @@ module Light
30
52
  include Concerns::StateManagement
31
53
  include Concerns::ParentService
32
54
 
33
- # Getters
34
- attr_reader :outputs, :arguments, :errors, :warnings
55
+ # @return [Collection::Base] collection of output values
56
+ attr_reader :outputs
35
57
 
58
+ # @return [Collection::Base] collection of argument values
59
+ attr_reader :arguments
60
+
61
+ # @return [Messages] collection of error messages
62
+ attr_reader :errors
63
+
64
+ # @return [Messages] collection of warning messages
65
+ attr_reader :warnings
66
+
67
+ # Initialize a new service instance.
68
+ #
69
+ # @param args [Hash] arguments to pass to the service
70
+ # @param config [Hash] runtime configuration overrides
71
+ # @param parent_service [Base, nil] parent service for nested calls
36
72
  def initialize(args = {}, config = {}, parent_service = nil)
37
73
  @config = Light::Services.config.merge(self.class.class_config || {}).merge(config)
38
74
  @parent_service = parent_service
@@ -40,37 +76,70 @@ module Light
40
76
  @outputs = Collection::Base.new(self, CollectionTypes::OUTPUTS)
41
77
  @arguments = Collection::Base.new(self, CollectionTypes::ARGUMENTS, args.dup)
42
78
 
43
- @done = false
79
+ @stopped = false
44
80
  @launched_steps = []
45
81
 
46
82
  initialize_errors
47
83
  initialize_warnings
48
84
  end
49
85
 
86
+ # Check if the service completed without errors.
87
+ #
88
+ # @return [Boolean] true if no errors were added
50
89
  def success?
51
90
  !errors?
52
91
  end
53
92
 
93
+ # Check if the service completed with errors.
94
+ #
95
+ # @return [Boolean] true if any errors were added
54
96
  def failed?
55
97
  errors?
56
98
  end
57
99
 
100
+ # Check if the service has any errors.
101
+ #
102
+ # @return [Boolean] true if errors collection is not empty
58
103
  def errors?
59
104
  @errors.any?
60
105
  end
61
106
 
107
+ # Check if the service has any warnings.
108
+ #
109
+ # @return [Boolean] true if warnings collection is not empty
62
110
  def warnings?
63
111
  @warnings.any?
64
112
  end
65
113
 
66
- def done!
67
- @done = true
114
+ # Stop executing remaining steps after the current step completes.
115
+ #
116
+ # @return [Boolean] true
117
+ def stop!
118
+ @stopped = true
68
119
  end
120
+ alias done! stop!
69
121
 
70
- def done?
71
- @done
122
+ # Check if the service has been stopped.
123
+ #
124
+ # @return [Boolean] true if stop! was called
125
+ def stopped?
126
+ @stopped
127
+ end
128
+ alias done? stopped?
129
+
130
+ # Stop execution immediately, skipping any remaining code in the current step.
131
+ #
132
+ # @raise [StopExecution] always raises to halt execution
133
+ # @return [void]
134
+ def stop_immediately!
135
+ @stopped = true
136
+ raise Light::Services::StopExecution
72
137
  end
73
138
 
139
+ # Execute the service steps.
140
+ #
141
+ # @return [void]
142
+ # @raise [StandardError] re-raises any exception after running always steps
74
143
  def call
75
144
  load_defaults_and_validate
76
145
 
@@ -87,20 +156,54 @@ module Light
87
156
  end
88
157
 
89
158
  class << self
159
+ # @return [Hash, nil] class-level configuration options
90
160
  attr_accessor :class_config
91
161
 
162
+ # Set class-level configuration for this service.
163
+ #
164
+ # @param config [Hash] configuration options
165
+ # @return [Hash] the configuration hash
92
166
  def config(config = {})
93
167
  self.class_config = config
94
168
  end
95
169
 
170
+ # Run the service and return the result.
171
+ #
172
+ # @param args [Hash] arguments to pass to the service
173
+ # @param config [Hash] runtime configuration overrides
174
+ # @return [Base] the executed service instance
175
+ #
176
+ # @example
177
+ # result = MyService.run(name: "test")
178
+ # result.success? # => true
96
179
  def run(args = {}, config = {})
97
180
  new(args, config).tap(&:call)
98
181
  end
99
182
 
183
+ # Run the service and raise an error if it fails.
184
+ #
185
+ # @param args [Hash] arguments to pass to the service
186
+ # @param config [Hash] runtime configuration overrides
187
+ # @return [Base] the executed service instance
188
+ # @raise [Error] if the service fails
189
+ #
190
+ # @example
191
+ # MyService.run!(name: "test") # raises if service fails
100
192
  def run!(args = {}, config = {})
101
193
  run(args, config.merge(raise_on_error: true))
102
194
  end
103
195
 
196
+ # Create a context for running the service with a parent service or config.
197
+ #
198
+ # @param service_or_config [Base, Hash] parent service or configuration hash
199
+ # @param config [Hash] configuration hash (when first param is a service)
200
+ # @return [BaseWithContext] context wrapper for running the service
201
+ #
202
+ # @example With parent service
203
+ # ChildService.with(self).run(data: value)
204
+ #
205
+ # @example With configuration
206
+ # MyService.with(use_transactions: false).run(name: "test")
104
207
  def with(service_or_config = {}, config = {})
105
208
  service = service_or_config.is_a?(Hash) ? nil : service_or_config
106
209
  config = service_or_config unless service