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/arguments.md
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# Arguments
|
|
2
|
+
|
|
3
|
+
Arguments are the inputs to a service. They are passed to the service when it is invoked.
|
|
4
|
+
|
|
5
|
+
## TL;DR
|
|
6
|
+
|
|
7
|
+
- Define arguments with the `arg` keyword in the service class
|
|
8
|
+
- Validate arguments by type
|
|
9
|
+
- Specify arguments as required or optional
|
|
10
|
+
- Set default values for arguments
|
|
11
|
+
- Access arguments like instance variables
|
|
12
|
+
- Use predicate methods for arguments
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
class User::Charge < ApplicationService
|
|
16
|
+
arg :user, type: User
|
|
17
|
+
arg :amount, type: Float
|
|
18
|
+
arg :send_receipt, type: [TrueClass, FalseClass], default: true
|
|
19
|
+
# In Rails you might prefer `Date.current`.
|
|
20
|
+
arg :invoice_date, type: Date, default: -> { Date.today }
|
|
21
|
+
|
|
22
|
+
step :send_email_receipt, if: :send_receipt?
|
|
23
|
+
|
|
24
|
+
# ...
|
|
25
|
+
end
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Define Arguments
|
|
29
|
+
|
|
30
|
+
Arguments are defined using the `arg` keyword in the service class.
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
class HappyBirthdayService < ApplicationService
|
|
34
|
+
arg :name
|
|
35
|
+
arg :age
|
|
36
|
+
end
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Type Validation
|
|
40
|
+
|
|
41
|
+
Arguments can be validated by type.
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
class HappyBirthdayService < ApplicationService
|
|
45
|
+
arg :name, type: String
|
|
46
|
+
arg :age, type: Integer
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
You can specify multiple allowed types using an array.
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
class HappyBirthdayService < ApplicationService
|
|
54
|
+
arg :name, type: [String, Symbol]
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### dry-types Support
|
|
59
|
+
|
|
60
|
+
Light Services supports [dry-types](https://dry-rb.org/gems/dry-types) for advanced type validation and coercion. When using dry-types, values are automatically coerced to the expected type.
|
|
61
|
+
|
|
62
|
+
First, set up your types module:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
require "dry-types"
|
|
66
|
+
|
|
67
|
+
module Types
|
|
68
|
+
include Dry.Types()
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Then use dry-types in your service arguments:
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
class User::Create < ApplicationService
|
|
76
|
+
# Strict types - must match exactly
|
|
77
|
+
arg :name, type: Types::Strict::String
|
|
78
|
+
|
|
79
|
+
# Coercible types - automatically convert values
|
|
80
|
+
arg :age, type: Types::Coercible::Integer
|
|
81
|
+
|
|
82
|
+
# Constrained types - add validation rules
|
|
83
|
+
arg :email, type: Types::String.constrained(format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i)
|
|
84
|
+
|
|
85
|
+
# Enum types - restrict to specific values
|
|
86
|
+
arg :status, type: Types::String.enum("active", "inactive", "pending")
|
|
87
|
+
|
|
88
|
+
# Array types with element validation
|
|
89
|
+
arg :tags, type: Types::Array.of(Types::String)
|
|
90
|
+
|
|
91
|
+
# Hash schemas
|
|
92
|
+
arg :metadata, type: Types::Hash.schema(key: Types::String)
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Coercion Example:**
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
# With coercible types, string "25" is automatically converted to integer 25
|
|
100
|
+
service = User::Create.run(name: "John", age: "25")
|
|
101
|
+
service.age # => 25 (Integer, not String)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Required Arguments
|
|
105
|
+
|
|
106
|
+
By default, arguments are required. You can make them optional by setting `optional` to `true`.
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
class HappyBirthdayService < ApplicationService
|
|
110
|
+
arg :name, type: String
|
|
111
|
+
arg :age, type: Integer, optional: true
|
|
112
|
+
end
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Default Values
|
|
116
|
+
|
|
117
|
+
Set a default value for an argument to make it optional.
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
class HappyBirthdayService < ApplicationService
|
|
121
|
+
arg :name, type: String
|
|
122
|
+
arg :age, type: Integer, default: 18
|
|
123
|
+
end
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Complex Default Values
|
|
127
|
+
|
|
128
|
+
Default values are deep duplicated when the service is invoked, making it safe to use mutable objects.
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
arg :options, type: Hash, default: { a: 1, b: 2 }
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Procs as Default Values
|
|
135
|
+
|
|
136
|
+
Use procs for dynamic default values.
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
arg :current_date, type: Date, default: -> { Date.current }
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Inheritance
|
|
143
|
+
|
|
144
|
+
Arguments are inherited from parent classes.
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
# UpdateRecordService
|
|
148
|
+
class UpdateRecordService < ApplicationService
|
|
149
|
+
# Arguments
|
|
150
|
+
arg :record, type: ApplicationRecord
|
|
151
|
+
arg :attributes, type: Hash
|
|
152
|
+
|
|
153
|
+
# Steps
|
|
154
|
+
step :authorize
|
|
155
|
+
step :update_record
|
|
156
|
+
end
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
# User::Update inherited from UpdateRecordService
|
|
161
|
+
class User::Update < UpdateRecordService
|
|
162
|
+
# Nothing to do here
|
|
163
|
+
# Arguments and steps are inherited from UpdateRecordService
|
|
164
|
+
end
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Removing Inherited Arguments
|
|
168
|
+
|
|
169
|
+
To remove an inherited argument, use `remove_arg`:
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
class BaseService < ApplicationService
|
|
173
|
+
arg :current_user, type: User
|
|
174
|
+
arg :audit_log, type: [TrueClass, FalseClass], default: true
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
class SystemTaskService < BaseService
|
|
178
|
+
# System tasks don't need a current_user
|
|
179
|
+
remove_arg :current_user
|
|
180
|
+
end
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Context Arguments
|
|
184
|
+
|
|
185
|
+
Context arguments are automatically passed to all child services in the same context. Define them using the `context` option. This is useful for passing objects like `current_user`.
|
|
186
|
+
|
|
187
|
+
Learn more about context in the [Context documentation](context.md).
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
class ApplicationService < Light::Services::Base
|
|
191
|
+
arg :current_user, type: User, optional: true, context: true
|
|
192
|
+
end
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Accessing Arguments
|
|
196
|
+
|
|
197
|
+
Arguments are accessible like instance variables, similar to `attr_accessor`.
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
class HappyBirthdayService < ApplicationService
|
|
201
|
+
# Arguments
|
|
202
|
+
arg :name, type: String
|
|
203
|
+
arg :age, type: Integer
|
|
204
|
+
|
|
205
|
+
# Steps
|
|
206
|
+
step :greet
|
|
207
|
+
|
|
208
|
+
private
|
|
209
|
+
|
|
210
|
+
def greet
|
|
211
|
+
puts "Happy birthday, #{name}! You are #{age} years old."
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Accessing Arguments Using `arguments`
|
|
217
|
+
|
|
218
|
+
For dynamic access or to avoid conflicts, use the `arguments` method.
|
|
219
|
+
|
|
220
|
+
```ruby
|
|
221
|
+
class HappyBirthdayService < ApplicationService
|
|
222
|
+
# Arguments
|
|
223
|
+
arg :name, type: String
|
|
224
|
+
arg :age, type: Integer
|
|
225
|
+
|
|
226
|
+
# Steps
|
|
227
|
+
step :greet
|
|
228
|
+
|
|
229
|
+
private
|
|
230
|
+
|
|
231
|
+
def greet
|
|
232
|
+
name = arguments[:name] # or arguments.get(:name)
|
|
233
|
+
age = arguments[:age] # or arguments.get(:age)
|
|
234
|
+
|
|
235
|
+
puts "Happy birthday, #{name}! You are #{age} years old."
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Argument Predicate Methods
|
|
241
|
+
|
|
242
|
+
Predicate methods are automatically generated for each argument, allowing you to check if an argument is `true` or `false`.
|
|
243
|
+
|
|
244
|
+
```ruby
|
|
245
|
+
class User::GenerateInvoice < ApplicationService
|
|
246
|
+
# Arguments
|
|
247
|
+
arg :user, type: User
|
|
248
|
+
arg :charge, type: [TrueClass, FalseClass], default: false
|
|
249
|
+
|
|
250
|
+
# Steps
|
|
251
|
+
step :generate_invoice
|
|
252
|
+
step :charge_user, if: :charge?
|
|
253
|
+
|
|
254
|
+
# ...
|
|
255
|
+
end
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
{% hint style="info" %}
|
|
259
|
+
The predicate methods return `true` or `false` based on Ruby's convention: `nil` and `false` are `false`, everything else is `true`.
|
|
260
|
+
{% endhint %}
|
|
261
|
+
|
|
262
|
+
## What's Next?
|
|
263
|
+
|
|
264
|
+
Next step is `steps` (I love this pun). Steps are the building blocks of a service, the methods that do the actual work.
|
|
265
|
+
|
|
266
|
+
[Next: Steps](steps.md)
|
|
267
|
+
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Best Practices
|
|
2
|
+
|
|
3
|
+
This guide explores best practices for building applications with Light Services, keeping things simple and effective.
|
|
4
|
+
|
|
5
|
+
## Create Top-Level Services
|
|
6
|
+
|
|
7
|
+
Creating top-level services for your application is highly recommended. This approach helps keep your services small and focused on a single task.
|
|
8
|
+
|
|
9
|
+
### Application Service
|
|
10
|
+
|
|
11
|
+
`ApplicationService` serves as the base class for all services in your application. Use it to place common methods, helpers, context arguments, etc. Remember, it should not contain any business logic.
|
|
12
|
+
|
|
13
|
+
### Create, Update, and Destroy Services
|
|
14
|
+
|
|
15
|
+
Since create, update, and destroy are fundamental operations in any application, having dedicated services for them is a good idea. This keeps important tasks like authorization, data sanitization, and WebSocket broadcasts close to the core of your application.
|
|
16
|
+
|
|
17
|
+
- `CreateRecordService` - for creating records
|
|
18
|
+
- `UpdateRecordService` - for updating records
|
|
19
|
+
- `DestroyRecordService` - for destroying records
|
|
20
|
+
|
|
21
|
+
Think of these services as wrappers around the `ActiveRecord::Base#create`, `#update`, and `#destroy` methods.
|
|
22
|
+
|
|
23
|
+
### Read Services
|
|
24
|
+
|
|
25
|
+
Similar to the above services but focused on finding records. Use these for generic authorization, filtering, sorting, pagination, etc.
|
|
26
|
+
|
|
27
|
+
- `FindRecordService` - for finding a single record
|
|
28
|
+
- `FindAllRecordsService` - for finding multiple records
|
|
29
|
+
|
|
30
|
+
## Avoid Defining Context Arguments Outside Top-Level Services
|
|
31
|
+
|
|
32
|
+
Using context arguments outside of top-level services can make your services less modular and more unpredictable. Keep them within the core services for better modularity.
|
|
33
|
+
|
|
34
|
+
## Keep Services Small
|
|
35
|
+
|
|
36
|
+
Aim to keep your services small and focused on a single task. Ideally, a service should have no more than 3-5 steps. If a service has more steps, consider splitting it into multiple services.
|
|
37
|
+
|
|
38
|
+
## Passing Arguments from Controllers
|
|
39
|
+
|
|
40
|
+
It's a good practice to create a wrapper method to extend arguments passed to the service from the controller.
|
|
41
|
+
|
|
42
|
+
Consider this example controller:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
class PostsController < ApplicationController
|
|
46
|
+
def index
|
|
47
|
+
service = Post::FindAll.run(current_user:, current_organization:)
|
|
48
|
+
render json: service.posts
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def create
|
|
52
|
+
service = Post::Create.run(attributes: params[:post], current_user:, current_organization:)
|
|
53
|
+
|
|
54
|
+
if service.success?
|
|
55
|
+
render json: service.post
|
|
56
|
+
else
|
|
57
|
+
render json: { errors: service.errors }, status: :unprocessable_entity
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def unpublish
|
|
62
|
+
service = Post::Unpublish.run(id: params[:id], current_user:, current_organization:)
|
|
63
|
+
|
|
64
|
+
if service.success?
|
|
65
|
+
render json: service.post
|
|
66
|
+
else
|
|
67
|
+
render json: { errors: service.errors }, status: :unprocessable_entity
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# ...
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Manually passing `current_user` and `current_organization` each time can be cumbersome. Let's simplify it with a helper method in our `ApplicationController`:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
class ApplicationController < ActionController::API
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def service_args(hash = {})
|
|
82
|
+
hash.reverse_merge(
|
|
83
|
+
current_user:,
|
|
84
|
+
current_organization:,
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Now we can refactor our controller:
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
class PostsController < ApplicationController
|
|
94
|
+
def index
|
|
95
|
+
service = Post::FindAll.run(service_args)
|
|
96
|
+
render json: service.posts
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def create
|
|
100
|
+
service = Post::Create.run(service_args(attributes: params[:post]))
|
|
101
|
+
|
|
102
|
+
if service.success?
|
|
103
|
+
render json: service.post
|
|
104
|
+
else
|
|
105
|
+
render json: { errors: service.errors }, status: :unprocessable_entity
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def unpublish
|
|
110
|
+
service = Post::Unpublish.run(service_args(id: params[:id]))
|
|
111
|
+
|
|
112
|
+
if service.success?
|
|
113
|
+
render json: service.post
|
|
114
|
+
else
|
|
115
|
+
render json: { errors: service.errors }, status: :unprocessable_entity
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# ...
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
With this setup, adding a new top-level context argument only requires a change to the `service_args` method in `ApplicationController`.
|
|
124
|
+
|
|
125
|
+
## Use Concerns
|
|
126
|
+
|
|
127
|
+
If you have common logic that you want to share between services, use concerns. Avoid putting too much logic into your `ApplicationService` class; it's better to split it into concerns.
|
|
128
|
+
|
|
129
|
+
For example, create an `AuthorizeUser` concern for authorization logic.
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
# app/services/concerns/authorize_user.rb
|
|
133
|
+
module AuthorizeUser
|
|
134
|
+
extend ActiveSupport::Concern
|
|
135
|
+
|
|
136
|
+
included do
|
|
137
|
+
# ...
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
# app/services/application_service.rb
|
|
144
|
+
class ApplicationService < Light::Services::Base
|
|
145
|
+
include AuthorizeUser
|
|
146
|
+
end
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## What's Next?
|
|
150
|
+
|
|
151
|
+
Explore practical recipes for common patterns:
|
|
152
|
+
|
|
153
|
+
[Next: Recipes](recipes.md)
|