light-services 2.2.1 → 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/.github/config/rubocop_linter_action.yml +4 -4
- data/.github/workflows/ci.yml +12 -12
- data/.gitignore +1 -0
- data/.rubocop.yml +83 -7
- data/CHANGELOG.md +38 -0
- data/CLAUDE.md +139 -0
- data/Gemfile +16 -11
- data/Gemfile.lock +53 -27
- data/README.md +84 -21
- data/docs/arguments.md +290 -0
- data/docs/best-practices.md +153 -0
- data/docs/callbacks.md +476 -0
- data/docs/concepts.md +80 -0
- data/docs/configuration.md +204 -0
- data/docs/context.md +128 -0
- data/docs/crud.md +525 -0
- data/docs/errors.md +280 -0
- data/docs/generators.md +250 -0
- data/docs/outputs.md +158 -0
- data/docs/pundit-authorization.md +320 -0
- data/docs/quickstart.md +134 -0
- data/docs/readme.md +101 -0
- data/docs/recipes.md +14 -0
- data/docs/rubocop.md +285 -0
- data/docs/ruby-lsp.md +133 -0
- data/docs/service-rendering.md +222 -0
- data/docs/steps.md +391 -0
- data/docs/summary.md +21 -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 +134 -122
- data/lib/light/services/base_with_context.rb +23 -1
- data/lib/light/services/callbacks.rb +157 -0
- data/lib/light/services/collection.rb +145 -0
- data/lib/light/services/concerns/execution.rb +79 -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 +82 -16
- data/lib/light/services/constants.rb +100 -0
- data/lib/light/services/dsl/arguments_dsl.rb +85 -0
- data/lib/light/services/dsl/outputs_dsl.rb +81 -0
- data/lib/light/services/dsl/steps_dsl.rb +205 -0
- data/lib/light/services/dsl/validation.rb +162 -0
- data/lib/light/services/exceptions.rb +25 -2
- data/lib/light/services/message.rb +28 -3
- data/lib/light/services/messages.rb +92 -32
- 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/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 +114 -0
- data/lib/light/services/settings/step.rb +53 -20
- data/lib/light/services/utils.rb +38 -0
- data/lib/light/services/version.rb +1 -1
- data/lib/light/services.rb +2 -0
- 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
- data/light-services.gemspec +6 -8
- metadata +68 -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/README.md
CHANGED
|
@@ -1,36 +1,50 @@
|
|
|
1
1
|
# 🚀 Light Services
|
|
2
2
|
|
|
3
|
-
Light Services is a simple yet powerful way to organize
|
|
3
|
+
Light Services is a simple yet powerful way to organize business logic in Ruby applications. Build services that are easy to test, maintain, and understand.
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
[](https://codecov.io/gh/light-ruby/light-services)
|
|
7
7
|
|
|
8
|
+
[Get started with Quickstart](https://light-services.kodkod.me/quickstart)
|
|
9
|
+
|
|
8
10
|
## Features
|
|
9
11
|
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
12
|
+
- ✨ **Simple**: Define your service as a class with `arguments`, `steps`, and `outputs`
|
|
13
|
+
- 📦 **No runtime dependencies**: Works stand-alone without requiring external gems at runtime
|
|
14
|
+
- 🔄 **Transactions**: Automatically rollback database changes if any step fails
|
|
15
|
+
- 🧬 **Inheritance**: Inherit from other services to reuse logic seamlessly
|
|
16
|
+
- ⚠️ **Error Handling**: Collect errors from steps and handle them your way
|
|
17
|
+
- 🔗 **Context**: Run multiple services sequentially within the same context
|
|
18
|
+
- 🧪 **RSpec Matchers**: Built-in RSpec matchers for expressive service tests
|
|
19
|
+
- 🌐 **Framework Agnostic**: Compatible with Rails, Hanami, or any Ruby framework
|
|
20
|
+
- 🧩 **Modularity**: Isolate and test your services with ease
|
|
21
|
+
- ✅ **100% Test Coverage**: Thoroughly tested and reliable
|
|
22
|
+
- ⚔️ **Battle-Tested**: In production use since 2017
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
gem "light-services", "~> 3.0"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
rails generate light_services:install
|
|
32
|
+
```
|
|
19
33
|
|
|
20
34
|
## Simple Example
|
|
21
35
|
|
|
22
36
|
```ruby
|
|
23
37
|
class GreetService < Light::Services::Base
|
|
24
38
|
# Arguments
|
|
25
|
-
arg :name
|
|
26
|
-
arg :age
|
|
39
|
+
arg :name, type: String
|
|
40
|
+
arg :age, type: Integer
|
|
27
41
|
|
|
28
42
|
# Steps
|
|
29
43
|
step :build_message
|
|
30
44
|
step :send_message
|
|
31
45
|
|
|
32
46
|
# Outputs
|
|
33
|
-
output :message
|
|
47
|
+
output :message, type: String
|
|
34
48
|
|
|
35
49
|
private
|
|
36
50
|
|
|
@@ -44,14 +58,14 @@ class GreetService < Light::Services::Base
|
|
|
44
58
|
end
|
|
45
59
|
```
|
|
46
60
|
|
|
47
|
-
## Advanced Example
|
|
61
|
+
## Advanced Example (with dry-types and conditions)
|
|
48
62
|
|
|
49
63
|
```ruby
|
|
50
64
|
class User::ResetPassword < Light::Services::Base
|
|
51
|
-
# Arguments
|
|
52
|
-
arg :user, type: User, optional: true
|
|
53
|
-
arg :email, type:
|
|
54
|
-
arg :send_email, type:
|
|
65
|
+
# Arguments with dry-types for advanced validation and coercion
|
|
66
|
+
arg :user, type: Types.Instance(User), optional: true
|
|
67
|
+
arg :email, type: Types::Coercible::String, optional: true
|
|
68
|
+
arg :send_email, type: Types::Params::Bool, default: true
|
|
55
69
|
|
|
56
70
|
# Steps
|
|
57
71
|
step :validate
|
|
@@ -60,9 +74,9 @@ class User::ResetPassword < Light::Services::Base
|
|
|
60
74
|
step :save_reset_token
|
|
61
75
|
step :send_reset_email, if: :send_email?
|
|
62
76
|
|
|
63
|
-
# Outputs
|
|
64
|
-
output :user, type: User
|
|
65
|
-
output :reset_token, type:
|
|
77
|
+
# Outputs with dry-types
|
|
78
|
+
output :user, type: Types.Instance(User)
|
|
79
|
+
output :reset_token, type: Types::Strict::String
|
|
66
80
|
|
|
67
81
|
private
|
|
68
82
|
|
|
@@ -96,6 +110,55 @@ class User::ResetPassword < Light::Services::Base
|
|
|
96
110
|
end
|
|
97
111
|
```
|
|
98
112
|
|
|
113
|
+
[Get started with Light Services](https://light-services.kodkod.me/quickstart)
|
|
114
|
+
|
|
115
|
+
## Rails Generators
|
|
116
|
+
|
|
117
|
+
Light Services includes Rails generators to help you quickly set up and create services in your Rails application.
|
|
118
|
+
|
|
119
|
+
### Install Generator
|
|
120
|
+
|
|
121
|
+
Set up Light Services in your Rails application:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
bin/rails generate light_services:install
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
This creates:
|
|
128
|
+
- `app/services/application_service.rb` - Base service class for your application
|
|
129
|
+
- `config/initializers/light_services.rb` - Configuration file
|
|
130
|
+
- `spec/services/application_service_spec.rb` - RSpec test file (if RSpec is detected)
|
|
131
|
+
|
|
132
|
+
**Options:**
|
|
133
|
+
- `--skip-initializer` - Skip creating the initializer file
|
|
134
|
+
- `--skip-spec` - Skip creating the spec file
|
|
135
|
+
|
|
136
|
+
### Service Generator
|
|
137
|
+
|
|
138
|
+
Create a new service class:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# Basic service
|
|
142
|
+
bin/rails generate light_services:service user/create
|
|
143
|
+
|
|
144
|
+
# Service with predefined structure
|
|
145
|
+
bin/rails generate light_services:service CreateOrder \
|
|
146
|
+
--args=user product \
|
|
147
|
+
--steps=validate process \
|
|
148
|
+
--outputs=order
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
This creates:
|
|
152
|
+
- `app/services/user/create.rb` - Service class file
|
|
153
|
+
- `spec/services/user/create_spec.rb` - RSpec test file (if RSpec is detected)
|
|
154
|
+
|
|
155
|
+
**Options:**
|
|
156
|
+
- `--args` - List of arguments for the service (e.g., `--args=user product`)
|
|
157
|
+
- `--steps` - List of steps for the service (e.g., `--steps=validate process`)
|
|
158
|
+
- `--outputs` - List of outputs for the service (e.g., `--outputs=result`)
|
|
159
|
+
- `--skip-spec` - Skip creating the spec file
|
|
160
|
+
- `--parent` - Parent class (default: ApplicationService)
|
|
161
|
+
|
|
99
162
|
## Documentation
|
|
100
163
|
|
|
101
164
|
You can find the full documentation at [light-services.kodkod.me](https://light-services.kodkod.me).
|
data/docs/arguments.md
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
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
|
+
### Type Enforcement (Enabled by Default)
|
|
59
|
+
|
|
60
|
+
By default, all arguments must have a `type` option. This helps catch type-related bugs early and makes your services self-documenting.
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
class MyService < ApplicationService
|
|
64
|
+
arg :name, type: String # ✓ Valid
|
|
65
|
+
arg :age # ✗ Raises MissingTypeError
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
To disable type enforcement for a specific service:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
class LegacyService < ApplicationService
|
|
73
|
+
config require_type: false
|
|
74
|
+
|
|
75
|
+
arg :name # Allowed when require_type is disabled
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
See the [Configuration documentation](configuration.md) for more details.
|
|
80
|
+
|
|
81
|
+
### dry-types Support
|
|
82
|
+
|
|
83
|
+
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.
|
|
84
|
+
|
|
85
|
+
First, set up your types module:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
require "dry-types"
|
|
89
|
+
|
|
90
|
+
module Types
|
|
91
|
+
include Dry.Types()
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Then use dry-types in your service arguments:
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
class User::Create < ApplicationService
|
|
99
|
+
# Strict types - must match exactly
|
|
100
|
+
arg :name, type: Types::Strict::String
|
|
101
|
+
|
|
102
|
+
# Coercible types - automatically convert values
|
|
103
|
+
arg :age, type: Types::Coercible::Integer
|
|
104
|
+
|
|
105
|
+
# Constrained types - add validation rules
|
|
106
|
+
arg :email, type: Types::String.constrained(format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i)
|
|
107
|
+
|
|
108
|
+
# Enum types - restrict to specific values
|
|
109
|
+
arg :status, type: Types::String.enum("active", "inactive", "pending")
|
|
110
|
+
|
|
111
|
+
# Array types with element validation
|
|
112
|
+
arg :tags, type: Types::Array.of(Types::String)
|
|
113
|
+
|
|
114
|
+
# Hash schemas
|
|
115
|
+
arg :metadata, type: Types::Hash.schema(key: Types::String)
|
|
116
|
+
end
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Coercion Example:**
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
# With coercible types, string "25" is automatically converted to integer 25
|
|
123
|
+
service = User::Create.run(name: "John", age: "25")
|
|
124
|
+
service.age # => 25 (Integer, not String)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Required Arguments
|
|
128
|
+
|
|
129
|
+
By default, arguments are required. You can make them optional by setting `optional` to `true`.
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
class HappyBirthdayService < ApplicationService
|
|
133
|
+
arg :name, type: String
|
|
134
|
+
arg :age, type: Integer, optional: true
|
|
135
|
+
end
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Default Values
|
|
139
|
+
|
|
140
|
+
Set a default value for an argument to make it optional.
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
class HappyBirthdayService < ApplicationService
|
|
144
|
+
arg :name, type: String
|
|
145
|
+
arg :age, type: Integer, default: 18
|
|
146
|
+
end
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Complex Default Values
|
|
150
|
+
|
|
151
|
+
Default values are deep duplicated when the service is invoked, making it safe to use mutable objects.
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
arg :options, type: Hash, default: { a: 1, b: 2 }
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Procs as Default Values
|
|
158
|
+
|
|
159
|
+
Use procs for dynamic default values.
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
arg :current_date, type: Date, default: -> { Date.current }
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Inheritance
|
|
166
|
+
|
|
167
|
+
Arguments are inherited from parent classes.
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
# UpdateRecordService
|
|
171
|
+
class UpdateRecordService < ApplicationService
|
|
172
|
+
# Arguments
|
|
173
|
+
arg :record, type: ApplicationRecord
|
|
174
|
+
arg :attributes, type: Hash
|
|
175
|
+
|
|
176
|
+
# Steps
|
|
177
|
+
step :authorize
|
|
178
|
+
step :update_record
|
|
179
|
+
end
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
# User::Update inherited from UpdateRecordService
|
|
184
|
+
class User::Update < UpdateRecordService
|
|
185
|
+
# Nothing to do here
|
|
186
|
+
# Arguments and steps are inherited from UpdateRecordService
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Removing Inherited Arguments
|
|
191
|
+
|
|
192
|
+
To remove an inherited argument, use `remove_arg`:
|
|
193
|
+
|
|
194
|
+
```ruby
|
|
195
|
+
class BaseService < ApplicationService
|
|
196
|
+
arg :current_user, type: User
|
|
197
|
+
arg :audit_log, type: [TrueClass, FalseClass], default: true
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
class SystemTaskService < BaseService
|
|
201
|
+
# System tasks don't need a current_user
|
|
202
|
+
remove_arg :current_user
|
|
203
|
+
end
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Context Arguments
|
|
207
|
+
|
|
208
|
+
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`.
|
|
209
|
+
|
|
210
|
+
Learn more about context in the [Context documentation](context.md).
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
class ApplicationService < Light::Services::Base
|
|
214
|
+
arg :current_user, type: User, optional: true, context: true
|
|
215
|
+
end
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Accessing Arguments
|
|
219
|
+
|
|
220
|
+
Arguments are accessible like instance variables, similar to `attr_accessor`.
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
class HappyBirthdayService < ApplicationService
|
|
224
|
+
# Arguments
|
|
225
|
+
arg :name, type: String
|
|
226
|
+
arg :age, type: Integer
|
|
227
|
+
|
|
228
|
+
# Steps
|
|
229
|
+
step :greet
|
|
230
|
+
|
|
231
|
+
private
|
|
232
|
+
|
|
233
|
+
def greet
|
|
234
|
+
puts "Happy birthday, #{name}! You are #{age} years old."
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Accessing Arguments Using `arguments`
|
|
240
|
+
|
|
241
|
+
For dynamic access or to avoid conflicts, use the `arguments` method.
|
|
242
|
+
|
|
243
|
+
```ruby
|
|
244
|
+
class HappyBirthdayService < ApplicationService
|
|
245
|
+
# Arguments
|
|
246
|
+
arg :name, type: String
|
|
247
|
+
arg :age, type: Integer
|
|
248
|
+
|
|
249
|
+
# Steps
|
|
250
|
+
step :greet
|
|
251
|
+
|
|
252
|
+
private
|
|
253
|
+
|
|
254
|
+
def greet
|
|
255
|
+
name = arguments[:name] # or arguments.get(:name)
|
|
256
|
+
age = arguments[:age] # or arguments.get(:age)
|
|
257
|
+
|
|
258
|
+
puts "Happy birthday, #{name}! You are #{age} years old."
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Argument Predicate Methods
|
|
264
|
+
|
|
265
|
+
Predicate methods are automatically generated for each argument, allowing you to check if an argument is `true` or `false`.
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
class User::GenerateInvoice < ApplicationService
|
|
269
|
+
# Arguments
|
|
270
|
+
arg :user, type: User
|
|
271
|
+
arg :charge, type: [TrueClass, FalseClass], default: false
|
|
272
|
+
|
|
273
|
+
# Steps
|
|
274
|
+
step :generate_invoice
|
|
275
|
+
step :charge_user, if: :charge?
|
|
276
|
+
|
|
277
|
+
# ...
|
|
278
|
+
end
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
{% hint style="info" %}
|
|
282
|
+
The predicate methods return `true` or `false` based on Ruby's convention: `nil` and `false` are `false`, everything else is `true`.
|
|
283
|
+
{% endhint %}
|
|
284
|
+
|
|
285
|
+
## What's Next?
|
|
286
|
+
|
|
287
|
+
Next step is `steps` (I love this pun). Steps are the building blocks of a service, the methods that do the actual work.
|
|
288
|
+
|
|
289
|
+
[Next: Steps](steps.md)
|
|
290
|
+
|
|
@@ -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)
|