service_actor 3.3.0 → 3.4.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: b1cffe28ea4e94682af7f8ee771ad803134e80ce17a9230552d13b4c169b20c7
4
- data.tar.gz: 179b2cbf950872b1dbf46d3bd3b24bafbbc3bbb8cd32574f4dbad6a361958abd
3
+ metadata.gz: ccd0af6d1704b2efc805ce3bfa5cdfd1e74797c9b0a015511daf351e692ab6e8
4
+ data.tar.gz: 6fa64250a89f505bfc0b9b2e300faf5c3ae222740317263fe4947a383e1b64c8
5
5
  SHA512:
6
- metadata.gz: 1e2804c9d25b20b853300d59976075c5ab451670b7e0b05986507e3e07fe9295efa79d3de34ef2d96a59a2f64154bebd4a88a5726ee308dbaed17c5fac2aa3a5
7
- data.tar.gz: 5fc857d43785fe5c6bf5c3723dd91253f4e914f6dccfffc0c258504e306bd4ebc3992ef72a6d37ccc70b4420fba0f86da51165e5fbf0d153b3d64ba9c5bfb18d
6
+ metadata.gz: a3041c28efd75c40e793e299432b743274803b7a237a22525a05de3b916b17f899c6de85a0eeeb98594de47f8d742d595c7582ecb505700a3ec9712f842d1c87
7
+ data.tar.gz: 2f6e0ece1cc4964311a25035dba7011844b1d3992d7ec14f9a36a497b6b62f361ba5826ee364e1871411dd3ce5fff144354a104c846f98f0bcfe971cd8b53030
data/README.md CHANGED
@@ -15,17 +15,17 @@ and controllers thin.
15
15
  - [Inputs](#inputs)
16
16
  - [Outputs](#outputs)
17
17
  - [Defaults](#defaults)
18
- - [Conditions](#conditions)
19
18
  - [Allow nil](#allow-nil)
19
+ - [Conditions](#conditions)
20
20
  - [Types](#types)
21
21
  - [Fail](#fail)
22
+ - [Custom input errors](#custom-input-errors)
22
23
  - [Play actors in a sequence](#play-actors-in-a-sequence)
23
24
  - [Rollback](#rollback)
24
25
  - [Inline actors](#inline-actors)
25
26
  - [Play conditions](#play-conditions)
26
27
  - [Testing](#testing)
27
- - [Build your own actor](#build-your-own-actor)
28
- - [Influences](#influences)
28
+ - [FAQ](#faq)
29
29
  - [Thanks](#thanks)
30
30
  - [Contributing](#contributing)
31
31
  - [License](#contributing)
@@ -120,22 +120,12 @@ The result you get from calling an actor will include the outputs you set:
120
120
  ```rb
121
121
  actor = BuildGreeting.call
122
122
  actor.greeting # => "Have a wonderful day!"
123
- ```
124
-
125
- For every output there is also a boolean method ending with `?` to test its
126
- presence:
127
-
128
- ```rb
129
- if actor.greeting?
130
- puts "Greetings is truthy"
131
- else
132
- puts "Greetings is falsey"
133
- end
123
+ actor.greeting? # => true
134
124
  ```
135
125
 
136
126
  ### Defaults
137
127
 
138
- Inputs can be marked as optional by providing a default:
128
+ Inputs can be optional by providing a default:
139
129
 
140
130
  ```rb
141
131
  class BuildGreeting < Actor
@@ -149,29 +139,30 @@ class BuildGreeting < Actor
149
139
  self.greeting = "Have a #{adjective} #{length_of_time} #{name}!"
150
140
  end
151
141
  end
152
- ```
153
-
154
- This lets you call the actor without specifying those keys:
155
142
 
156
- ```rb
157
143
  actor = BuildGreeting.call(name: "Jim")
158
144
  actor.greeting # => "Have a wonderful week Jim!"
159
145
  ```
160
146
 
161
- If an input does not have a default, it will raise a error:
147
+ ### Allow nil
148
+
149
+ By default inputs accept `nil` values. To raise an error instead:
162
150
 
163
151
  ```rb
164
- BuildGreeting.call
165
- => ServiceActor::ArgumentError: Input name on BuildGreeting is missing.
152
+ class UpdateUser < Actor
153
+ input :user, allow_nil: false
154
+
155
+ # …
156
+ end
166
157
  ```
167
158
 
168
159
  ### Conditions
169
160
 
170
- You can ensure an input is included in a collection by using `in`:
161
+ You can ensure an input is included in a collection by using `inclusion`:
171
162
 
172
163
  ```rb
173
164
  class Pay < Actor
174
- input :currency, in: %w[EUR USD]
165
+ input :currency, inclusion: %w[EUR USD]
175
166
 
176
167
  # …
177
168
  end
@@ -180,7 +171,7 @@ end
180
171
  This raises an argument error if the input does not match one of the given
181
172
  values.
182
173
 
183
- You can also add custom conditions with the name of your choice by using `must`:
174
+ Declare custom conditions with the name of your choice by using `must`:
184
175
 
185
176
  ```rb
186
177
  class UpdateAdminUser < Actor
@@ -193,19 +184,8 @@ class UpdateAdminUser < Actor
193
184
  end
194
185
  ```
195
186
 
196
- This raises an argument error if the given lambda returns a falsey value.
197
-
198
- ### Allow nil
199
-
200
- By default inputs accept `nil` values. To raise an error instead:
201
-
202
- ```rb
203
- class UpdateUser < Actor
204
- input :user, allow_nil: false
205
-
206
- # …
207
- end
208
- ```
187
+ This will raise an argument error if any of the given lambdas returns a falsey
188
+ value.
209
189
 
210
190
  ### Types
211
191
 
@@ -234,7 +214,7 @@ When using a type condition, `allow_nil` defaults to `false`.
234
214
  To stop the execution and mark an actor as having failed, use `fail!`:
235
215
 
236
216
  ```rb
237
- class UpdateUser
217
+ class UpdateUser < Actor
238
218
  input :user
239
219
  input :attributes
240
220
 
@@ -248,7 +228,8 @@ class UpdateUser
248
228
  end
249
229
  ```
250
230
 
251
- This will raise an error in your app with the given data added to the result.
231
+ This will raise an error in your application with the given data added to the
232
+ result.
252
233
 
253
234
  To test for the success of your actor instead of raising an exception, use
254
235
  `.result` instead of `.call`. You can then call `success?` or `failure?` on
@@ -270,8 +251,118 @@ class UsersController < ApplicationController
270
251
  end
271
252
  ```
272
253
 
273
- The keys you add to `fail!` will be added to the result, for example you could
274
- do: `fail!(error_type: "validation", error_code: "uv52", …)`.
254
+ ### Custom input errors
255
+
256
+ Use a `Hash` with `is:` and `message:` keys to prepare custom
257
+ error messages on inputs. For example:
258
+
259
+ ```rb
260
+ class UpdateAdminUser < Actor
261
+ input :user,
262
+ must: {
263
+ be_an_admin: {
264
+ is: -> user { user.admin? },
265
+ message: "The user is not an administrator"
266
+ }
267
+ }
268
+
269
+ # ...
270
+ end
271
+ ```
272
+
273
+ You can also use incoming arguments when shaping your error text:
274
+
275
+ ```rb
276
+ class UpdateUser < Actor
277
+ input :user,
278
+ allow_nil: {
279
+ is: false,
280
+ message: (lambda do |input_key:, **|
281
+ "The value \"#{input_key}\" cannot be empty"
282
+ end)
283
+ }
284
+
285
+ # ...
286
+ end
287
+ ```
288
+
289
+ <details>
290
+ <summary>See examples of custom messages on all input arguments</summary>
291
+
292
+ #### Inclusion
293
+
294
+ ```ruby
295
+ class Pay < Actor
296
+ input :provider,
297
+ inclusion: {
298
+ in: ["MANGOPAY", "PayPal", "Stripe"],
299
+ message: (lambda do |value:, **|
300
+ "Payment system \"#{value}\" is not supported"
301
+ end)
302
+ }
303
+ end
304
+ ```
305
+
306
+ #### Must
307
+
308
+ ```ruby
309
+ class Pay < Actor
310
+ input :provider,
311
+ must: {
312
+ exist: {
313
+ is: -> provider { PROVIDERS.include?(provider) },
314
+ message: (lambda do |value:, **|
315
+ "The specified provider \"#{value}\" was not found."
316
+ end)
317
+ }
318
+ }
319
+ end
320
+ ```
321
+
322
+ #### Default
323
+
324
+ ```ruby
325
+ class MultiplyThing < Actor
326
+ input :multiplier,
327
+ default: {
328
+ is: -> { rand(1..10) },
329
+ message: (lambda do |input_key:, **|
330
+ "Input \"#{input_key}\" is required"
331
+ end)
332
+ }
333
+ end
334
+ ```
335
+
336
+ #### Type
337
+
338
+ ```ruby
339
+ class ReduceOrderAmount < Actor
340
+ input :bonus_applied,
341
+ type: {
342
+ is: [TrueClass, FalseClass],
343
+ message: (lambda do |input_key:, expected_type:, given_type:, **|
344
+ "Wrong type \"#{given_type}\" for \"#{input_key}\". " \
345
+ "Expected: \"#{expected_type}\""
346
+ end)
347
+ }
348
+ end
349
+ ```
350
+
351
+ #### Allow nil
352
+
353
+ ```ruby
354
+ class CreateUser < Actor
355
+ input :name,
356
+ allow_nil: {
357
+ is: false,
358
+ message: (lambda do |input_key:, **|
359
+ "The value \"#{input_key}\" cannot be empty"
360
+ end)
361
+ }
362
+ end
363
+ ```
364
+
365
+ </details>
275
366
 
276
367
  ## Play actors in a sequence
277
368
 
@@ -287,13 +378,13 @@ class PlaceOrder < Actor
287
378
  end
288
379
  ```
289
380
 
290
- This creates a `call` method that will call every actor along the way. Inputs
291
- and outputs will go from one actor to the next, all sharing the same result set
292
- until it is finally returned.
381
+ Calling this actor will now call every actor along the way. Inputs and outputs
382
+ will go from one actor to the next, all sharing the same result set until it is
383
+ finally returned.
293
384
 
294
385
  ### Rollback
295
386
 
296
- When using `play`, when an actor calls `fail!`, the following actors will not be
387
+ When using `play`, if an actor calls `fail!`, the following actors will not be
297
388
  called.
298
389
 
299
390
  Instead, all the actors that succeeded will have their `rollback` method called
@@ -384,22 +475,6 @@ class PlaceOrder < Actor
384
475
  end
385
476
  ```
386
477
 
387
- ### Fail on argument error
388
-
389
- By default, errors on inputs will raise an error, even when using `.result`
390
- instead of `.call`. If instead you want to mark the actor as failed, you can
391
- catch the exception to treat it as an actor failure:
392
-
393
- ```rb
394
- class PlaceOrder < Actor
395
- fail_on ServiceActor::ArgumentError
396
-
397
- input :currency, in: ["EUR", "USD"]
398
-
399
- # …
400
- end
401
- ```
402
-
403
478
  ## Testing
404
479
 
405
480
  In your application, add automated testing to your actors as you would do to any
@@ -408,57 +483,22 @@ other part of your applications.
408
483
  You will find that cutting your business logic into single purpose actors will
409
484
  make it easier for you to test your application.
410
485
 
411
- ## Build your own actor
412
-
413
- If you application already uses a class called “Actor”, you can build your own
414
- by changing the gem’s require statement:
415
-
416
- ```rb
417
- gem "service_actor", require: "service_actor/base"
418
- ```
419
-
420
- And building your own class to inherit from:
421
-
422
- ```rb
423
- class ApplicationActor
424
- include ServiceActor::Base
425
- end
426
- ```
486
+ ## FAQ
427
487
 
428
- ## Influences
429
-
430
- This gem is heavily influenced by
431
- [Interactor](https://github.com/collectiveidea/interactor) ♥.
432
- Some key differences make Actor unique:
433
-
434
- - Does not [hide errors when an actor fails inside another
435
- actor](https://github.com/collectiveidea/interactor/issues/170).
436
- - Requires you to document arguments with `input` and `output`.
437
- - Defaults to raising errors on failures: actor uses `call` and `result`
438
- instead of `call!` and `call`. This way, the _default_ is to raise an error
439
- and failures are not hidden away because you forgot to use `!`.
440
- - Allows defaults, type checking, requirements and conditions on inputs.
441
- - Delegates methods on the context: `foo` vs `context.foo`, `self.foo =` vs
442
- `context.foo = `, `fail!` vs `context.fail!`.
443
- - Shorter setup syntax: inherit from `< Actor` vs having to `include Interactor`
444
- and `include Interactor::Organizer`.
445
- - Organizers allow lambdas, instance methods, being called multiple times,
446
- and having conditions.
447
- - Allows early success with conditions inside organizers.
448
- - No `before`, `after` and `around` hooks, prefer using `play` with lambdas or
449
- overriding `call`.
450
-
451
- Actor supports mixing actors & interactors when using `play` for a smooth
452
- migration.
488
+ Howtos and frequently asked questions can be found on the
489
+ [wiki](https://github.com/sunny/actor/wiki).
453
490
 
454
491
  ## Thanks
455
492
 
456
- Thank you to @nicoolas25, @AnneSottise & @williampollet for the early thoughts
457
- and feedback on this gem.
493
+ This gem is influenced by (and compatible with)
494
+ [Interactor](https://github.com/sunny/actor/wiki/Interactor).
458
495
 
459
496
  Thank you to the wonderful
460
497
  [contributors](https://github.com/sunny/actor/graphs/contributors).
461
498
 
499
+ Thank you to @nicoolas25, @AnneSottise & @williampollet for the early thoughts
500
+ and feedback on this gem.
501
+
462
502
  Photo by [Lloyd Dirks](https://unsplash.com/photos/4SLz_RCk6kQ).
463
503
 
464
504
  ## Contributing
@@ -1,6 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ServiceActor
4
- # Raised when an input or output does not match the given conditions.
5
- class ArgumentError < Error; end
6
- end
3
+ # Raised when an input or output does not match the given conditions.
4
+ class ServiceActor::ArgumentError < ServiceActor::Error; end
@@ -1,59 +1,57 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ServiceActor
4
- # DSL to document the accepted attributes.
5
- #
6
- # class CreateUser < Actor
7
- # input :name
8
- # output :name
9
- # end
10
- module Attributable
11
- def self.included(base)
12
- base.extend(ClassMethods)
13
- end
14
-
15
- module ClassMethods
16
- def inherited(child)
17
- super
18
-
19
- child.inputs.merge!(inputs)
20
- child.outputs.merge!(outputs)
21
- end
3
+ # DSL to document the accepted attributes.
4
+ #
5
+ # class CreateUser < Actor
6
+ # input :name
7
+ # output :name
8
+ # end
9
+ module ServiceActor::Attributable
10
+ def self.included(base)
11
+ base.extend(ClassMethods)
12
+ end
22
13
 
23
- def input(name, **arguments)
24
- inputs[name] = arguments
14
+ module ClassMethods
15
+ def inherited(child)
16
+ super
25
17
 
26
- define_method(name) do
27
- result[name]
28
- end
18
+ child.inputs.merge!(inputs)
19
+ child.outputs.merge!(outputs)
20
+ end
29
21
 
30
- # For avoid method redefined warning messages.
31
- alias_method name, name if method_defined?(name)
22
+ def input(name, **arguments)
23
+ inputs[name] = arguments
32
24
 
33
- protected name
25
+ define_method(name) do
26
+ result[name]
34
27
  end
35
28
 
36
- def inputs
37
- @inputs ||= {}
38
- end
29
+ # For avoid method redefined warning messages.
30
+ alias_method name, name if method_defined?(name)
39
31
 
40
- def output(name, **arguments)
41
- outputs[name] = arguments
32
+ protected name
33
+ end
42
34
 
43
- define_method(name) do
44
- result[name]
45
- end
35
+ def inputs
36
+ @inputs ||= {}
37
+ end
46
38
 
47
- define_method("#{name}=") do |value|
48
- result[name] = value
49
- end
39
+ def output(name, **arguments)
40
+ outputs[name] = arguments
50
41
 
51
- protected name, "#{name}="
42
+ define_method(name) do
43
+ result[name]
52
44
  end
53
45
 
54
- def outputs
55
- @outputs ||= {}
46
+ define_method("#{name}=") do |value|
47
+ result[name] = value
56
48
  end
49
+
50
+ protected name, "#{name}="
51
+ end
52
+
53
+ def outputs
54
+ @outputs ||= {}
57
55
  end
58
56
  end
59
57
  end
@@ -1,39 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Exceptions
4
- require "service_actor/error"
5
- require "service_actor/failure"
6
- require "service_actor/argument_error"
3
+ require "service_actor/support/loader"
7
4
 
8
- # Core
9
- require "service_actor/core"
10
- require "service_actor/attributable"
11
- require "service_actor/playable"
12
- require "service_actor/result"
5
+ module ServiceActor::Base
6
+ def self.included(base)
7
+ # Essential mechanics
8
+ base.include(ServiceActor::Core)
9
+ base.include(ServiceActor::Raisable)
10
+ base.include(ServiceActor::Attributable)
11
+ base.include(ServiceActor::Playable)
13
12
 
14
- # Concerns
15
- require "service_actor/type_checkable"
16
- require "service_actor/nil_checkable"
17
- require "service_actor/conditionable"
18
- require "service_actor/defaultable"
19
- require "service_actor/collectionable"
20
- require "service_actor/failable"
21
-
22
- module ServiceActor
23
- module Base
24
- def self.included(base)
25
- # Core
26
- base.include(Core)
27
- base.include(Attributable)
28
- base.include(Playable)
29
-
30
- # Concerns
31
- base.include(TypeCheckable)
32
- base.include(NilCheckable)
33
- base.include(Conditionable)
34
- base.include(Collectionable)
35
- base.include(Defaultable)
36
- base.include(Failable)
37
- end
13
+ # Extra concerns
14
+ base.include(ServiceActor::TypeCheckable)
15
+ base.include(ServiceActor::NilCheckable)
16
+ base.include(ServiceActor::Conditionable)
17
+ base.include(ServiceActor::Collectionable)
18
+ base.include(ServiceActor::Defaultable)
19
+ base.include(ServiceActor::Failable)
38
20
  end
39
21
  end
@@ -1,32 +1,68 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ServiceActor
4
- # Add checks to your inputs, by specifying what values are authorized
5
- # under the "in" key.
6
- #
7
- # Example:
8
- #
9
- # class Pay < Actor
10
- # input :provider, in: ["MANGOPAY", "PayPal", "Stripe"]
11
- # end
12
- module Collectionable
13
- def self.included(base)
14
- base.prepend(PrependedMethods)
3
+ # Add checks to your inputs, by specifying what values are authorized under the
4
+ # "in" key.
5
+ #
6
+ # Example:
7
+ #
8
+ # class Pay < Actor
9
+ # input :provider, inclusion: ["MANGOPAY", "PayPal", "Stripe"]
10
+ # end
11
+ #
12
+ # class Pay < Actor
13
+ # input :provider,
14
+ # inclusion: {
15
+ # in: ["MANGOPAY", "PayPal", "Stripe"],
16
+ # message: (lambda do |input_key:, actor:, inclusion_in:, value:|
17
+ # "Payment system \"#{value}\" is not supported"
18
+ # end)
19
+ # }
20
+ # end
21
+ module ServiceActor::Collectionable
22
+ def self.included(base)
23
+ base.prepend(PrependedMethods)
24
+ end
25
+
26
+ module PrependedMethods
27
+ DEFAULT_MESSAGE = lambda do |input_key:, actor:, inclusion_in:, value:|
28
+ "The \"#{input_key}\" input must be included " \
29
+ "in #{inclusion_in.inspect} on \"#{actor}\" " \
30
+ "instead of #{value.inspect}"
15
31
  end
16
32
 
17
- module PrependedMethods
18
- def _call
19
- self.class.inputs.each do |key, options|
20
- next unless options[:in]
33
+ private_constant :DEFAULT_MESSAGE
34
+
35
+ def _call # rubocop:disable Metrics/MethodLength
36
+ self.class.inputs.each do |key, options|
37
+ value = result[key]
38
+
39
+ # DEPRECATED: `in` is deprecated in favor of `inclusion`.
40
+ inclusion = options[:inclusion] || options[:in]
41
+
42
+ inclusion_in, message = define_inclusion_from(inclusion)
21
43
 
22
- next if options[:in].include?(result[key])
44
+ next if inclusion_in.nil?
45
+ next if inclusion_in.include?(value)
46
+
47
+ raise_error_with(
48
+ message,
49
+ input_key: key,
50
+ actor: self.class,
51
+ inclusion_in: inclusion_in,
52
+ value: value,
53
+ )
54
+ end
55
+
56
+ super
57
+ end
23
58
 
24
- raise ArgumentError,
25
- "Input #{key} must be included in #{options[:in].inspect} " \
26
- "but instead was #{result[key].inspect}"
27
- end
59
+ private
28
60
 
29
- super
61
+ def define_inclusion_from(inclusion)
62
+ if inclusion.is_a?(Hash)
63
+ inclusion.values_at(:in, :message)
64
+ else
65
+ [inclusion, DEFAULT_MESSAGE]
30
66
  end
31
67
  end
32
68
  end