service_actor 3.6.1 → 3.7.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/README.md +186 -177
- data/lib/service_actor/defaultable.rb +8 -4
- data/lib/service_actor/playable.rb +8 -1
- data/lib/service_actor/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5654d6b33f83eaccbc860a7072e0f3f3fcf694e4e9ba5fdb4224ab6bd180f860
|
4
|
+
data.tar.gz: 755220b315eccaf564f6eeb85c234a4da1deeb24ea3e71740f7bba3cb5029135
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 753d97cc97069242febc4212ca78c3f1149083dd928578e581c52073f556d40a6f275d08d28a28b97e1fd04841c05e78e3be3b122c2005cbd2775b7257f93b91
|
7
|
+
data.tar.gz: 511d62768af23e9b84aa8ae6d268901f5227caf54956cd6261c1835d518823413c43dae16d278ecf63d45737df4373dbb755b5d1e779afd4c90d7bec78848bc4
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# ServiceActor
|
2
2
|
|
3
|
-
This Ruby gem lets you move your application logic into
|
3
|
+
This Ruby gem lets you move your application logic into small composable
|
4
4
|
service objects. It is a lightweight framework that helps you keep your models
|
5
5
|
and controllers thin.
|
6
6
|
|
@@ -12,17 +12,18 @@ and controllers thin.
|
|
12
12
|
- [Usage](#usage)
|
13
13
|
- [Inputs](#inputs)
|
14
14
|
- [Outputs](#outputs)
|
15
|
-
- [Defaults](#defaults)
|
16
|
-
- [Allow nil](#allow-nil)
|
17
|
-
- [Conditions](#conditions)
|
18
|
-
- [Types](#types)
|
19
15
|
- [Fail](#fail)
|
20
|
-
- [Custom input errors](#custom-input-errors)
|
21
16
|
- [Play actors in a sequence](#play-actors-in-a-sequence)
|
22
17
|
- [Rollback](#rollback)
|
23
18
|
- [Inline actors](#inline-actors)
|
24
19
|
- [Play conditions](#play-conditions)
|
25
20
|
- [Input aliases](#input-aliases)
|
21
|
+
- [Input options](#input-options)
|
22
|
+
- [Defaults](#defaults)
|
23
|
+
- [Allow nil](#allow-nil)
|
24
|
+
- [Conditions](#conditions)
|
25
|
+
- [Types](#types)
|
26
|
+
- [Custom input errors](#custom-input-errors)
|
26
27
|
- [Testing](#testing)
|
27
28
|
- [FAQ](#faq)
|
28
29
|
- [Thanks](#thanks)
|
@@ -77,7 +78,7 @@ SendNotification.call # => <ServiceActor::Result…>
|
|
77
78
|
When called, an actor returns a result. Reading and writing to this result allows
|
78
79
|
actors to accept and return multiple arguments. Let’s find out how to do that
|
79
80
|
and then we’ll see how to
|
80
|
-
[chain multiple actors
|
81
|
+
[chain multiple actors together](#play-actors-in-a-sequence).
|
81
82
|
|
82
83
|
### Inputs
|
83
84
|
|
@@ -122,25 +123,200 @@ actor.greeting # => "Have a wonderful day!"
|
|
122
123
|
actor.greeting? # => true
|
123
124
|
```
|
124
125
|
|
126
|
+
### Fail
|
127
|
+
|
128
|
+
To stop the execution and mark an actor as having failed, use `fail!`:
|
129
|
+
|
130
|
+
```rb
|
131
|
+
class UpdateUser < Actor
|
132
|
+
input :user
|
133
|
+
input :attributes
|
134
|
+
|
135
|
+
def call
|
136
|
+
user.attributes = attributes
|
137
|
+
|
138
|
+
fail!(error: "Invalid user") unless user.valid?
|
139
|
+
|
140
|
+
# …
|
141
|
+
end
|
142
|
+
end
|
143
|
+
```
|
144
|
+
|
145
|
+
This will raise an error in your application with the given data added to the
|
146
|
+
result.
|
147
|
+
|
148
|
+
To test for the success of your actor instead of raising an exception, use
|
149
|
+
`.result` instead of `.call`. You can then call `success?` or `failure?` on
|
150
|
+
the result.
|
151
|
+
|
152
|
+
For example in a Rails controller:
|
153
|
+
|
154
|
+
```rb
|
155
|
+
# app/controllers/users_controller.rb
|
156
|
+
class UsersController < ApplicationController
|
157
|
+
def create
|
158
|
+
actor = UpdateUser.result(user: user, attributes: user_attributes)
|
159
|
+
if actor.success?
|
160
|
+
redirect_to actor.user
|
161
|
+
else
|
162
|
+
render :new, notice: actor.error
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
```
|
167
|
+
|
168
|
+
## Play actors in a sequence
|
169
|
+
|
170
|
+
To help you create actors that are small, single-responsibility actions, an
|
171
|
+
actor can use `play` to call other actors:
|
172
|
+
|
173
|
+
```rb
|
174
|
+
class PlaceOrder < Actor
|
175
|
+
play CreateOrder,
|
176
|
+
PayOrder,
|
177
|
+
SendOrderConfirmation,
|
178
|
+
NotifyAdmins
|
179
|
+
end
|
180
|
+
```
|
181
|
+
|
182
|
+
Calling this actor will now call every actor along the way. Inputs and outputs
|
183
|
+
will go from one actor to the next, all sharing the same result set until it is
|
184
|
+
finally returned.
|
185
|
+
|
186
|
+
### Rollback
|
187
|
+
|
188
|
+
When using `play`, if an actor calls `fail!`, the following actors will not be
|
189
|
+
called.
|
190
|
+
|
191
|
+
Instead, all the actors that succeeded will have their `rollback` method called
|
192
|
+
in reverse order. This allows actors a chance to cleanup, for example:
|
193
|
+
|
194
|
+
```rb
|
195
|
+
class CreateOrder < Actor
|
196
|
+
output :order
|
197
|
+
|
198
|
+
def call
|
199
|
+
self.order = Order.create!(…)
|
200
|
+
end
|
201
|
+
|
202
|
+
def rollback
|
203
|
+
order.destroy
|
204
|
+
end
|
205
|
+
end
|
206
|
+
```
|
207
|
+
|
208
|
+
Rollback is only called on the _previous_ actors in `play` and is not called on
|
209
|
+
the failing actor itself. Actors should be kept to a single purpose and not have
|
210
|
+
anything to clean up if they call `fail!`.
|
211
|
+
|
212
|
+
### Inline actors
|
213
|
+
|
214
|
+
For small work or preparing the result set for the next actors, you can create
|
215
|
+
inline actors by using lambdas. Each lambda has access to the shared result. For
|
216
|
+
example:
|
217
|
+
|
218
|
+
```rb
|
219
|
+
class PayOrder < Actor
|
220
|
+
input :order
|
221
|
+
|
222
|
+
play -> actor { actor.order.currency ||= "EUR" },
|
223
|
+
CreatePayment,
|
224
|
+
UpdateOrderBalance,
|
225
|
+
-> actor { Logger.info("Order #{actor.order.id} paid") }
|
226
|
+
end
|
227
|
+
```
|
228
|
+
|
229
|
+
You can also call instance methods. For example:
|
230
|
+
|
231
|
+
```rb
|
232
|
+
class PayOrder < Actor
|
233
|
+
input :order
|
234
|
+
|
235
|
+
play :assign_default_currency,
|
236
|
+
CreatePayment,
|
237
|
+
UpdateOrderBalance,
|
238
|
+
:log_payment
|
239
|
+
|
240
|
+
private
|
241
|
+
|
242
|
+
def assign_default_currency
|
243
|
+
order.currency ||= "EUR"
|
244
|
+
end
|
245
|
+
|
246
|
+
def log_payment
|
247
|
+
Logger.info("Order #{order.id} paid")
|
248
|
+
end
|
249
|
+
end
|
250
|
+
```
|
251
|
+
|
252
|
+
If you want to do work around the whole actor, you can also override the `call`
|
253
|
+
method. For example:
|
254
|
+
|
255
|
+
```rb
|
256
|
+
class PayOrder < Actor
|
257
|
+
# …
|
258
|
+
|
259
|
+
def call
|
260
|
+
Time.with_timezone("Paris") do
|
261
|
+
super
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
```
|
266
|
+
|
267
|
+
### Play conditions
|
268
|
+
|
269
|
+
Actors in a play can be called conditionally:
|
270
|
+
|
271
|
+
```rb
|
272
|
+
class PlaceOrder < Actor
|
273
|
+
play CreateOrder,
|
274
|
+
Pay
|
275
|
+
play NotifyAdmins, if: -> actor { actor.order.amount > 42 }
|
276
|
+
play CreatePayment, unless: -> actor { actor.order.currency == "USD" }
|
277
|
+
end
|
278
|
+
```
|
279
|
+
|
280
|
+
### Input aliases
|
281
|
+
|
282
|
+
You can use `alias_input` to transform the output of an actor into the input of
|
283
|
+
the next actors.
|
284
|
+
|
285
|
+
```rb
|
286
|
+
class PlaceComment < Actor
|
287
|
+
play CreateComment,
|
288
|
+
NotifyCommentFollowers,
|
289
|
+
alias_input(commenter: :user),
|
290
|
+
UpdateUserStats
|
291
|
+
end
|
292
|
+
```
|
293
|
+
|
294
|
+
## Input options
|
295
|
+
|
125
296
|
### Defaults
|
126
297
|
|
127
|
-
Inputs can be optional by providing a default
|
298
|
+
Inputs can be optional by providing a `default` value or lambda.
|
128
299
|
|
129
300
|
```rb
|
130
301
|
class BuildGreeting < Actor
|
131
302
|
input :name
|
132
303
|
input :adjective, default: "wonderful"
|
133
304
|
input :length_of_time, default: -> { ["day", "week", "month"].sample }
|
305
|
+
input :article,
|
306
|
+
default: -> context { context.adjective =~ /^aeiou/ ? 'an' : 'a' }
|
134
307
|
|
135
308
|
output :greeting
|
136
309
|
|
137
310
|
def call
|
138
|
-
self.greeting = "Have
|
311
|
+
self.greeting = "Have #{article} #{length_of_time}, #{name}!"
|
139
312
|
end
|
140
313
|
end
|
141
314
|
|
142
315
|
actor = BuildGreeting.call(name: "Jim")
|
143
|
-
actor.greeting # => "Have a wonderful week Jim!"
|
316
|
+
actor.greeting # => "Have a wonderful week, Jim!"
|
317
|
+
|
318
|
+
actor = BuildGreeting.call(name: "Siobhan", adjective: "elegant")
|
319
|
+
actor.greeting # => "Have an elegant week, Siobhan!"
|
144
320
|
```
|
145
321
|
|
146
322
|
### Allow nil
|
@@ -208,48 +384,6 @@ You may also use strings instead of constants, such as `type: "User"`.
|
|
208
384
|
|
209
385
|
When using a type condition, `allow_nil` defaults to `false`.
|
210
386
|
|
211
|
-
### Fail
|
212
|
-
|
213
|
-
To stop the execution and mark an actor as having failed, use `fail!`:
|
214
|
-
|
215
|
-
```rb
|
216
|
-
class UpdateUser < Actor
|
217
|
-
input :user
|
218
|
-
input :attributes
|
219
|
-
|
220
|
-
def call
|
221
|
-
user.attributes = attributes
|
222
|
-
|
223
|
-
fail!(error: "Invalid user") unless user.valid?
|
224
|
-
|
225
|
-
# …
|
226
|
-
end
|
227
|
-
end
|
228
|
-
```
|
229
|
-
|
230
|
-
This will raise an error in your application with the given data added to the
|
231
|
-
result.
|
232
|
-
|
233
|
-
To test for the success of your actor instead of raising an exception, use
|
234
|
-
`.result` instead of `.call`. You can then call `success?` or `failure?` on
|
235
|
-
the result.
|
236
|
-
|
237
|
-
For example in a Rails controller:
|
238
|
-
|
239
|
-
```rb
|
240
|
-
# app/controllers/users_controller.rb
|
241
|
-
class UsersController < ApplicationController
|
242
|
-
def create
|
243
|
-
actor = UpdateUser.result(user: user, attributes: user_attributes)
|
244
|
-
if actor.success?
|
245
|
-
redirect_to actor.user
|
246
|
-
else
|
247
|
-
render :new, notice: actor.error
|
248
|
-
end
|
249
|
-
end
|
250
|
-
end
|
251
|
-
```
|
252
|
-
|
253
387
|
### Custom input errors
|
254
388
|
|
255
389
|
Use a `Hash` with `is:` and `message:` keys to prepare custom
|
@@ -363,131 +497,6 @@ end
|
|
363
497
|
|
364
498
|
</details>
|
365
499
|
|
366
|
-
## Play actors in a sequence
|
367
|
-
|
368
|
-
To help you create actors that are small, single-responsibility actions, an
|
369
|
-
actor can use `play` to call other actors:
|
370
|
-
|
371
|
-
```rb
|
372
|
-
class PlaceOrder < Actor
|
373
|
-
play CreateOrder,
|
374
|
-
PayOrder,
|
375
|
-
SendOrderConfirmation,
|
376
|
-
NotifyAdmins
|
377
|
-
end
|
378
|
-
```
|
379
|
-
|
380
|
-
Calling this actor will now call every actor along the way. Inputs and outputs
|
381
|
-
will go from one actor to the next, all sharing the same result set until it is
|
382
|
-
finally returned.
|
383
|
-
|
384
|
-
### Rollback
|
385
|
-
|
386
|
-
When using `play`, if an actor calls `fail!`, the following actors will not be
|
387
|
-
called.
|
388
|
-
|
389
|
-
Instead, all the actors that succeeded will have their `rollback` method called
|
390
|
-
in reverse order. This allows actors a chance to cleanup, for example:
|
391
|
-
|
392
|
-
```rb
|
393
|
-
class CreateOrder < Actor
|
394
|
-
output :order
|
395
|
-
|
396
|
-
def call
|
397
|
-
self.order = Order.create!(…)
|
398
|
-
end
|
399
|
-
|
400
|
-
def rollback
|
401
|
-
order.destroy
|
402
|
-
end
|
403
|
-
end
|
404
|
-
```
|
405
|
-
|
406
|
-
Rollback is only called on the _previous_ actors in `play` and is not called on
|
407
|
-
the failing actor itself. Actors should be kept to a single purpose and not have
|
408
|
-
anything to clean up if they call `fail!`.
|
409
|
-
|
410
|
-
### Inline actors
|
411
|
-
|
412
|
-
For small work or preparing the result set for the next actors, you can create
|
413
|
-
inline actors by using lambdas. Each lambda has access to the shared result. For
|
414
|
-
example:
|
415
|
-
|
416
|
-
```rb
|
417
|
-
class PayOrder < Actor
|
418
|
-
input :order
|
419
|
-
|
420
|
-
play -> actor { actor.order.currency ||= "EUR" },
|
421
|
-
CreatePayment,
|
422
|
-
UpdateOrderBalance,
|
423
|
-
-> actor { Logger.info("Order #{actor.order.id} paid") }
|
424
|
-
end
|
425
|
-
```
|
426
|
-
|
427
|
-
You can also call instance methods. For example:
|
428
|
-
|
429
|
-
```rb
|
430
|
-
class PayOrder < Actor
|
431
|
-
input :order
|
432
|
-
|
433
|
-
play :assign_default_currency,
|
434
|
-
CreatePayment,
|
435
|
-
UpdateOrderBalance,
|
436
|
-
:log_payment
|
437
|
-
|
438
|
-
private
|
439
|
-
|
440
|
-
def assign_default_currency
|
441
|
-
order.currency ||= "EUR"
|
442
|
-
end
|
443
|
-
|
444
|
-
def log_payment
|
445
|
-
Logger.info("Order #{order.id} paid")
|
446
|
-
end
|
447
|
-
end
|
448
|
-
```
|
449
|
-
|
450
|
-
If you want to do work around the whole actor, you can also override the `call`
|
451
|
-
method. For example:
|
452
|
-
|
453
|
-
```rb
|
454
|
-
class PayOrder < Actor
|
455
|
-
# …
|
456
|
-
|
457
|
-
def call
|
458
|
-
Time.with_timezone("Paris") do
|
459
|
-
super
|
460
|
-
end
|
461
|
-
end
|
462
|
-
end
|
463
|
-
```
|
464
|
-
|
465
|
-
### Play conditions
|
466
|
-
|
467
|
-
Actors in a play can be called conditionally:
|
468
|
-
|
469
|
-
```rb
|
470
|
-
class PlaceOrder < Actor
|
471
|
-
play CreateOrder,
|
472
|
-
Pay
|
473
|
-
play NotifyAdmins, if: -> actor { actor.order.amount > 42 }
|
474
|
-
end
|
475
|
-
```
|
476
|
-
|
477
|
-
### Input aliases
|
478
|
-
|
479
|
-
You can use `alias_input` to transform the output of an actor into the input of
|
480
|
-
the next actors.
|
481
|
-
|
482
|
-
```rb
|
483
|
-
class PlaceComment < Actor
|
484
|
-
play CreateComment,
|
485
|
-
NotifyCommentFollowers,
|
486
|
-
alias_input(commenter: :user),
|
487
|
-
UpdateUserStats
|
488
|
-
end
|
489
|
-
```
|
490
|
-
|
491
500
|
## Testing
|
492
501
|
|
493
502
|
In your application, add automated testing to your actors as you would do to any
|
@@ -56,8 +56,7 @@ module ServiceActor::Defaultable
|
|
56
56
|
private
|
57
57
|
|
58
58
|
def default_for_normal_mode_with(result, key, default)
|
59
|
-
|
60
|
-
result[key] = default
|
59
|
+
result[key] = reify_default(result, default)
|
61
60
|
end
|
62
61
|
|
63
62
|
def default_for_advanced_mode_with(result, key, content)
|
@@ -67,8 +66,7 @@ module ServiceActor::Defaultable
|
|
67
66
|
raise_error_with(message, input_key: key, actor: self.class)
|
68
67
|
end
|
69
68
|
|
70
|
-
|
71
|
-
result[key] = default
|
69
|
+
result[key] = reify_default(result, default)
|
72
70
|
|
73
71
|
message.call(key, self.class)
|
74
72
|
end
|
@@ -79,5 +77,11 @@ module ServiceActor::Defaultable
|
|
79
77
|
|
80
78
|
raise self.class.argument_error_class, message
|
81
79
|
end
|
80
|
+
|
81
|
+
def reify_default(result, default)
|
82
|
+
return default unless default.is_a?(Proc)
|
83
|
+
|
84
|
+
default.arity.zero? ? default.call : default.call(result)
|
85
|
+
end
|
82
86
|
end
|
83
87
|
end
|
@@ -56,7 +56,7 @@ module ServiceActor::Playable
|
|
56
56
|
module PrependedMethods
|
57
57
|
def call
|
58
58
|
self.class.play_actors.each do |options|
|
59
|
-
next
|
59
|
+
next unless callable_actor?(options)
|
60
60
|
|
61
61
|
options[:actors].each { |actor| play_actor(actor) }
|
62
62
|
end
|
@@ -77,6 +77,13 @@ module ServiceActor::Playable
|
|
77
77
|
|
78
78
|
private
|
79
79
|
|
80
|
+
def callable_actor?(options)
|
81
|
+
return false if options[:if] && !options[:if].call(result)
|
82
|
+
return false if options[:unless]&.call(result)
|
83
|
+
|
84
|
+
true
|
85
|
+
end
|
86
|
+
|
80
87
|
def play_actor(actor)
|
81
88
|
play_service_actor(actor) ||
|
82
89
|
play_method(actor) ||
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: service_actor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sunny Ripert
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-05-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: zeitwerk
|