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.
- checksums.yaml +4 -4
- data/.rubocop.yml +7 -1
- data/CHANGELOG.md +15 -0
- data/CLAUDE.md +1 -1
- data/Gemfile.lock +1 -1
- data/README.md +11 -11
- data/docs/arguments.md +23 -0
- data/docs/concepts.md +2 -2
- data/docs/configuration.md +36 -0
- data/docs/errors.md +31 -1
- data/docs/outputs.md +23 -0
- data/docs/quickstart.md +1 -1
- data/docs/readme.md +12 -11
- data/docs/rubocop.md +285 -0
- data/docs/ruby-lsp.md +133 -0
- data/docs/steps.md +62 -8
- data/docs/summary.md +2 -0
- data/docs/testing.md +1 -1
- data/lib/light/services/base.rb +109 -7
- data/lib/light/services/base_with_context.rb +23 -1
- data/lib/light/services/callbacks.rb +59 -5
- data/lib/light/services/collection.rb +50 -2
- data/lib/light/services/concerns/execution.rb +3 -0
- data/lib/light/services/config.rb +83 -3
- data/lib/light/services/constants.rb +3 -0
- data/lib/light/services/dsl/arguments_dsl.rb +1 -0
- data/lib/light/services/dsl/outputs_dsl.rb +1 -0
- data/lib/light/services/dsl/validation.rb +30 -0
- data/lib/light/services/exceptions.rb +19 -1
- data/lib/light/services/message.rb +28 -3
- data/lib/light/services/messages.rb +74 -2
- data/lib/light/services/rubocop/cop/light_services/argument_type_required.rb +52 -0
- data/lib/light/services/rubocop/cop/light_services/condition_method_exists.rb +173 -0
- data/lib/light/services/rubocop/cop/light_services/deprecated_methods.rb +113 -0
- data/lib/light/services/rubocop/cop/light_services/dsl_order.rb +176 -0
- data/lib/light/services/rubocop/cop/light_services/missing_private_keyword.rb +102 -0
- data/lib/light/services/rubocop/cop/light_services/no_direct_instantiation.rb +66 -0
- data/lib/light/services/rubocop/cop/light_services/output_type_required.rb +52 -0
- data/lib/light/services/rubocop/cop/light_services/step_method_exists.rb +109 -0
- data/lib/light/services/rubocop.rb +12 -0
- data/lib/light/services/settings/field.rb +33 -5
- data/lib/light/services/settings/step.rb +23 -5
- data/lib/light/services/version.rb +1 -1
- data/lib/ruby_lsp/light_services/addon.rb +36 -0
- data/lib/ruby_lsp/light_services/definition.rb +132 -0
- data/lib/ruby_lsp/light_services/indexing_enhancement.rb +263 -0
- 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 `
|
|
190
|
+
## Early Exit with `stop!`
|
|
191
191
|
|
|
192
|
-
Use `
|
|
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
|
-
|
|
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 `
|
|
222
|
+
You can check if `stop!` was called using `stopped?`:
|
|
223
223
|
|
|
224
224
|
```ruby
|
|
225
225
|
def some_step
|
|
226
|
-
|
|
226
|
+
stop!
|
|
227
227
|
|
|
228
228
|
# This code still runs within the same step
|
|
229
|
-
puts "
|
|
229
|
+
puts "Stopped? #{stopped?}" # => "Stopped? true"
|
|
230
230
|
end
|
|
231
231
|
|
|
232
232
|
def next_step
|
|
233
|
-
# This step will NOT run because
|
|
233
|
+
# This step will NOT run because stop! was called
|
|
234
234
|
end
|
|
235
235
|
```
|
|
236
236
|
|
|
237
237
|
{% hint style="info" %}
|
|
238
|
-
`
|
|
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
data/lib/light/services/base.rb
CHANGED
|
@@ -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
|
-
#
|
|
34
|
-
attr_reader :outputs
|
|
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
|
-
@
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
|
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
|
-
#
|
|
60
|
-
#
|
|
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
|
|
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
|
|