service_actor 1.0.0 → 3.1.1

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: a16f2ccd8bf0d2e83d5cb9ec163c81b9df3be06515cb716138049f9d33420759
4
- data.tar.gz: bfb6c4dca34cb64fa3c8ce84b085faa85547c1a554f241ccf49d92a7a67d012b
3
+ metadata.gz: c9d2367f70743f81df7df55ba4bc9a7fbcb5c27c08c2219abad25139763914f3
4
+ data.tar.gz: 107897dcd684becef83e44d9f07a2061b41a36d726120a92ebfef5ba549b4375
5
5
  SHA512:
6
- metadata.gz: 18df62d093865eefd2acd8a9d3567c1379be5c097c087b6c2d2eee1dc8cf0485e5b0b6f712b11a19492eca3465b48ba0281de7b88130e170681923ec3cc2086c
7
- data.tar.gz: 80df1b4cc8269802cfe217663fa16ed907103a1b3e4b8ee42d1d01ac08c47a571f4993439f27d1065fbbb38738132be459b042aadfd04bdfe229bb6a2a4dbc27
6
+ metadata.gz: 335b5893e4cc4bc8c506c0503700f2617ba845fcf9d819a70fd4e42e67116c9cd3fbf9570a4ab2660f54c00b112a49108581d1858a65bbfa3e77f97417b8336f
7
+ data.tar.gz: d450f69555986cf33a96fe52b5fa254e768c3c96f60e7c9dbcd345906c3c66bd4f92149e0de66d87c78059c5ecf5ba6b936705a57cbec2a773136b18bdab22ff
data/README.md CHANGED
@@ -2,19 +2,45 @@
2
2
 
3
3
  ![Tests](https://github.com/sunny/actor/workflows/Tests/badge.svg)
4
4
 
5
- Ruby service objects. Lets you move your application logic into small
6
- building blocs to keep your controllers and your models thin.
5
+ This Ruby gem lets you move your application logic into into small composable
6
+ service objects. It is a lightweight framework that helps you keep your models
7
+ and controllers thin.
8
+
9
+ ![Photo of theater seats](https://user-images.githubusercontent.com/132/78340166-e7567000-7595-11ea-97c0-b3e5da2de7a1.png)
10
+
11
+ ## Contents
12
+
13
+ - [Installation](#installation)
14
+ - [Usage](#usage)
15
+ - [Inputs](#inputs)
16
+ - [Outputs](#outputs)
17
+ - [Defaults](#defaults)
18
+ - [Conditions](#conditions)
19
+ - [Allow nil](#allow-nil)
20
+ - [Types](#types)
21
+ - [Result](#result)
22
+ - [Play actors in a sequence](#play-actors-in-a-sequence)
23
+ - [Rollback](#rollback)
24
+ - [Lambdas](#lambdas)
25
+ - [Play conditions](#play-conditions)
26
+ - [Use with Rails](#use-with-rails)
27
+ - [Testing](#testing)
28
+ - [Build your own actor](#build-your-own-actor)
29
+ - [Influences](#influences)
30
+ - [Thanks](#thanks)
31
+ - [Development](#development)
32
+ - [Contributing](#contributing)
33
+ - [License](#contributing)
7
34
 
8
35
  ## Installation
9
36
 
10
37
  Add these lines to your application's Gemfile:
11
38
 
12
39
  ```rb
13
- # Service objects to keep the business logic
40
+ # Composable service objects
14
41
  gem 'service_actor'
15
42
  ```
16
43
 
17
-
18
44
  ## Usage
19
45
 
20
46
  Actors are single-purpose actions in your application that represent your
@@ -30,15 +56,19 @@ class SendNotification < Actor
30
56
  end
31
57
  ```
32
58
 
33
- Use `.call` to use them in your application:
59
+ Trigger them in your application with `.call`:
34
60
 
35
61
  ```rb
36
- SendNotification.call
62
+ SendNotification.call # => <ServiceActor::Result…>
37
63
  ```
38
64
 
65
+ When called, actors return a Result. Reading and writing to this result allows
66
+ actors to accept and return multiple arguments. Let's find out how to do that
67
+ and then we'll see how to chain multiple actors togethor.
68
+
39
69
  ### Inputs
40
70
 
41
- Actors can accept arguments with `input`:
71
+ To accept arguments, use `input` to create a method named after this input:
42
72
 
43
73
  ```rb
44
74
  class GreetUser < Actor
@@ -50,7 +80,7 @@ class GreetUser < Actor
50
80
  end
51
81
  ```
52
82
 
53
- And receive them as arguments to `call`:
83
+ You can now call your actor by providing the correct arguments:
54
84
 
55
85
  ```rb
56
86
  GreetUser.call(user: User.first)
@@ -58,20 +88,20 @@ GreetUser.call(user: User.first)
58
88
 
59
89
  ### Outputs
60
90
 
61
- Use `output` to declare what your actor can return, then assign them to your
62
- context.
91
+ An actor can return multiple arguments. Declare them using `output`, which adds
92
+ a setter method to let you modify the result from your actor:
63
93
 
64
94
  ```rb
65
95
  class BuildGreeting < Actor
66
96
  output :greeting
67
97
 
68
98
  def call
69
- context.greeting = "Have a wonderful day!"
99
+ self.greeting = 'Have a wonderful day!'
70
100
  end
71
101
  end
72
102
  ```
73
103
 
74
- Calling an actor returns a context:
104
+ The result you get from calling an actor will include the outputs you set:
75
105
 
76
106
  ```rb
77
107
  result = BuildGreeting.call
@@ -80,39 +110,53 @@ result.greeting # => "Have a wonderful day!"
80
110
 
81
111
  ### Defaults
82
112
 
83
- Inputs can have defaults:
113
+ Inputs can be marked as optional by providing a default:
84
114
 
85
115
  ```rb
86
- class PrintWelcome < Actor
87
- input :user
88
- input :adjective, default: "wonderful"
89
- input :length_of_time, default: -> { ["day", "week", "month"].sample }
116
+ class BuildGreeting < Actor
117
+ input :name
118
+ input :adjective, default: 'wonderful'
119
+ input :length_of_time, default: -> { ['day', 'week', 'month'].sample }
90
120
 
91
121
  output :greeting
92
122
 
93
123
  def call
94
- context.greeting = "Hello #{name}! Have a #{adjective} #{length_of_time}!"
124
+ self.greeting = "Have a #{adjective} #{length_of_time} #{name}!"
95
125
  end
96
126
  end
97
127
  ```
98
128
 
99
- ### Types
129
+ This lets you call the actor without specifying those keys:
100
130
 
101
- Inputs can define a type, or an array of possible types it must match:
131
+ ```rb
132
+ result = BuildGreeting.call(name: 'Jim')
133
+ result.greeting # => "Have a wonderful week Jim!"
134
+ ```
135
+
136
+ If an input does not have a default, it will raise a error:
102
137
 
103
138
  ```rb
104
- class UpdateUser < Actor
105
- input :user, type: 'User'
106
- input :age, type: %w[Integer Float]
139
+ result = BuildGreeting.call
140
+ => ServiceActor::ArgumentError: Input name on BuildGreeting is missing.
141
+ ```
142
+
143
+ ### Conditions
144
+
145
+ You can ensure an input is included in a collection by using `in` (unreleased
146
+ yet):
147
+
148
+ ```rb
149
+ class Pay < Actor
150
+ input :currency, in: %w[EUR USD]
107
151
 
108
152
  # …
109
153
  end
110
154
  ```
111
155
 
112
- ### Conditions
156
+ This raises an argument error if the input does not match one of the given
157
+ values.
113
158
 
114
- If types don't cut it, you can add small conditions with the name of your choice
115
- under `must`:
159
+ You can also add custom conditions with the name of your choice by using `must`:
116
160
 
117
161
  ```rb
118
162
  class UpdateAdminUser < Actor
@@ -120,13 +164,51 @@ class UpdateAdminUser < Actor
120
164
  must: {
121
165
  be_an_admin: ->(user) { user.admin? }
122
166
  }
167
+
168
+ # …
123
169
  end
124
170
  ```
125
171
 
172
+ This raises an argument error if the given lambda returns a falsey value.
173
+
174
+ ### Allow nil
175
+
176
+ By default inputs accept `nil` values. To raise an error instead:
177
+
178
+ ```rb
179
+ class UpdateUser < Actor
180
+ input :user, allow_nil: false
181
+
182
+ # …
183
+ end
184
+ ```
185
+
186
+ ### Types
187
+
188
+ Sometimes it can help to have a quick way of making sure we didn't mess up our
189
+ inputs.
190
+
191
+ For that you can use the `type` option and giving a class or an array
192
+ of possible classes. If the input or output doesn't match is not an instance of
193
+ these types, an error is raised.
194
+
195
+ ```rb
196
+ class UpdateUser < Actor
197
+ input :user, type: User
198
+ input :age, type: [Integer, Float]
199
+
200
+ # …
201
+ end
202
+ ```
203
+
204
+ You may also use strings instead of constants, such as `type: 'User'`.
205
+
206
+ When using a type condition, `allow_nil` defaults to `false`.
207
+
126
208
  ### Result
127
209
 
128
- All actors are successful by default. To stop its execution and mark is as
129
- having failed, use `fail!`:
210
+ All actors return a successful result by default. To stop the execution and
211
+ mark an actor as having failed, use `fail!`:
130
212
 
131
213
  ```rb
132
214
  class UpdateUser
@@ -136,16 +218,17 @@ class UpdateUser
136
218
  def call
137
219
  user.attributes = attributes
138
220
 
139
- fail!(error: "Invalid user") unless user.valid?
221
+ fail!(error: 'Invalid user') unless user.valid?
140
222
 
141
223
  # …
142
224
  end
143
225
  end
144
226
  ```
145
227
 
146
- You can then test for the success by calling your actor with `.result` instead
147
- of `.call`. This will let you test for `success?` or `failure?` on the context
148
- instead of raising an exception.
228
+ This will raise an error in your app with the given data added to the result.
229
+
230
+ To test for the success of your actor instead of raising an exception, use
231
+ `.result` instead of `.call` and call `success?` or `failure?` on the result.
149
232
 
150
233
  For example in a Rails controller:
151
234
 
@@ -163,10 +246,13 @@ class UsersController < ApplicationController
163
246
  end
164
247
  ```
165
248
 
166
- ### Play
249
+ Any keys you add to `fail!` will be added to the result, for example you could
250
+ do: `fail!(error_type: "validation", error_code: "uv52", …)`.
251
+
252
+ ## Play actors in a sequence
167
253
 
168
- An actor can call actors in sequence by using `play`. Each actor will hand over
169
- the context to the next actor.
254
+ To help you create actors that are small, single-responsibility actions, an
255
+ actor can use `play` to call other actors:
170
256
 
171
257
  ```rb
172
258
  class PlaceOrder < Actor
@@ -177,51 +263,56 @@ class PlaceOrder < Actor
177
263
  end
178
264
  ```
179
265
 
266
+ This creates a `call` method that will call every actor along the way. Inputs
267
+ and outputs will go from one actor to the next, all sharing the same result set
268
+ until it is finally returned.
269
+
180
270
  ### Rollback
181
271
 
182
- When using `play`, if one of the actors calls `fail!`, the following actors will
183
- not be called.
272
+ When using `play`, when an actor calls `fail!`, the following actors will not be
273
+ called.
184
274
 
185
- Also, any _previous_ actor that succeeded will call the `rollback` method, if
186
- you defined one.
275
+ Instead, all the actors that succeeded will have their `rollback` method called
276
+ in reverse order. This allows actors a chance to cleanup, for example:
187
277
 
188
278
  ```rb
189
279
  class CreateOrder < Actor
280
+ output :order
281
+
190
282
  def call
191
- context.order = Order.create!(…)
283
+ self.order = Order.create!(…)
192
284
  end
193
285
 
194
286
  def rollback
195
- context.order.destroy
287
+ order.destroy
196
288
  end
197
289
  end
198
290
  ```
199
291
 
200
- ### Early success
201
-
202
- When using `play` you can use `succeed!` so that the following actors will not
203
- be called, but still consider the actor to be successful.
292
+ Rollback is only called on the _previous_ actors in `play` and is not called on
293
+ the failing actor itself. Actors should be kept to a single purpose and not have
294
+ anything to clean up if they call `fail!`.
204
295
 
205
296
  ### Lambdas
206
297
 
207
- You can call inline actions using lambdas:
298
+ You can use inline actions using lambdas. Inside these lambdas, you don't have
299
+ getters and setters but have access to the shared result:
208
300
 
209
301
  ```rb
210
- class Pay
211
- play ->(ctx) { ctx.payment_provider = "stripe" },
302
+ class Pay < Actor
303
+ play ->(result) { result.payment_provider = "stripe" },
212
304
  CreatePayment,
213
- ->(ctx) { ctx.user_to_notify = ctx.payment.user },
305
+ ->(result) { result.user_to_notify = result.payment.user },
214
306
  SendNotification
215
307
  end
216
308
  ```
217
309
 
218
- ### Before, after and around
219
-
220
- To do actions before or after actors, use lambdas or simply override `call` and
221
- use `super`. For example:
310
+ Like in this example, lambdas can be useful for small work or preparing the
311
+ result for the next actors. If you want to do more work before, or after the
312
+ whole `play`, you can also override the `call` method. For example:
222
313
 
223
314
  ```rb
224
- class Pay
315
+ class Pay < Actor
225
316
  # …
226
317
 
227
318
  def call
@@ -234,13 +325,59 @@ end
234
325
 
235
326
  ### Play conditions
236
327
 
237
- Some actors in a play can be called conditionaly:
328
+ Actors in a play can be called conditionally:
238
329
 
239
330
  ```rb
240
331
  class PlaceOrder < Actor
241
332
  play CreateOrder,
242
333
  Pay
243
- play NotifyAdmins, if: ->(ctx) { ctx.order.amount > 42 }
334
+ play NotifyAdmins, if: ->(result) { result.order.amount > 42 }
335
+ end
336
+ ```
337
+
338
+ You can use this to trigger an early success.
339
+
340
+ ### Fail on argument error
341
+
342
+ By default, errors on inputs will raise an error, even when using `.result`
343
+ instead of `.call`. If instead you want to mark the actor as failed, you can
344
+ catch the exception to treat it as an actor failure:
345
+
346
+ ```rb
347
+ class PlaceOrder < Actor
348
+ fail_on ServiceActor::ArgumentError
349
+
350
+ # …
351
+ end
352
+ ```
353
+
354
+ ## Use with Rails
355
+
356
+ The [service_actor-rails](https://github.com/sunny/actor-rails/) gem includes a
357
+ Rails generator to create an actor and its spec.
358
+
359
+ ## Testing
360
+
361
+ In your application, add automated testing to your actors as you would do to any
362
+ other part of your applications.
363
+
364
+ You will find that cutting your business logic into single purpose actors will
365
+ make it easier for you to test your application.
366
+
367
+ ## Build your own actor
368
+
369
+ If you application already uses an "Actor" class, you can build your own by
370
+ changing the gem's require statement:
371
+
372
+ ```rb
373
+ gem 'service_actor', require: 'service_actor/base'
374
+ ```
375
+
376
+ And building your own class to inherit from:
377
+
378
+ ```rb
379
+ class ApplicationActor
380
+ include ServiceActor::Base
244
381
  end
245
382
  ```
246
383
 
@@ -248,51 +385,57 @@ end
248
385
 
249
386
  This gem is heavily influenced by
250
387
  [Interactor](https://github.com/collectiveidea/interactor) ♥.
251
- However there a a few key differences which make `actor` unique:
252
-
253
- - Defaults to raising errors on failures. Actor uses `call` and `result` instead of `call!` and `call`. This way, the default is to raise an error and failures are not hidden away.
254
- - Does not [hide errors when an actor fails inside another actor](https://github.com/collectiveidea/interactor/issues/170).
255
- - You can use lambdas inside organizers.
256
- - Requires you to document the arguments with `input` and `output`.
257
- - Type checking of inputs and outputs.
258
- - Inputs and outputs can be required.
259
- - Defaults for inputs.
260
- - Conditions on inputs.
261
- - Shorter fail syntax: `fail!` vs `context.fail!`.
262
- - Trigger early success in organisers with `succeed!`.
263
- - Shorter setup syntax: inherit from `< Actor` vs having to `include Interactor` or `include Interactor::Organizer`.
264
- - Multiple organizers.
265
- - Conditions inside organizers.
266
- - No `before`, `after` and `around` hooks. Prefer simply overriding `call` with `super` which allows wrapping the whole method.
267
- - [Does not rely on `OpenStruct`](https://github.com/collectiveidea/interactor/issues/183)
268
- - Does not print warnings on Ruby 2.7.
269
-
388
+ However there are a few key differences which make `actor` unique:
389
+
390
+ - Does not [hide errors when an actor fails inside another
391
+ actor](https://github.com/collectiveidea/interactor/issues/170).
392
+ - Requires you to document all arguments with `input` and `output`.
393
+ - Defaults to raising errors on failures: actor uses `call` and `result`
394
+ instead of `call!` and `call`. This way, the _default_ is to raise an error
395
+ and failures are not hidden away because you forgot to use `!`.
396
+ - Allows defaults, type checking, requirements and conditions on inputs.
397
+ - Delegates methods on the context: `foo` vs `context.foo`, `self.foo =` vs
398
+ `context.foo = `, `fail!` vs `context.fail!`.
399
+ - Shorter setup syntax: inherit from `< Actor` vs having to `include Interactor`
400
+ and `include Interactor::Organizer`.
401
+ - Organizers allow lambdas, being called multiple times, and having conditions.
402
+ - Allows early success with conditions inside organizers.
403
+ - No `before`, `after` and `around` hooks, prefer using `play` with lambdas or
404
+ overriding `call`.
405
+
406
+ Actor supports mixing actors & interactors when using `play` for a smooth
407
+ migration.
408
+
409
+ ## Thanks
410
+
411
+ Thank you to @nicoolas25, @AnneSottise & @williampollet for the early thoughts
412
+ and feedback on this gem.
413
+
414
+ Photo by [Lloyd Dirks](https://unsplash.com/photos/4SLz_RCk6kQ).
270
415
 
271
416
  ## Development
272
417
 
273
418
  After checking out the repo, run `bin/setup` to install dependencies. Then, run
274
- `rake` to run the tests. You can also run `bin/console` for an interactive
275
- prompt that will allow you to experiment.
419
+ `rake` to run the tests and linting. You can also run `bin/console` for an
420
+ interactive prompt.
276
421
 
277
- To install this gem onto your local machine, run `bundle exec rake install`.
278
- To release a new version, update the version number in `version.rb`, and then
279
- run `bundle exec rake release`, which will create a git tag for the version,
280
- push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
422
+ To release a new version, update the version number in `version.rb`, and in the
423
+ `CHANGELOG.md`. Update the `README.md` if there are missing segments, make sure
424
+ tests and linting are pristine by calling `bundle && rake`, then create a commit
425
+ for this version. You can then run `rake release`, which will assign a git tag,
426
+ push using git, and push the gem to [rubygems.org](https://rubygems.org).
281
427
 
282
428
  ## Contributing
283
429
 
284
- Bug reports and pull requests are welcome on GitHub at
285
- https://github.com/sunny/actor. This project is intended to be a safe,
286
- welcoming space for collaboration, and contributors are expected to adhere to
287
- the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
430
+ Bug reports and pull requests are welcome
431
+ [on GitHub](https://github.com/sunny/actor).
432
+
433
+ This project is intended to be a safe, welcoming space for collaboration, and
434
+ everyone interacting in the project’s codebase and issue tracker is expected to
435
+ adhere to the [Contributor Covenant code of
436
+ conduct](https://github.com/sunny/actor/blob/main/CODE_OF_CONDUCT.md).
288
437
 
289
438
  ## License
290
439
 
291
440
  The gem is available as open source under the terms of the
292
441
  [MIT License](https://opensource.org/licenses/MIT).
293
-
294
- ## Code of Conduct
295
-
296
- Everyone interacting in the Test project’s codebases, issue trackers, chat
297
- rooms and mailing lists is expected to follow the
298
- [code of conduct](https://github.com/sunny/actor/blob/master/CODE_OF_CONDUCT.md).