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/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)
|