service_actor 3.6.1 → 3.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0597123f9fd025cb0fdd1d099c9a4c29865a086a107d67f7b336b3828e6743c5'
4
- data.tar.gz: fa2e8c72f82970ff779f1c870fa4074630b7e4d2d947f10f57e8a0670a9bae07
3
+ metadata.gz: 5654d6b33f83eaccbc860a7072e0f3f3fcf694e4e9ba5fdb4224ab6bd180f860
4
+ data.tar.gz: 755220b315eccaf564f6eeb85c234a4da1deeb24ea3e71740f7bba3cb5029135
5
5
  SHA512:
6
- metadata.gz: 73dea56535940a843c9cf9bee977882293a0dd2ad4b6c0d3069fe28ea4b7d2569f3c3508cc68f8d378ebb526b00027b72a232373049bcf541e91861fe558d589
7
- data.tar.gz: 282acf3196702a03f15e8faba840abdf68075abcc3e132dce6efcb090c5349b426f8261bd13fafc68f4e4fbac2a25a1401da8ad9b2638eb1d2a907cb548c8887
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 into small composable
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 togethor](#play-actors-in-a-sequence).
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 a #{adjective} #{length_of_time} #{name}!"
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
- default = default.call if default.is_a?(Proc)
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
- default = default.call if default.is_a?(Proc)
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 if options[:if] && !options[:if].call(result)
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) ||
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ServiceActor
4
- VERSION = "3.6.1"
4
+ VERSION = "3.7.0"
5
5
  end
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.6.1
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-02-10 00:00:00.000000000 Z
11
+ date: 2023-05-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: zeitwerk