service_actor 3.1.3 → 3.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 40bbf17640409642508a6cb2dc7bd7197a49266ae9639c4a97c5cf52a3696ddd
4
- data.tar.gz: 6b2627086780fcbead6e4ce3feff3565eab1bc5e3181a743296e8a7099e22480
3
+ metadata.gz: 8bde0ceb526951927f7349b5bd80dc2d6ca036e40e7dbc3ca98bc027f47fbd4c
4
+ data.tar.gz: cd90e8762fe61eccd643fcdb318ebb27b79a561dbc8075ae4e2c802734ace9f7
5
5
  SHA512:
6
- metadata.gz: 0af13a24439bbab76ce2ae6ddf57e41b86b34c4f81e7d333650675195ab27d2519385e479bc1c41eee3c61f54253812c7e6579fea47bd7d8d17839d76a9808af
7
- data.tar.gz: d25a43b1637a9dd1d79c6c0686ccc9bc2bb69dfbf0667c5a8775b1ac3fffab4c3d2233a955a17d676a0ee17b1af1a583449ac39dea220bbc02df3f82e9df6275
6
+ metadata.gz: 2cddbccf7794088dc4a25b011cdb1155d0b6e855abd4a83630402ec90d4fe2f6c4c7fa4a0935ddca7faee58e49b2ff3d1f9f99b430c76dcc2990452d06d16ded
7
+ data.tar.gz: 81444f049effd685780bd4904c3513c7d7adf220c2ecf4ca6a3bf18e077d1b207786f6b85eeb02597e23700bf2a661c3a83f2c4602333bf85c6adf7e810652ef
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Actor
1
+ # ServiceActor
2
2
 
3
3
  ![Tests](https://github.com/sunny/actor/workflows/Tests/badge.svg)
4
4
 
@@ -21,31 +21,37 @@ and controllers thin.
21
21
  - [Fail](#fail)
22
22
  - [Play actors in a sequence](#play-actors-in-a-sequence)
23
23
  - [Rollback](#rollback)
24
- - [Lambdas](#lambdas)
24
+ - [Inline actors](#inline-actors)
25
25
  - [Play conditions](#play-conditions)
26
26
  - [Testing](#testing)
27
27
  - [Build your own actor](#build-your-own-actor)
28
28
  - [Influences](#influences)
29
29
  - [Thanks](#thanks)
30
- - [Development](#development)
31
30
  - [Contributing](#contributing)
32
31
  - [License](#contributing)
33
32
 
34
33
  ## Installation
35
34
 
36
- Add these lines to your application’s Gemfile:
35
+ Add the gem to your application’s Gemfile by executing:
37
36
 
38
- ```rb
39
- # Composable service objects
40
- gem 'service_actor'
37
+ ```sh
38
+ bundle add service_actor
41
39
  ```
42
40
 
43
- When using Rails, you can include the
41
+ ### Extensions
42
+
43
+ For **Rails generators**, you can use the
44
44
  [service_actor-rails](https://github.com/sunny/actor-rails) gem:
45
45
 
46
- ```ruby
47
- # Composable service objects
48
- gem "service_actor-rails"
46
+ ```sh
47
+ bundle add service_actor-rails
48
+ ```
49
+
50
+ For **TTY prompts**, you can use the
51
+ [service_actor-promptable](https://github.com/pboling/service_actor-promptable) gem:
52
+
53
+ ```sh
54
+ bundle add service_actor-promptable
49
55
  ```
50
56
 
51
57
  ## Usage
@@ -69,9 +75,10 @@ Trigger them in your application with `.call`:
69
75
  SendNotification.call # => <ServiceActor::Result…>
70
76
  ```
71
77
 
72
- When called, actors return a Result. Reading and writing to this result allows
73
- actors to accept and return multiple arguments. Let's find out how to do that
74
- and then we'll see how to chain multiple actors togethor.
78
+ When called, an actor returns a result. Reading and writing to this result allows
79
+ actors to accept and return multiple arguments. Lets find out how to do that
80
+ and then well see how to
81
+ [chain multiple actors togethor](#play-actors-in-a-sequence).
75
82
 
76
83
  ### Inputs
77
84
 
@@ -111,8 +118,19 @@ end
111
118
  The result you get from calling an actor will include the outputs you set:
112
119
 
113
120
  ```rb
114
- result = BuildGreeting.call
115
- result.greeting # => "Have a wonderful day!"
121
+ actor = BuildGreeting.call
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
116
134
  ```
117
135
 
118
136
  ### Defaults
@@ -136,14 +154,14 @@ end
136
154
  This lets you call the actor without specifying those keys:
137
155
 
138
156
  ```rb
139
- result = BuildGreeting.call(name: "Jim")
140
- result.greeting # => "Have a wonderful week Jim!"
157
+ actor = BuildGreeting.call(name: "Jim")
158
+ actor.greeting # => "Have a wonderful week Jim!"
141
159
  ```
142
160
 
143
161
  If an input does not have a default, it will raise a error:
144
162
 
145
163
  ```rb
146
- result = BuildGreeting.call
164
+ BuildGreeting.call
147
165
  => ServiceActor::ArgumentError: Input name on BuildGreeting is missing.
148
166
  ```
149
167
 
@@ -168,7 +186,7 @@ You can also add custom conditions with the name of your choice by using `must`:
168
186
  class UpdateAdminUser < Actor
169
187
  input :user,
170
188
  must: {
171
- be_an_admin: ->(user) { user.admin? }
189
+ be_an_admin: -> user { user.admin? }
172
190
  }
173
191
 
174
192
  # …
@@ -207,7 +225,7 @@ class UpdateUser < Actor
207
225
  end
208
226
  ```
209
227
 
210
- You may also use strings instead of constants, such as `type: 'User'`.
228
+ You may also use strings instead of constants, such as `type: "User"`.
211
229
 
212
230
  When using a type condition, `allow_nil` defaults to `false`.
213
231
 
@@ -242,11 +260,11 @@ For example in a Rails controller:
242
260
  # app/controllers/users_controller.rb
243
261
  class UsersController < ApplicationController
244
262
  def create
245
- result = UpdateUser.result(user: user, attributes: user_attributes)
246
- if result.success?
247
- redirect_to result.user
263
+ actor = UpdateUser.result(user: user, attributes: user_attributes)
264
+ if actor.success?
265
+ redirect_to actor.user
248
266
  else
249
- render :new, notice: result.error
267
+ render :new, notice: actor.error
250
268
  end
251
269
  end
252
270
  end
@@ -263,7 +281,7 @@ actor can use `play` to call other actors:
263
281
  ```rb
264
282
  class PlaceOrder < Actor
265
283
  play CreateOrder,
266
- Pay,
284
+ PayOrder,
267
285
  SendOrderConfirmation,
268
286
  NotifyAdmins
269
287
  end
@@ -299,26 +317,51 @@ Rollback is only called on the _previous_ actors in `play` and is not called on
299
317
  the failing actor itself. Actors should be kept to a single purpose and not have
300
318
  anything to clean up if they call `fail!`.
301
319
 
302
- ### Lambdas
320
+ ### Inline actors
303
321
 
304
- You can use inline actions using lambdas. Inside these lambdas you have access to
305
- the shared result:
322
+ For small work or preparing the result set for the next actors, you can create
323
+ inline actors by using lambdas. Each lambda has access to the shared result. For
324
+ example:
306
325
 
307
326
  ```rb
308
- class Pay < Actor
309
- play ->(result) { result.payment_provider = "stripe" },
327
+ class PayOrder < Actor
328
+ input :order
329
+
330
+ play -> actor { actor.order.currency ||= "EUR" },
310
331
  CreatePayment,
311
- ->(result) { result.user_to_notify = result.payment.user },
312
- SendNotification
332
+ UpdateOrderBalance,
333
+ -> actor { Logger.info("Order #{actor.order.id} paid") }
313
334
  end
314
335
  ```
315
336
 
316
- Like in this example, lambdas can be useful for small work or preparing the
317
- result for the next actors. If you want to do more work before, after or around
318
- the whole `play`, you can also override the `call` method. For example:
337
+ You can also call instance methods. For example:
319
338
 
320
339
  ```rb
321
- class Pay < Actor
340
+ class PayOrder < Actor
341
+ input :order
342
+
343
+ play :assign_default_currency,
344
+ CreatePayment,
345
+ UpdateOrderBalance,
346
+ :log_payment
347
+
348
+ private
349
+
350
+ def assign_default_currency
351
+ order.currency ||= "EUR"
352
+ end
353
+
354
+ def log_payment
355
+ Logger.info("Order #{order.id} paid")
356
+ end
357
+ end
358
+ ```
359
+
360
+ If you want to do work around the whole actor, you can also override the `call`
361
+ method. For example:
362
+
363
+ ```rb
364
+ class PayOrder < Actor
322
365
  # …
323
366
 
324
367
  def call
@@ -337,12 +380,10 @@ Actors in a play can be called conditionally:
337
380
  class PlaceOrder < Actor
338
381
  play CreateOrder,
339
382
  Pay
340
- play NotifyAdmins, if: ->(result) { result.order.amount > 42 }
383
+ play NotifyAdmins, if: -> actor { actor.order.amount > 42 }
341
384
  end
342
385
  ```
343
386
 
344
- You can use this to trigger an early success.
345
-
346
387
  ### Fail on argument error
347
388
 
348
389
  By default, errors on inputs will raise an error, even when using `.result`
@@ -392,7 +433,7 @@ Some key differences make Actor unique:
392
433
 
393
434
  - Does not [hide errors when an actor fails inside another
394
435
  actor](https://github.com/collectiveidea/interactor/issues/170).
395
- - Requires you to document all arguments with `input` and `output`.
436
+ - Requires you to document arguments with `input` and `output`.
396
437
  - Defaults to raising errors on failures: actor uses `call` and `result`
397
438
  instead of `call!` and `call`. This way, the _default_ is to raise an error
398
439
  and failures are not hidden away because you forgot to use `!`.
@@ -401,7 +442,8 @@ Some key differences make Actor unique:
401
442
  `context.foo = `, `fail!` vs `context.fail!`.
402
443
  - Shorter setup syntax: inherit from `< Actor` vs having to `include Interactor`
403
444
  and `include Interactor::Organizer`.
404
- - Organizers allow lambdas, being called multiple times, and having conditions.
445
+ - Organizers allow lambdas, instance methods, being called multiple times,
446
+ and having conditions.
405
447
  - Allows early success with conditions inside organizers.
406
448
  - No `before`, `after` and `around` hooks, prefer using `play` with lambdas or
407
449
  overriding `call`.
@@ -414,33 +456,17 @@ migration.
414
456
  Thank you to @nicoolas25, @AnneSottise & @williampollet for the early thoughts
415
457
  and feedback on this gem.
416
458
 
417
- Photo by [Lloyd Dirks](https://unsplash.com/photos/4SLz_RCk6kQ).
418
-
419
- ## Development
459
+ Thank you to the wonderful
460
+ [contributors](https://github.com/sunny/actor/graphs/contributors).
420
461
 
421
- After checking out the repo, run `bin/setup` to install dependencies. Then, run
422
- `bin/rake` to run the tests and linting. You can also run `bin/console` for an
423
- interactive prompt.
424
-
425
- To release a new version, update the version number in `version.rb`, and in the
426
- `CHANGELOG.md`. Update the `README.md` if there are missing segments, make sure
427
- tests and linting are pristine by calling `bundle && bin/rake`, then create a
428
- commit for this version.
429
-
430
- You can then run `rake release`, which will assign a git tag, push using git,
431
- and push the gem to [rubygems.org](https://rubygems.org).
462
+ Photo by [Lloyd Dirks](https://unsplash.com/photos/4SLz_RCk6kQ).
432
463
 
433
464
  ## Contributing
434
465
 
435
- Bug reports and pull requests are welcome
436
- [on GitHub](https://github.com/sunny/actor).
437
-
438
- This project is intended to be a safe, welcoming space for collaboration, and
439
- everyone interacting in the project’s codebase and issue tracker is expected to
440
- adhere to the [Contributor Covenant code of
441
- conduct](https://github.com/sunny/actor/blob/main/CODE_OF_CONDUCT.md).
466
+ See
467
+ [CONTRIBUTING.md](https://github.com/sunny/actor/blob/main/CONTRIBUTING.md).
442
468
 
443
469
  ## License
444
470
 
445
471
  The gem is available as open source under the terms of the
446
- [MIT License](https://opensource.org/licenses/MIT).
472
+ [MIT License](https://choosealicense.com/licenses/mit/).
@@ -1,23 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Exceptions
4
- require 'service_actor/error'
5
- require 'service_actor/failure'
6
- require 'service_actor/argument_error'
4
+ require "service_actor/error"
5
+ require "service_actor/failure"
6
+ require "service_actor/argument_error"
7
7
 
8
8
  # Core
9
- require 'service_actor/core'
10
- require 'service_actor/attributable'
11
- require 'service_actor/playable'
12
- require 'service_actor/result'
9
+ require "service_actor/core"
10
+ require "service_actor/attributable"
11
+ require "service_actor/playable"
12
+ require "service_actor/result"
13
13
 
14
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'
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
21
 
22
22
  module ServiceActor
23
23
  module Base
@@ -7,7 +7,7 @@ module ServiceActor
7
7
  # Example:
8
8
  #
9
9
  # class Pay < Actor
10
- # input :provider, in: ['MANGOPAY', 'PayPal', 'Stripe']
10
+ # input :provider, in: ["MANGOPAY", "PayPal", "Stripe"]
11
11
  # end
12
12
  module Collectionable
13
13
  def self.included(base)
@@ -11,7 +11,7 @@ module ServiceActor
11
11
  # class Pay < Actor
12
12
  # input :provider,
13
13
  # must: {
14
- # exist: ->(provider) { PROVIDERS.include?(provider) }
14
+ # exist: -> provider { PROVIDERS.include?(provider) },
15
15
  # }
16
16
  # end
17
17
  module Conditionable
@@ -9,9 +9,9 @@ module ServiceActor
9
9
  module ClassMethods
10
10
  # Call an actor with a given result. Returns the result.
11
11
  #
12
- # CreateUser.call(name: 'Joe')
13
- def call(options = nil, **arguments)
14
- result = Result.to_result(options).merge!(arguments)
12
+ # CreateUser.call(name: "Joe")
13
+ def call(result = nil, **arguments)
14
+ result = Result.to_result(result).merge!(arguments)
15
15
  new(result)._call
16
16
  result
17
17
  end
@@ -19,9 +19,9 @@ module ServiceActor
19
19
  # Call an actor with arguments. Returns the result and does not raise on
20
20
  # failure.
21
21
  #
22
- # CreateUser.result(name: 'Joe')
23
- def result(data = nil, **arguments)
24
- call(data, **arguments)
22
+ # CreateUser.result(name: "Joe")
23
+ def result(result = nil, **arguments)
24
+ call(result, **arguments)
25
25
  rescue Failure => e
26
26
  e.result
27
27
  end
@@ -16,11 +16,11 @@ module ServiceActor
16
16
 
17
17
  module PrependedMethods
18
18
  def _call
19
- check_context_for_nil(self.class.inputs, origin: 'input')
19
+ check_context_for_nil(self.class.inputs, origin: "input")
20
20
 
21
21
  super
22
22
 
23
- check_context_for_nil(self.class.outputs, origin: 'output')
23
+ check_context_for_nil(self.class.outputs, origin: "output")
24
24
  end
25
25
 
26
26
  private
@@ -31,7 +31,7 @@ module ServiceActor
31
31
 
32
32
  raise ArgumentError,
33
33
  "The #{origin} \"#{name}\" on #{self.class} does not allow " \
34
- 'nil values'
34
+ "nil values"
35
35
  end
36
36
  end
37
37
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  module ServiceActor
4
4
  # Play class method to call a series of actors with the same result. On
5
- # failure, calls rollback on any actor that succeeded.
5
+ # failure, calls rollback on actors that succeeded.
6
6
  #
7
7
  # class CreateUser < Actor
8
8
  # play SaveUser,
@@ -16,14 +16,6 @@ module ServiceActor
16
16
  end
17
17
 
18
18
  module ClassMethods
19
- def inherited(child)
20
- super
21
-
22
- play_actors.each do |actor|
23
- child.play_actors << actor
24
- end
25
- end
26
-
27
19
  def play(*actors, **options)
28
20
  actors.each do |actor|
29
21
  play_actors.push({ actor: actor, **options })
@@ -33,6 +25,14 @@ module ServiceActor
33
25
  def play_actors
34
26
  @play_actors ||= []
35
27
  end
28
+
29
+ def inherited(child)
30
+ super
31
+
32
+ play_actors.each do |actor|
33
+ child.play_actors << actor
34
+ end
35
+ end
36
36
  end
37
37
 
38
38
  module PrependedMethods
@@ -61,6 +61,7 @@ module ServiceActor
61
61
 
62
62
  def play_actor(actor)
63
63
  play_service_actor(actor) ||
64
+ play_symbol(actor) ||
64
65
  play_interactor(actor) ||
65
66
  actor.call(result)
66
67
  end
@@ -75,9 +76,17 @@ module ServiceActor
75
76
  (@played ||= []).unshift(actor)
76
77
  end
77
78
 
79
+ def play_symbol(actor)
80
+ return unless actor.is_a?(Symbol)
81
+
82
+ send(actor)
83
+
84
+ true
85
+ end
86
+
78
87
  def play_interactor(actor)
79
88
  return unless actor.is_a?(Class)
80
- return unless actor.ancestors.map(&:name).include?('Interactor')
89
+ return unless actor.ancestors.map(&:name).include?("Interactor")
81
90
 
82
91
  result.merge!(actor.call(result).to_h)
83
92
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ostruct'
3
+ require "ostruct"
4
4
 
5
5
  module ServiceActor
6
6
  # Represents the context of an actor, holding the data from both its inputs
@@ -51,5 +51,28 @@ module ServiceActor
51
51
  def display
52
52
  to_h.fetch(:display)
53
53
  end
54
+
55
+ private
56
+
57
+ def respond_to_missing?(method_name, include_private = false)
58
+ method_name.to_s.end_with?("?") || super
59
+ end
60
+
61
+ def method_missing(symbol, *args)
62
+ attribute = symbol.to_s.chomp("?")
63
+
64
+ if symbol.to_s.end_with?("?") && respond_to?(attribute)
65
+ define_singleton_method symbol do
66
+ attribute_value = send(attribute.to_sym)
67
+
68
+ # Same as ActiveSupport’s #present?
69
+ attribute_value.respond_to?(:empty?) ? !attribute_value.empty? : !!attribute_value
70
+ end
71
+
72
+ return send(symbol)
73
+ end
74
+
75
+ super symbol, *args
76
+ end
54
77
  end
55
78
  end
@@ -7,7 +7,7 @@ module ServiceActor
7
7
  # Example:
8
8
  #
9
9
  # class ReduceOrderAmount < Actor
10
- # input :order, type: 'Order'
10
+ # input :order, type: "Order"
11
11
  # input :amount, type: [Integer, Float]
12
12
  # input :bonus_applied, type: [TrueClass FalseClass]
13
13
  # end
@@ -18,11 +18,11 @@ module ServiceActor
18
18
 
19
19
  module PrependedMethods
20
20
  def _call
21
- check_type_definitions(self.class.inputs, kind: 'Input')
21
+ check_type_definitions(self.class.inputs, kind: "Input")
22
22
 
23
23
  super
24
24
 
25
- check_type_definitions(self.class.outputs, kind: 'Output')
25
+ check_type_definitions(self.class.outputs, kind: "Output")
26
26
  end
27
27
 
28
28
  private
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ServiceActor
4
- VERSION = '3.1.3'
4
+ VERSION = "3.2.0"
5
5
  end
data/lib/service_actor.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'service_actor/base'
3
+ require "service_actor/base"
4
4
 
5
5
  # Class to inherit from in your application.
6
6
  class 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.1.3
4
+ version: 3.2.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: 2022-01-21 00:00:00.000000000 Z
11
+ date: 2022-07-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -53,19 +53,25 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: rubocop
56
+ name: rubocop-lts
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '12.0'
59
62
  - - ">="
60
63
  - !ruby/object:Gem::Version
61
- version: '0'
64
+ version: 12.0.1
62
65
  type: :development
63
66
  prerelease: false
64
67
  version_requirements: !ruby/object:Gem::Requirement
65
68
  requirements:
69
+ - - "~>"
70
+ - !ruby/object:Gem::Version
71
+ version: '12.0'
66
72
  - - ">="
67
73
  - !ruby/object:Gem::Version
68
- version: '0'
74
+ version: 12.0.1
69
75
  - !ruby/object:Gem::Dependency
70
76
  name: rubocop-rspec
71
77
  requirement: !ruby/object:Gem::Requirement