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 +4 -4
- data/README.md +234 -91
- data/lib/service_actor.rb +3 -83
- data/lib/service_actor/argument_error.rb +6 -0
- data/lib/{actor → service_actor}/attributable.rb +14 -17
- data/lib/service_actor/base.rb +41 -0
- data/lib/service_actor/collectionable.rb +32 -0
- data/lib/service_actor/conditionable.rb +40 -0
- data/lib/service_actor/core.rb +56 -0
- data/lib/service_actor/defaultable.rb +37 -0
- data/lib/service_actor/error.rb +6 -0
- data/lib/service_actor/failable.rb +40 -0
- data/lib/service_actor/failure.rb +16 -0
- data/lib/service_actor/nil_checkable.rb +51 -0
- data/lib/{actor → service_actor}/playable.rb +10 -9
- data/lib/service_actor/result.rb +55 -0
- data/lib/service_actor/type_checkable.rb +51 -0
- data/lib/service_actor/version.rb +5 -0
- metadata +76 -16
- data/lib/actor/conditionable.rb +0 -34
- data/lib/actor/context.rb +0 -92
- data/lib/actor/defaultable.rb +0 -25
- data/lib/actor/failure.rb +0 -16
- data/lib/actor/filtered_context.rb +0 -49
- data/lib/actor/requireable.rb +0 -36
- data/lib/actor/success.rb +0 -6
- data/lib/actor/type_checkable.rb +0 -43
- data/lib/actor/version.rb +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c9d2367f70743f81df7df55ba4bc9a7fbcb5c27c08c2219abad25139763914f3
|
4
|
+
data.tar.gz: 107897dcd684becef83e44d9f07a2061b41a36d726120a92ebfef5ba549b4375
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 335b5893e4cc4bc8c506c0503700f2617ba845fcf9d819a70fd4e42e67116c9cd3fbf9570a4ab2660f54c00b112a49108581d1858a65bbfa3e77f97417b8336f
|
7
|
+
data.tar.gz: d450f69555986cf33a96fe52b5fa254e768c3c96f60e7c9dbcd345906c3c66bd4f92149e0de66d87c78059c5ecf5ba6b936705a57cbec2a773136b18bdab22ff
|
data/README.md
CHANGED
@@ -2,19 +2,45 @@
|
|
2
2
|
|
3
3
|

|
4
4
|
|
5
|
-
Ruby
|
6
|
-
|
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
|
+

|
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
|
-
#
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
62
|
-
|
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
|
-
|
99
|
+
self.greeting = 'Have a wonderful day!'
|
70
100
|
end
|
71
101
|
end
|
72
102
|
```
|
73
103
|
|
74
|
-
|
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
|
113
|
+
Inputs can be marked as optional by providing a default:
|
84
114
|
|
85
115
|
```rb
|
86
|
-
class
|
87
|
-
input :
|
88
|
-
input :adjective, default:
|
89
|
-
input :length_of_time, default: -> { [
|
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
|
-
|
124
|
+
self.greeting = "Have a #{adjective} #{length_of_time} #{name}!"
|
95
125
|
end
|
96
126
|
end
|
97
127
|
```
|
98
128
|
|
99
|
-
|
129
|
+
This lets you call the actor without specifying those keys:
|
100
130
|
|
101
|
-
|
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
|
-
|
105
|
-
|
106
|
-
|
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
|
-
|
156
|
+
This raises an argument error if the input does not match one of the given
|
157
|
+
values.
|
113
158
|
|
114
|
-
|
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
|
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:
|
221
|
+
fail!(error: 'Invalid user') unless user.valid?
|
140
222
|
|
141
223
|
# …
|
142
224
|
end
|
143
225
|
end
|
144
226
|
```
|
145
227
|
|
146
|
-
|
147
|
-
|
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
|
-
|
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
|
-
|
169
|
-
|
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`,
|
183
|
-
|
272
|
+
When using `play`, when an actor calls `fail!`, the following actors will not be
|
273
|
+
called.
|
184
274
|
|
185
|
-
|
186
|
-
|
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
|
-
|
283
|
+
self.order = Order.create!(…)
|
192
284
|
end
|
193
285
|
|
194
286
|
def rollback
|
195
|
-
|
287
|
+
order.destroy
|
196
288
|
end
|
197
289
|
end
|
198
290
|
```
|
199
291
|
|
200
|
-
|
201
|
-
|
202
|
-
|
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
|
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 ->(
|
302
|
+
class Pay < Actor
|
303
|
+
play ->(result) { result.payment_provider = "stripe" },
|
212
304
|
CreatePayment,
|
213
|
-
->(
|
305
|
+
->(result) { result.user_to_notify = result.payment.user },
|
214
306
|
SendNotification
|
215
307
|
end
|
216
308
|
```
|
217
309
|
|
218
|
-
|
219
|
-
|
220
|
-
|
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
|
-
|
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: ->(
|
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
|
252
|
-
|
253
|
-
-
|
254
|
-
|
255
|
-
-
|
256
|
-
-
|
257
|
-
|
258
|
-
|
259
|
-
-
|
260
|
-
-
|
261
|
-
|
262
|
-
-
|
263
|
-
|
264
|
-
-
|
265
|
-
-
|
266
|
-
- No `before`, `after` and `around` hooks
|
267
|
-
|
268
|
-
|
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
|
275
|
-
prompt
|
419
|
+
`rake` to run the tests and linting. You can also run `bin/console` for an
|
420
|
+
interactive prompt.
|
276
421
|
|
277
|
-
To
|
278
|
-
|
279
|
-
|
280
|
-
|
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
|
285
|
-
https://github.com/sunny/actor.
|
286
|
-
|
287
|
-
|
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).
|