light-services 2.2 → 3.0.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/.github/config/rubocop_linter_action.yml +4 -4
- data/.github/workflows/ci.yml +12 -12
- data/.gitignore +5 -0
- data/.rubocop.yml +77 -7
- data/CHANGELOG.md +23 -0
- data/CLAUDE.md +139 -0
- data/Gemfile +16 -11
- data/Gemfile.lock +53 -27
- data/README.md +76 -13
- data/docs/arguments.md +267 -0
- data/docs/best-practices.md +153 -0
- data/docs/callbacks.md +476 -0
- data/docs/concepts.md +80 -0
- data/docs/configuration.md +168 -0
- data/docs/context.md +128 -0
- data/docs/crud.md +525 -0
- data/docs/errors.md +250 -0
- data/docs/generators.md +250 -0
- data/docs/outputs.md +135 -0
- data/docs/pundit-authorization.md +320 -0
- data/docs/quickstart.md +134 -0
- data/docs/readme.md +100 -0
- data/docs/recipes.md +14 -0
- data/docs/service-rendering.md +222 -0
- data/docs/steps.md +337 -0
- data/docs/summary.md +19 -0
- data/docs/testing.md +549 -0
- data/lib/generators/light_services/install/USAGE +15 -0
- data/lib/generators/light_services/install/install_generator.rb +41 -0
- data/lib/generators/light_services/install/templates/application_service.rb.tt +8 -0
- data/lib/generators/light_services/install/templates/application_service_spec.rb.tt +7 -0
- data/lib/generators/light_services/install/templates/initializer.rb.tt +30 -0
- data/lib/generators/light_services/service/USAGE +21 -0
- data/lib/generators/light_services/service/service_generator.rb +68 -0
- data/lib/generators/light_services/service/templates/service.rb.tt +48 -0
- data/lib/generators/light_services/service/templates/service_spec.rb.tt +40 -0
- data/lib/light/services/base.rb +24 -114
- data/lib/light/services/base_with_context.rb +2 -3
- data/lib/light/services/callbacks.rb +103 -0
- data/lib/light/services/collection.rb +97 -0
- data/lib/light/services/concerns/execution.rb +76 -0
- data/lib/light/services/concerns/parent_service.rb +34 -0
- data/lib/light/services/concerns/state_management.rb +30 -0
- data/lib/light/services/config.rb +4 -18
- data/lib/light/services/constants.rb +97 -0
- data/lib/light/services/dsl/arguments_dsl.rb +84 -0
- data/lib/light/services/dsl/outputs_dsl.rb +80 -0
- data/lib/light/services/dsl/steps_dsl.rb +205 -0
- data/lib/light/services/dsl/validation.rb +132 -0
- data/lib/light/services/exceptions.rb +7 -2
- data/lib/light/services/messages.rb +19 -31
- data/lib/light/services/rspec/matchers/define_argument.rb +174 -0
- data/lib/light/services/rspec/matchers/define_output.rb +147 -0
- data/lib/light/services/rspec/matchers/define_step.rb +225 -0
- data/lib/light/services/rspec/matchers/execute_step.rb +230 -0
- data/lib/light/services/rspec/matchers/have_error_on.rb +148 -0
- data/lib/light/services/rspec/matchers/have_warning_on.rb +148 -0
- data/lib/light/services/rspec/matchers/trigger_callback.rb +138 -0
- data/lib/light/services/rspec.rb +15 -0
- data/lib/light/services/settings/field.rb +86 -0
- data/lib/light/services/settings/step.rb +31 -16
- data/lib/light/services/utils.rb +38 -0
- data/lib/light/services/version.rb +1 -1
- data/lib/light/services.rb +2 -0
- data/light-services.gemspec +6 -8
- metadata +54 -26
- data/lib/light/services/class_based_collection/base.rb +0 -86
- data/lib/light/services/class_based_collection/mount.rb +0 -33
- data/lib/light/services/collection/arguments.rb +0 -34
- data/lib/light/services/collection/base.rb +0 -59
- data/lib/light/services/collection/outputs.rb +0 -16
- data/lib/light/services/settings/argument.rb +0 -68
- data/lib/light/services/settings/output.rb +0 -34
data/docs/errors.md
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# Errors
|
|
2
|
+
|
|
3
|
+
Errors are a natural part of every application. This guide explores how to handle errors within Light Services, drawing parallels to ActiveModel errors.
|
|
4
|
+
|
|
5
|
+
## Error Structure
|
|
6
|
+
|
|
7
|
+
Light Service errors follow a structure similar to ActiveModel errors. Here's a simplified example:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
{
|
|
11
|
+
email: ["must be a valid email"],
|
|
12
|
+
password: ["is too short", "must contain at least one number"]
|
|
13
|
+
}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Adding Errors
|
|
17
|
+
|
|
18
|
+
To add an error to your service, use the `errors.add` method.
|
|
19
|
+
|
|
20
|
+
{% hint style="info" %}
|
|
21
|
+
By default, adding an error marks the service as failed, preventing subsequent steps from executing. This behavior can be customized in the configuration for individual services and errors.
|
|
22
|
+
{% endhint %}
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
class ParsePage < ApplicationService
|
|
26
|
+
# Arguments
|
|
27
|
+
arg :url, type: String
|
|
28
|
+
# ...
|
|
29
|
+
|
|
30
|
+
# Steps
|
|
31
|
+
step :validate
|
|
32
|
+
step :parse
|
|
33
|
+
# ...
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def validate
|
|
38
|
+
# Multiple errors can be added with the same key
|
|
39
|
+
errors.add(:url, "must be a valid URL") unless url.match?(URI::DEFAULT_PARSER.make_regexp)
|
|
40
|
+
errors.add(:url, "must be a secure link") unless url.start_with?("https")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# ...
|
|
44
|
+
end
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Reading Errors
|
|
48
|
+
|
|
49
|
+
To check if a service has errors, you can use the `#failed?` method. You can also use methods like `errors.any?` to inspect errors.
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
class ParsePage < ApplicationService
|
|
53
|
+
def parse
|
|
54
|
+
nodes.each do |node|
|
|
55
|
+
if node.nil? || (node.respond_to?(:empty?) && node.empty?)
|
|
56
|
+
errors.add(:base, "Node #{node} is blank")
|
|
57
|
+
else
|
|
58
|
+
parse_node(node)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
if failed? # or errors.any?
|
|
63
|
+
puts "Not all nodes were parsed"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
You can access errors outside the service using the `#errors` method.
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
service = ParsePage.run(url: "rubygems")
|
|
73
|
+
|
|
74
|
+
if service.failed?
|
|
75
|
+
puts service.errors
|
|
76
|
+
puts service.errors[:url]
|
|
77
|
+
puts service.errors.to_h # Returns errors as a hash
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Adding Warnings
|
|
82
|
+
|
|
83
|
+
Sometimes, you may want to add a warning instead of an error. Warnings are similar to errors but they do not mark the service as failed. By default they also do not stop execution and do not roll back the transaction (both behaviors can be configured globally or per-message).
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
class ParsePage < ApplicationService
|
|
87
|
+
def validate
|
|
88
|
+
errors.add(:url, "must be a valid URL") unless url.match?(URI::DEFAULT_PARSER.make_regexp)
|
|
89
|
+
warnings.add(:url, "should be a secure link") unless url.start_with?("https")
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
service = ParsePage.run(url: "http://rubygems.org")
|
|
96
|
+
|
|
97
|
+
if service.warnings.any?
|
|
98
|
+
puts service.warnings
|
|
99
|
+
puts service.warnings[:url]
|
|
100
|
+
puts service.warnings.to_h # Returns warnings as a hash
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Copying Errors
|
|
105
|
+
|
|
106
|
+
### From ActiveRecord Models
|
|
107
|
+
|
|
108
|
+
Use `errors.copy_from` (or its alias `errors.from_record`) to copy errors from an ActiveRecord model:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
class User::Create < ApplicationService
|
|
112
|
+
def create_user
|
|
113
|
+
self.user = User.new(attributes)
|
|
114
|
+
|
|
115
|
+
unless user.save
|
|
116
|
+
errors.copy_from(user) # Copies all validation errors from the user model
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### From Another Service
|
|
123
|
+
|
|
124
|
+
Copy errors from a child service that wasn't run in the same context:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
class Order::Process < ApplicationService
|
|
128
|
+
def process_payment
|
|
129
|
+
payment_service = Payment::Charge.run(amount:, card:)
|
|
130
|
+
|
|
131
|
+
if payment_service.failed?
|
|
132
|
+
errors.copy_from(payment_service)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Converting Errors to Hash
|
|
139
|
+
|
|
140
|
+
Use `errors.to_h` to get a hash representation of all errors:
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
service = User::Create.run(email: "invalid")
|
|
144
|
+
|
|
145
|
+
if service.failed?
|
|
146
|
+
service.errors.to_h
|
|
147
|
+
# => { email: ["is invalid"], password: ["can't be blank"] }
|
|
148
|
+
end
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Per-Message Options
|
|
152
|
+
|
|
153
|
+
When adding errors, you can control behavior on a per-message basis:
|
|
154
|
+
|
|
155
|
+
### Control Break Behavior
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
def validate
|
|
159
|
+
# This error won't stop subsequent steps from running
|
|
160
|
+
errors.add(:warning_field, "has a minor issue", break: false)
|
|
161
|
+
|
|
162
|
+
# This error WILL stop execution (default behavior)
|
|
163
|
+
errors.add(:critical_field, "is completely invalid")
|
|
164
|
+
end
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Control Rollback Behavior
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
def process
|
|
171
|
+
# This error won't trigger a transaction rollback
|
|
172
|
+
errors.add(:notification, "failed to send", rollback: false)
|
|
173
|
+
|
|
174
|
+
# This error WILL rollback (default behavior when use_transactions is true)
|
|
175
|
+
errors.add(:payment, "failed to process")
|
|
176
|
+
end
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Checking for Errors and Warnings
|
|
180
|
+
|
|
181
|
+
Light Services provides convenient methods to check error/warning states:
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
service = MyService.run(args)
|
|
185
|
+
|
|
186
|
+
# Check if service has any errors
|
|
187
|
+
service.failed? # => true/false
|
|
188
|
+
service.success? # => true/false (opposite of failed?)
|
|
189
|
+
service.errors? # => true/false (same as errors.any?)
|
|
190
|
+
|
|
191
|
+
# Check if service has any warnings
|
|
192
|
+
service.warnings? # => true/false (same as warnings.any?)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
By following these guidelines, you can effectively manage errors and warnings in Light Services, ensuring a smoother and more robust application experience.
|
|
196
|
+
|
|
197
|
+
## Exception Classes
|
|
198
|
+
|
|
199
|
+
Light Services defines several exception classes for different error scenarios:
|
|
200
|
+
|
|
201
|
+
| Exception | Description |
|
|
202
|
+
|-----------|-------------|
|
|
203
|
+
| `Light::Services::Error` | Base exception class for all Light Services errors |
|
|
204
|
+
| `Light::Services::ArgTypeError` | Raised when an argument type validation fails |
|
|
205
|
+
| `Light::Services::ReservedNameError` | Raised when using a reserved name for arguments, outputs, or steps |
|
|
206
|
+
| `Light::Services::InvalidNameError` | Raised when using an invalid name format |
|
|
207
|
+
| `Light::Services::NoStepsError` | Raised when a service has no steps defined and no `run` method |
|
|
208
|
+
|
|
209
|
+
### NoStepsError
|
|
210
|
+
|
|
211
|
+
This exception is raised when you attempt to execute a service that has no steps defined and no `run` method as a fallback:
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
class EmptyService < ApplicationService
|
|
215
|
+
# No steps defined and no run method
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
EmptyService.run # => raises Light::Services::NoStepsError
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
To fix this, either define at least one step or implement a `run` method:
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
224
|
+
# Option 1: Define steps
|
|
225
|
+
class MyService < ApplicationService
|
|
226
|
+
step :do_work
|
|
227
|
+
|
|
228
|
+
private
|
|
229
|
+
|
|
230
|
+
def do_work
|
|
231
|
+
# ...
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Option 2: Use run method
|
|
236
|
+
class MyService < ApplicationService
|
|
237
|
+
private
|
|
238
|
+
|
|
239
|
+
def run
|
|
240
|
+
# ...
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## What's next?
|
|
246
|
+
|
|
247
|
+
Learn about callbacks to add logging, benchmarking, and other cross-cutting concerns to your services.
|
|
248
|
+
|
|
249
|
+
[Next: Callbacks](callbacks.md)
|
|
250
|
+
|
data/docs/generators.md
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# Rails Generators
|
|
2
|
+
|
|
3
|
+
Light Services includes Rails generators to help you quickly set up and create services in your Rails application. These generators follow Rails conventions and integrate seamlessly with your Rails workflow.
|
|
4
|
+
|
|
5
|
+
## Install Generator
|
|
6
|
+
|
|
7
|
+
The install generator sets up Light Services in your Rails application by creating the base `ApplicationService` class and configuration files.
|
|
8
|
+
|
|
9
|
+
### Usage
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bin/rails generate light_services:install
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### What It Creates
|
|
16
|
+
|
|
17
|
+
The install generator creates the following files:
|
|
18
|
+
|
|
19
|
+
1. **`app/services/application_service.rb`** - Base service class for your application
|
|
20
|
+
```ruby
|
|
21
|
+
class ApplicationService < Light::Services::Base
|
|
22
|
+
# Add common arguments, callbacks, or helpers shared across all services.
|
|
23
|
+
#
|
|
24
|
+
# Example: Add a context argument for the current user
|
|
25
|
+
# arg :current_user, type: User, optional: true, context: true
|
|
26
|
+
end
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
2. **`config/initializers/light_services.rb`** - Configuration file (unless `--skip-initializer` is used)
|
|
30
|
+
This file contains the global configuration for Light Services in your Rails application.
|
|
31
|
+
|
|
32
|
+
3. **`spec/services/application_service_spec.rb`** - RSpec test file (if RSpec is detected and `--skip-spec` is not used)
|
|
33
|
+
|
|
34
|
+
### Options
|
|
35
|
+
|
|
36
|
+
- `--skip-initializer` - Skip creating the initializer file
|
|
37
|
+
- `--skip-spec` - Skip creating the spec file
|
|
38
|
+
|
|
39
|
+
### Examples
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Standard installation
|
|
43
|
+
bin/rails generate light_services:install
|
|
44
|
+
|
|
45
|
+
# Skip initializer
|
|
46
|
+
bin/rails generate light_services:install --skip-initializer
|
|
47
|
+
|
|
48
|
+
# Skip spec file
|
|
49
|
+
bin/rails generate light_services:install --skip-spec
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Service Generator
|
|
53
|
+
|
|
54
|
+
The service generator creates a new service class that inherits from `ApplicationService`. It supports namespaced services and can pre-populate arguments, steps, and outputs.
|
|
55
|
+
|
|
56
|
+
### Usage
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
bin/rails generate light_services:service NAME [options]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### What It Creates
|
|
63
|
+
|
|
64
|
+
The service generator creates:
|
|
65
|
+
|
|
66
|
+
1. **Service file** - `app/services/{name}.rb`
|
|
67
|
+
2. **Spec file** - `spec/services/{name}_spec.rb` (if RSpec is detected and `--skip-spec` is not used)
|
|
68
|
+
|
|
69
|
+
### Options
|
|
70
|
+
|
|
71
|
+
- `--args` - List of arguments for the service (space-separated)
|
|
72
|
+
- `--steps` - List of steps for the service (space-separated)
|
|
73
|
+
- `--outputs` - List of outputs for the service (space-separated)
|
|
74
|
+
- `--skip-spec` - Skip creating the spec file
|
|
75
|
+
- `--parent` - Parent class (default: `ApplicationService`)
|
|
76
|
+
|
|
77
|
+
### Examples
|
|
78
|
+
|
|
79
|
+
#### Basic Service
|
|
80
|
+
|
|
81
|
+
Create a simple service without any predefined structure:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
bin/rails generate light_services:service user/create
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
This creates:
|
|
88
|
+
```ruby
|
|
89
|
+
# app/services/user/create.rb
|
|
90
|
+
class User::Create < ApplicationService
|
|
91
|
+
# step :step_a
|
|
92
|
+
# step :step_b
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
# def step_a
|
|
97
|
+
# # TODO: Implement service logic
|
|
98
|
+
# end
|
|
99
|
+
|
|
100
|
+
# def step_b
|
|
101
|
+
# # TODO: Implement service logic
|
|
102
|
+
# end
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
#### Service with Arguments, Steps, and Outputs
|
|
107
|
+
|
|
108
|
+
Create a fully structured service:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
bin/rails generate light_services:service CreateOrder \
|
|
112
|
+
--args=user product quantity \
|
|
113
|
+
--steps=validate_stock create_order send_confirmation \
|
|
114
|
+
--outputs=order
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
This creates:
|
|
118
|
+
```ruby
|
|
119
|
+
# app/services/create_order.rb
|
|
120
|
+
class CreateOrder < ApplicationService
|
|
121
|
+
# Arguments
|
|
122
|
+
arg :user
|
|
123
|
+
arg :product
|
|
124
|
+
arg :quantity
|
|
125
|
+
|
|
126
|
+
# Steps
|
|
127
|
+
step :validate_stock
|
|
128
|
+
step :create_order
|
|
129
|
+
step :send_confirmation
|
|
130
|
+
|
|
131
|
+
# Outputs
|
|
132
|
+
output :order
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
def validate_stock
|
|
137
|
+
# TODO: Implement validate_stock
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def create_order
|
|
141
|
+
# TODO: Implement create_order
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def send_confirmation
|
|
145
|
+
# TODO: Implement send_confirmation
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
#### Namespaced Service
|
|
151
|
+
|
|
152
|
+
Create a service within a namespace:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
bin/rails generate light_services:service payment/process \
|
|
156
|
+
--args=order payment_method \
|
|
157
|
+
--steps=validate_payment charge_card update_order \
|
|
158
|
+
--outputs=transaction
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
This creates:
|
|
162
|
+
```ruby
|
|
163
|
+
# app/services/payment/process.rb
|
|
164
|
+
class Payment::Process < ApplicationService
|
|
165
|
+
# Arguments
|
|
166
|
+
arg :order
|
|
167
|
+
arg :payment_method
|
|
168
|
+
|
|
169
|
+
# Steps
|
|
170
|
+
step :validate_payment
|
|
171
|
+
step :charge_card
|
|
172
|
+
step :update_order
|
|
173
|
+
|
|
174
|
+
# Outputs
|
|
175
|
+
output :transaction
|
|
176
|
+
|
|
177
|
+
private
|
|
178
|
+
|
|
179
|
+
def validate_payment
|
|
180
|
+
# TODO: Implement validate_payment
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def charge_card
|
|
184
|
+
# TODO: Implement charge_card
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def update_order
|
|
188
|
+
# TODO: Implement update_order
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
#### Custom Parent Class
|
|
194
|
+
|
|
195
|
+
Create a service that inherits from a custom parent class:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
bin/rails generate light_services:service admin/reports/generate \
|
|
199
|
+
--parent=AdminService \
|
|
200
|
+
--args=start_date end_date \
|
|
201
|
+
--steps=fetch_data generate_report \
|
|
202
|
+
--outputs=report
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## RSpec Integration
|
|
206
|
+
|
|
207
|
+
Both generators automatically detect if RSpec is installed in your Rails application by checking for the presence of the `spec/` directory. If RSpec is detected, the generators will create corresponding spec files with basic test structure.
|
|
208
|
+
|
|
209
|
+
### Example Spec File
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
# spec/services/user/create_spec.rb
|
|
213
|
+
require "rails_helper"
|
|
214
|
+
|
|
215
|
+
RSpec.describe User::Create do
|
|
216
|
+
describe ".run" do
|
|
217
|
+
it "creates a user" do
|
|
218
|
+
service = described_class.run(...)
|
|
219
|
+
expect(service).to be_success
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
You can skip spec file generation with the `--skip-spec` option:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
bin/rails generate light_services:service user/create --skip-spec
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Best Practices
|
|
232
|
+
|
|
233
|
+
1. **Run the install generator first** - Always run `light_services:install` before creating individual services to set up the base `ApplicationService` class.
|
|
234
|
+
|
|
235
|
+
2. **Use namespaces** - Organize related services under namespaces (e.g., `User::Create`, `Payment::Process`) to keep your services organized.
|
|
236
|
+
|
|
237
|
+
3. **Start with structure** - Use `--args`, `--steps`, and `--outputs` options to create a skeleton for your service, then fill in the implementation.
|
|
238
|
+
|
|
239
|
+
4. **Keep it simple** - Don't over-specify. If you're not sure about the exact steps, create a basic service and add them as you develop.
|
|
240
|
+
|
|
241
|
+
5. **Follow conventions** - Use descriptive names for services that indicate the action being performed (e.g., `CreateOrder`, `User::Authenticate`, `Payment::Refund`).
|
|
242
|
+
|
|
243
|
+
## Next Steps
|
|
244
|
+
|
|
245
|
+
After generating your services, learn more about:
|
|
246
|
+
|
|
247
|
+
- [Arguments](arguments.md) - Define and validate service inputs
|
|
248
|
+
- [Steps](steps.md) - Organize service logic into steps
|
|
249
|
+
- [Outputs](outputs.md) - Define service outputs
|
|
250
|
+
- [Testing](testing.md) - Write comprehensive tests for your services
|
data/docs/outputs.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Outputs
|
|
2
|
+
|
|
3
|
+
Outputs are the results of a service.
|
|
4
|
+
|
|
5
|
+
## TL;DR
|
|
6
|
+
|
|
7
|
+
- Define outputs using the `output` keyword in the service class
|
|
8
|
+
- Outputs can have default values
|
|
9
|
+
- Outputs can be validated by type (validated when the service succeeds)
|
|
10
|
+
|
|
11
|
+
## Define Outputs
|
|
12
|
+
|
|
13
|
+
You define outputs using the `output` keyword in the service class.
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
class AI::Chat < ApplicationService
|
|
17
|
+
output :messages
|
|
18
|
+
output :cost
|
|
19
|
+
end
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Write Outputs
|
|
23
|
+
|
|
24
|
+
Outputs function similarly to instance variables created with `attr_accessor`.
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
class AI::Chat < ApplicationService
|
|
28
|
+
# Steps
|
|
29
|
+
step :chat
|
|
30
|
+
|
|
31
|
+
# Outputs
|
|
32
|
+
output :messages
|
|
33
|
+
output :cost
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def chat
|
|
38
|
+
self.messages = ["Hello!", "Hi, how are you?"]
|
|
39
|
+
self.cost = 0.0013
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
To set outputs programmatically, use the `outputs.set` method or hash syntax.
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
class AI::Chat < ApplicationService
|
|
48
|
+
# ...
|
|
49
|
+
|
|
50
|
+
def chat
|
|
51
|
+
outputs.set(:messages, ["Hello!", "Hi, how are you?"])
|
|
52
|
+
outputs.set(:cost, 0.0013)
|
|
53
|
+
|
|
54
|
+
# Or use hash syntax
|
|
55
|
+
|
|
56
|
+
outputs[:messages] = ["Hello!", "Hi, how are you?"]
|
|
57
|
+
outputs[:cost] = 0.0013
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Type Validation
|
|
63
|
+
|
|
64
|
+
You can specify the type of output using the `type` option. The output type will be validated when the service successfully completes.
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
class AI::Chat < ApplicationService
|
|
68
|
+
output :messages, type: Array
|
|
69
|
+
output :cost, type: Float
|
|
70
|
+
end
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
You can specify multiple allowed types using an array.
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
class AI::Chat < ApplicationService
|
|
77
|
+
output :result, type: [String, Hash]
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### dry-types Support
|
|
82
|
+
|
|
83
|
+
Outputs also support [dry-types](https://dry-rb.org/gems/dry-types) for advanced type validation and coercion.
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
require "dry-types"
|
|
87
|
+
|
|
88
|
+
module Types
|
|
89
|
+
include Dry.Types()
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
class AI::Chat < ApplicationService
|
|
93
|
+
# Strict type validation
|
|
94
|
+
output :messages, type: Types::Strict::Array.of(Types::Hash)
|
|
95
|
+
|
|
96
|
+
# Coercible types - values are coerced on output validation
|
|
97
|
+
output :total_tokens, type: Types::Coercible::Integer
|
|
98
|
+
|
|
99
|
+
# Constrained types
|
|
100
|
+
output :cost, type: Types::Float.constrained(gteq: 0)
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Default Values
|
|
105
|
+
|
|
106
|
+
Set default values for outputs using the `default` option. The default value will be automatically set before the execution of steps.
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
class AI::Chat < ApplicationService
|
|
110
|
+
output :cost, default: 0.0
|
|
111
|
+
end
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Removing Inherited Outputs
|
|
115
|
+
|
|
116
|
+
When inheriting from a parent service, you can remove outputs using `remove_output`:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
class BaseReportService < ApplicationService
|
|
120
|
+
output :report
|
|
121
|
+
output :debug_info
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
class ProductionReportService < BaseReportService
|
|
125
|
+
# Don't expose debug info in production
|
|
126
|
+
remove_output :debug_info
|
|
127
|
+
end
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## What's Next?
|
|
131
|
+
|
|
132
|
+
Next, learn about context.
|
|
133
|
+
|
|
134
|
+
[Next: Context](context.md)
|
|
135
|
+
|