light-services 2.2.1 → 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 +1 -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 +23 -113
- 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/context.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Context
|
|
2
|
+
|
|
3
|
+
Context allows services to be run within the same execution scope, enabling shared state and coordinated transactions.
|
|
4
|
+
|
|
5
|
+
## Key Features
|
|
6
|
+
|
|
7
|
+
- Services share arguments marked as `context: true`
|
|
8
|
+
- If any service fails, the entire context fails and rolls back database changes
|
|
9
|
+
|
|
10
|
+
## How to Run Services in the Same Context
|
|
11
|
+
|
|
12
|
+
To run a service in the same context, call `with(self)` before the `#run` method.
|
|
13
|
+
|
|
14
|
+
## Context Rollback
|
|
15
|
+
|
|
16
|
+
### Example:
|
|
17
|
+
|
|
18
|
+
Let's say we have two services: `User::Create` and `Profile::Create`. We want to ensure that if either service fails, all database changes are rolled back.
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
class User::Create < ApplicationService
|
|
22
|
+
# Arguments
|
|
23
|
+
arg :attributes, type: Hash
|
|
24
|
+
|
|
25
|
+
# Steps
|
|
26
|
+
step :create_user
|
|
27
|
+
step :create_profile
|
|
28
|
+
step :send_welcome_email
|
|
29
|
+
|
|
30
|
+
# Outputs
|
|
31
|
+
output :user, type: User
|
|
32
|
+
output :profile, type: Profile
|
|
33
|
+
|
|
34
|
+
def create_user
|
|
35
|
+
self.user = User.create!(attributes)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def create_profile
|
|
39
|
+
service = Profile::Create
|
|
40
|
+
.with(self) # This runs the service in the same context
|
|
41
|
+
.run(user:)
|
|
42
|
+
|
|
43
|
+
self.profile = service.profile
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# If the Profile::Create service fails, this step and any following steps won't execute
|
|
47
|
+
# And all database changes will be rolled back
|
|
48
|
+
def send_welcome_email
|
|
49
|
+
# We don't run this service in the same context
|
|
50
|
+
# Because we don't care too much if it fails
|
|
51
|
+
service = Mailer::SendWelcomeEmail.run(user:)
|
|
52
|
+
|
|
53
|
+
# Handle the failure manually if needed
|
|
54
|
+
if service.failed?
|
|
55
|
+
# Handle the failure
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Context Arguments
|
|
62
|
+
|
|
63
|
+
Context arguments are shared between services running in the same context. This can make them a bit less predictable and harder to test.
|
|
64
|
+
|
|
65
|
+
It's recommended to use context arguments only when necessary and keep them as close to the root service as possible. For example, you can use them to share `current_user` or `current_organization` between services.
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
class ApplicationService < Light::Services::Base
|
|
69
|
+
arg :current_user, type: User, context: true
|
|
70
|
+
end
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
class Comment::Create < ApplicationService
|
|
75
|
+
# Arguments
|
|
76
|
+
# We don't need to specify current_user here
|
|
77
|
+
# as it's automatically inherited from the ApplicationService
|
|
78
|
+
arg :post_id, type: Integer
|
|
79
|
+
arg :text, type: String
|
|
80
|
+
arg :subscribe, type: [TrueClass, FalseClass]
|
|
81
|
+
|
|
82
|
+
# Steps
|
|
83
|
+
step :create_comment
|
|
84
|
+
step :subscribe_to_post, if: :subscribe?
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def create_comment
|
|
89
|
+
# ...
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def subscribe_to_post
|
|
93
|
+
Post::Subscribe
|
|
94
|
+
.with(self) # Run service in the same context
|
|
95
|
+
.run(post_id:) # We omit current_user here as context will handle it for us
|
|
96
|
+
|
|
97
|
+
# If we run Post::Subscribe without `with(self)`
|
|
98
|
+
# It'll fail because it won't have information about the `current_user`
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
class Post::Subscribe < ApplicationService
|
|
105
|
+
# Arguments
|
|
106
|
+
arg :post_id, type: Integer
|
|
107
|
+
|
|
108
|
+
# Steps
|
|
109
|
+
step :subscribe
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def subscribe
|
|
114
|
+
# We have access to current_user here because we run it in the same context
|
|
115
|
+
#
|
|
116
|
+
# Even if we would run this service without context this won't be a problem
|
|
117
|
+
# because we specified this argument in top-level service (ApplicationService)
|
|
118
|
+
current_user.subscriptions.create!(post_id:)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
# What's Next?
|
|
124
|
+
|
|
125
|
+
The next step is to learn about error handling in Light Service.
|
|
126
|
+
|
|
127
|
+
[Next: Errors](errors.md)
|
|
128
|
+
|
data/docs/crud.md
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
# CRUD
|
|
2
|
+
|
|
3
|
+
In this recipe we'll create top-level CRUD services to manage our records.
|
|
4
|
+
|
|
5
|
+
This approach has been tested in production for many years and has saved significant time and effort.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
We want to put all the common logic for creating, updating, destroying and finding records in one place.
|
|
10
|
+
|
|
11
|
+
We want to minimize possibility of a mistake by putting logic as close to core as possible.
|
|
12
|
+
|
|
13
|
+
## How to use it in controllers
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
class PostsController < ApplicationController
|
|
17
|
+
def index
|
|
18
|
+
render json: crud_find_all(Post)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def show
|
|
22
|
+
render json: crud_find(Post)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def create
|
|
26
|
+
render_service crud_create(Post)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def update
|
|
30
|
+
render_service crud_update(Post)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def destroy
|
|
34
|
+
render_service crud_destroy(Post)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
{% hint style="info" %}
|
|
40
|
+
`render_service` method is a method from [Rendering Services](service-rendering.md) recipe.
|
|
41
|
+
{% endhint %}
|
|
42
|
+
|
|
43
|
+
## How to use it in services
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
class ParseProfiles < ApplicationService
|
|
47
|
+
# ...
|
|
48
|
+
|
|
49
|
+
def create_profiles
|
|
50
|
+
profiles.each do |profile|
|
|
51
|
+
# Create profile service is automatically run within the same context
|
|
52
|
+
create(
|
|
53
|
+
Profile,
|
|
54
|
+
name: profile.name,
|
|
55
|
+
age: profile.age,
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Customization
|
|
63
|
+
|
|
64
|
+
You don't need to create a new service for every CRUD operation. But you can create a service for specific model if you need to customize the behavior.
|
|
65
|
+
|
|
66
|
+
Just create a service called `{Model}::Create`, `{Model}::Update`, `{Model}::Destroy` and inherit from `CreateRecordService`, `UpdateRecordService`, `DestroyRecordService` respectively.
|
|
67
|
+
|
|
68
|
+
For example:
|
|
69
|
+
|
|
70
|
+
**Adding additional steps to create user:**
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
class User::Create < CreateRecordService
|
|
74
|
+
step :create_profile
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def create_profile
|
|
79
|
+
create!(Profile, user:)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Setting default attributes:**
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
class Post::Create < CreateRecordService
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def default_attributes
|
|
91
|
+
{ status: :draft }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Override attributes:**
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
class Post::Update < UpdateRecordService
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def override_attributes
|
|
103
|
+
{ updated_by: current_user }
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Skipping step:**
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
class Post::Destroy < DestroyRecordService
|
|
112
|
+
remove_step :authorize
|
|
113
|
+
end
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Code
|
|
117
|
+
|
|
118
|
+
{% hint style="info" %}
|
|
119
|
+
This code is just a starting point. You can customize it to fit your needs.
|
|
120
|
+
{% endhint %}
|
|
121
|
+
|
|
122
|
+
{% hint style="info" %}
|
|
123
|
+
You need to add `Pundit Authorization` and `Request Concern` to make this code work.
|
|
124
|
+
{% endhint %}
|
|
125
|
+
|
|
126
|
+
**app/services/create\_record\_service.rb:**
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
class CreateRecordService < ApplicationService
|
|
130
|
+
# Arguments
|
|
131
|
+
arg :record_class, type: Class, default: -> { self.class.module_parent }
|
|
132
|
+
arg :attributes, type: Hash, default: {}
|
|
133
|
+
|
|
134
|
+
# Steps
|
|
135
|
+
step :create_alias
|
|
136
|
+
step :authorize_user
|
|
137
|
+
step :initialize_record
|
|
138
|
+
step :assign_attributes
|
|
139
|
+
step :save_record
|
|
140
|
+
|
|
141
|
+
# Outputs
|
|
142
|
+
output :record
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
# Create a readable alias for the record based on the class name (e.g. `user` for `User`)
|
|
147
|
+
def create_alias
|
|
148
|
+
define_singleton_method(record_class.to_s.underscore) { record }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Check if the user is authorized to create a record
|
|
152
|
+
def authorize_user
|
|
153
|
+
auth(record_class, :create?)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Initialize a new record
|
|
157
|
+
def initialize_record
|
|
158
|
+
self.record = record_class.new
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Assign attributes to the record
|
|
162
|
+
def assign_attributes
|
|
163
|
+
assign_attributes = default_attributes
|
|
164
|
+
.merge(params_attributes)
|
|
165
|
+
.merge(attributes)
|
|
166
|
+
.merge(override_attributes)
|
|
167
|
+
|
|
168
|
+
record.assign_attributes(assign_attributes)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Save the record
|
|
172
|
+
def save_record
|
|
173
|
+
record.save_with!(self)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Extract permitted attributes using Pundit
|
|
177
|
+
def params_attributes
|
|
178
|
+
return {} if !attributes.nil? && !(attributes.respond_to?(:empty?) && attributes.empty?)
|
|
179
|
+
|
|
180
|
+
permitted_attributes(record, :create)
|
|
181
|
+
rescue ActionController::ParameterMissing
|
|
182
|
+
{}
|
|
183
|
+
rescue Pundit::NotDefinedError
|
|
184
|
+
raise unless system
|
|
185
|
+
|
|
186
|
+
{}
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Default attributes, which can be overridden in subclasses
|
|
190
|
+
def default_attributes
|
|
191
|
+
{}
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Override attributes, which can be overridden in subclasses
|
|
195
|
+
def override_attributes
|
|
196
|
+
{}
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**app/services/update\_record\_service.rb:**
|
|
202
|
+
|
|
203
|
+
```ruby
|
|
204
|
+
class UpdateRecordService < ApplicationService
|
|
205
|
+
# Arguments
|
|
206
|
+
arg :record, type: ActiveRecord::Base
|
|
207
|
+
arg :attributes, type: Hash, default: {}
|
|
208
|
+
|
|
209
|
+
# Steps
|
|
210
|
+
step :create_alias
|
|
211
|
+
step :validate_record_class
|
|
212
|
+
step :authorize_user
|
|
213
|
+
step :assign_attributes
|
|
214
|
+
step :save_record
|
|
215
|
+
|
|
216
|
+
private
|
|
217
|
+
|
|
218
|
+
# Create a readable alias for the record based on the class name (e.g. `user` for `User`)
|
|
219
|
+
def create_alias
|
|
220
|
+
define_singleton_method(record.class.to_s.underscore) { record }
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Make sure record is an instance of the correct class
|
|
224
|
+
def validate_record_class
|
|
225
|
+
return if self.class.module_parent == Object # No parent module
|
|
226
|
+
return if self.class.module_parent == record.class
|
|
227
|
+
|
|
228
|
+
errors.add(:base, "record must be #{self.class.module_parent}")
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Check if the user is authorized to update this record
|
|
232
|
+
def authorize_user
|
|
233
|
+
auth(record, :update?) if attributes.nil? || (attributes.respond_to?(:empty?) && attributes.empty?)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Assign attributes to the record
|
|
237
|
+
def assign_attributes
|
|
238
|
+
assign_attributes = default_attributes
|
|
239
|
+
.merge(params_attributes)
|
|
240
|
+
.merge(attributes)
|
|
241
|
+
.merge(override_attributes)
|
|
242
|
+
|
|
243
|
+
record.assign_attributes(assign_attributes)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Save the record
|
|
247
|
+
def save_record
|
|
248
|
+
record.save!
|
|
249
|
+
rescue ActiveRecord::RecordInvalid
|
|
250
|
+
errors.copy_from(record)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Extract permitted attributes from params using Pundit
|
|
254
|
+
def params_attributes
|
|
255
|
+
return {} if !attributes.nil? && !(attributes.respond_to?(:empty?) && attributes.empty?)
|
|
256
|
+
|
|
257
|
+
permitted_attributes(record, :update)
|
|
258
|
+
rescue ActionController::ParameterMissing
|
|
259
|
+
{}
|
|
260
|
+
rescue Pundit::NotDefinedError
|
|
261
|
+
raise unless system
|
|
262
|
+
|
|
263
|
+
{}
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Default attributes, which can be overridden in subclasses
|
|
267
|
+
def default_attributes
|
|
268
|
+
{}
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Overridden attributes, which can be overridden in subclasses
|
|
272
|
+
def override_attributes
|
|
273
|
+
{}
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**app/services/destroy\_record\_service.rb:**
|
|
279
|
+
|
|
280
|
+
```ruby
|
|
281
|
+
class DestroyRecordService < ApplicationService
|
|
282
|
+
# Arguments
|
|
283
|
+
arg :record, type: ActiveRecord::Base
|
|
284
|
+
arg :attributes, type: Hash, default: {}
|
|
285
|
+
|
|
286
|
+
# Steps
|
|
287
|
+
step :create_alias
|
|
288
|
+
step :authorize_user
|
|
289
|
+
step :destroy_record
|
|
290
|
+
|
|
291
|
+
private
|
|
292
|
+
|
|
293
|
+
# Create a readable alias for the record based on the class name (e.g. `user` for `User`)
|
|
294
|
+
def create_alias
|
|
295
|
+
define_singleton_method(record.class.to_s.underscore) { record }
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Check if the user is authorized to update this record
|
|
299
|
+
def authorize_user
|
|
300
|
+
auth(record, :destroy?)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Delete the record
|
|
304
|
+
def destroy_record
|
|
305
|
+
record.destroy!
|
|
306
|
+
rescue ActiveRecord::RecordNotDestroyed
|
|
307
|
+
errors.copy_from(record)
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**app/controller/application\_controller.rb:**
|
|
313
|
+
|
|
314
|
+
```ruby
|
|
315
|
+
class ApplicationController < ActionController::Base
|
|
316
|
+
# Includes
|
|
317
|
+
include CRUDControllers
|
|
318
|
+
include AuthenticateUser
|
|
319
|
+
|
|
320
|
+
private
|
|
321
|
+
|
|
322
|
+
def service_args(hash = {})
|
|
323
|
+
hash.reverse_merge(
|
|
324
|
+
params:,
|
|
325
|
+
request:,
|
|
326
|
+
current_user:,
|
|
327
|
+
current_administrator:,
|
|
328
|
+
)
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**app/controllers/concerns/crud\_controllers.rb:**
|
|
334
|
+
|
|
335
|
+
```ruby
|
|
336
|
+
module CRUDControllers
|
|
337
|
+
extend ActiveSupport::Concern
|
|
338
|
+
|
|
339
|
+
included do
|
|
340
|
+
def crud_find(klass, args = {})
|
|
341
|
+
crud_service(
|
|
342
|
+
klass,
|
|
343
|
+
"Find",
|
|
344
|
+
FindRecordService,
|
|
345
|
+
args.merge(record_class: klass),
|
|
346
|
+
).record
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def crud_find_all(klass, args = {})
|
|
350
|
+
crud_service(
|
|
351
|
+
klass,
|
|
352
|
+
"FindAll",
|
|
353
|
+
FindAllRecordsService,
|
|
354
|
+
args.merge(record_class: klass),
|
|
355
|
+
).scope
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def crud_create(klass, args = {})
|
|
359
|
+
crud_service(
|
|
360
|
+
klass,
|
|
361
|
+
"Create",
|
|
362
|
+
CreateRecordService,
|
|
363
|
+
args.merge(record_class: klass),
|
|
364
|
+
)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def crud_update(record, args = {})
|
|
368
|
+
crud_service(
|
|
369
|
+
record.class,
|
|
370
|
+
"Update",
|
|
371
|
+
UpdateRecordService,
|
|
372
|
+
args.merge(record:),
|
|
373
|
+
)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def crud_destroy(record, args = {})
|
|
377
|
+
crud_service(
|
|
378
|
+
record.class,
|
|
379
|
+
"Destroy",
|
|
380
|
+
DestroyRecordService,
|
|
381
|
+
args.merge(record:),
|
|
382
|
+
)
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
private
|
|
386
|
+
|
|
387
|
+
def crud_service(klass, class_postfix, default_class, args)
|
|
388
|
+
begin
|
|
389
|
+
service_class = "#{klass}::#{class_postfix}".constantize
|
|
390
|
+
rescue NameError
|
|
391
|
+
service_class = default_class
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
service_class.run(service_args(args))
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
**app/services/application\_service.rb:**
|
|
401
|
+
|
|
402
|
+
```ruby
|
|
403
|
+
class ApplicationService < Light::Services::Base
|
|
404
|
+
# Includes
|
|
405
|
+
include CRUDServices
|
|
406
|
+
include RequestConcern
|
|
407
|
+
end
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
**app/services/concerns/crud\_services.rb:**
|
|
411
|
+
|
|
412
|
+
```ruby
|
|
413
|
+
module CRUDServices
|
|
414
|
+
extend ActiveSupport::Concern
|
|
415
|
+
|
|
416
|
+
included do
|
|
417
|
+
def find(klass, args = {})
|
|
418
|
+
run_service(
|
|
419
|
+
klass,
|
|
420
|
+
"Find",
|
|
421
|
+
FindRecordService,
|
|
422
|
+
args.merge(record_class: klass),
|
|
423
|
+
)
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def find_all(klass, args = {})
|
|
427
|
+
args.reverse_merge!(no_filters: true)
|
|
428
|
+
|
|
429
|
+
run_service(
|
|
430
|
+
klass,
|
|
431
|
+
"FindAll",
|
|
432
|
+
FindAllRecordsService,
|
|
433
|
+
args.merge(record_class: klass),
|
|
434
|
+
plural_output: true,
|
|
435
|
+
)
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def create(klass, attributes = {}, args = {})
|
|
439
|
+
run_service(
|
|
440
|
+
klass,
|
|
441
|
+
"Create",
|
|
442
|
+
CreateRecordService,
|
|
443
|
+
args.merge(record_class: klass, attributes:),
|
|
444
|
+
)
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def create!(klass, attributes = {}, args = {})
|
|
448
|
+
create(klass, attributes, args.merge(raise_on_error: true))
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def update(record, attributes = {}, args = {})
|
|
452
|
+
run_service(
|
|
453
|
+
record.class,
|
|
454
|
+
"Update",
|
|
455
|
+
UpdateRecordService,
|
|
456
|
+
args.merge(record:, attributes:),
|
|
457
|
+
)
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def update!(record, attributes = {}, args = {})
|
|
461
|
+
update(record, attributes, args.merge(raise_on_error: true))
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def destroy(record, args = {})
|
|
465
|
+
run_service(
|
|
466
|
+
record.class,
|
|
467
|
+
"Destroy",
|
|
468
|
+
DestroyRecordService,
|
|
469
|
+
args.merge(record:),
|
|
470
|
+
)
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def destroy!(record, args = {})
|
|
474
|
+
destroy(record, args.merge(raise_on_error: true))
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def create_or_update!(klass, record, attributes = {}, args = {})
|
|
478
|
+
if record
|
|
479
|
+
update!(record, attributes, args)
|
|
480
|
+
else
|
|
481
|
+
create!(klass, attributes, args)
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
private
|
|
486
|
+
|
|
487
|
+
def resource_name(klass, plural: false)
|
|
488
|
+
name = klass.name.demodulize.underscore
|
|
489
|
+
plural ? name.pluralize : name
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def run_service(klass, class_postfix, default_class, args, opts = {})
|
|
493
|
+
begin
|
|
494
|
+
service_class = "#{klass}::#{class_postfix}".constantize
|
|
495
|
+
rescue NameError
|
|
496
|
+
service_class = default_class
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
service_class
|
|
500
|
+
.with(self)
|
|
501
|
+
.run(args)
|
|
502
|
+
.public_send(resource_name(klass, plural: opts[:plural_output]))
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
**app/services/concerns/request\_concern.rb:**
|
|
509
|
+
|
|
510
|
+
```ruby
|
|
511
|
+
module RequestConcern
|
|
512
|
+
extend ActiveSupport::Concern
|
|
513
|
+
|
|
514
|
+
included do
|
|
515
|
+
arg :params, type: [Hash, ActionController::Parameters], default: ActionController::Parameters.new({}), context: true
|
|
516
|
+
arg :request, type: ActionDispatch::Request, default: ActionDispatch::Request.new({}), context: true
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
## What's Next?
|
|
522
|
+
|
|
523
|
+
Learn how to render service results cleanly in your controllers:
|
|
524
|
+
|
|
525
|
+
[Next: Service Rendering](service-rendering.md)
|