service_actor 1.1.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +181 -96
- data/lib/service_actor.rb +3 -85
- data/lib/service_actor/argument_error.rb +6 -0
- data/lib/{actor → service_actor}/attributable.rb +14 -17
- data/lib/service_actor/base.rb +36 -0
- data/lib/service_actor/conditionable.rb +38 -0
- data/lib/service_actor/core.rb +80 -0
- data/lib/service_actor/defaultable.rb +36 -0
- data/lib/service_actor/error.rb +6 -0
- data/lib/service_actor/failure.rb +16 -0
- data/lib/service_actor/nil_checkable.rb +64 -0
- data/lib/{actor → service_actor}/playable.rb +8 -8
- data/lib/{actor/context.rb → service_actor/result.rb} +18 -13
- data/lib/{actor → service_actor}/success.rb +3 -2
- data/lib/service_actor/type_checkable.rb +52 -0
- data/lib/service_actor/version.rb +5 -0
- metadata +30 -13
- data/lib/actor/conditionable.rb +0 -34
- data/lib/actor/defaultable.rb +0 -30
- 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/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: e103cfa965d8a142d4d47ad910bd4411f90392ab4057b8536a265e12febbf42b
|
4
|
+
data.tar.gz: cf297af32084c0a7ff7d3f85ecab5f52137516d5fc2d2c0aa1f5a1fb1aceccfd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 76cc57cc0c3eca0c951f3bf046e7132bd28157081ff7a815fa44b9ef6375180f035aecee14bf735d0a655e72fa48f840610694c505f90e81cb72e84bc67ea7b7
|
7
|
+
data.tar.gz: 77c0cae0fe870e7a771023a45fbe919eeda92b8ed6e6d32187b4a85ddf906fbae27b1b615830d659214754914a7c0038ecdbba4a7b88b81234e2b96cdc08486c
|
data/README.md
CHANGED
@@ -2,19 +2,41 @@
|
|
2
2
|
|
3
3
|
![Tests](https://github.com/sunny/actor/workflows/Tests/badge.svg)
|
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
|
+
## Contents
|
10
|
+
|
11
|
+
- [Installation](#installation)
|
12
|
+
- [Usage](#usage)
|
13
|
+
- [Inputs](#inputs)
|
14
|
+
- [Outputs](#outputs)
|
15
|
+
- [Defaults](#defaults)
|
16
|
+
- [Conditions](#conditions)
|
17
|
+
- [Allow nil](#allow-nil)
|
18
|
+
- [Types](#types)
|
19
|
+
- [Result](#result)
|
20
|
+
- [Play actors in a sequence](#play-actors-in-a-sequence)
|
21
|
+
- [Rollback](#rollback)
|
22
|
+
- [Lambdas](#lambdas)
|
23
|
+
- [Play conditions](#play-conditions)
|
24
|
+
- [Build your own actor](#build-your-own-actor)
|
25
|
+
- [Testing](#testing)
|
26
|
+
- [Influences](#influences)
|
27
|
+
- [Development](#development)
|
28
|
+
- [Contributing](#contributing)
|
29
|
+
- [License](#contributing)
|
7
30
|
|
8
31
|
## Installation
|
9
32
|
|
10
33
|
Add these lines to your application's Gemfile:
|
11
34
|
|
12
35
|
```rb
|
13
|
-
#
|
36
|
+
# Composable service objects
|
14
37
|
gem 'service_actor'
|
15
38
|
```
|
16
39
|
|
17
|
-
|
18
40
|
## Usage
|
19
41
|
|
20
42
|
Actors are single-purpose actions in your application that represent your
|
@@ -30,15 +52,18 @@ class SendNotification < Actor
|
|
30
52
|
end
|
31
53
|
```
|
32
54
|
|
33
|
-
|
55
|
+
Trigger them in your application with `.call`:
|
34
56
|
|
35
57
|
```rb
|
36
|
-
SendNotification.call
|
58
|
+
SendNotification.call # => <ServiceActor::Result…>
|
37
59
|
```
|
38
60
|
|
61
|
+
When called, actors return a Result. Reading and writing to this result allows
|
62
|
+
actors to accept and return multiple arguments. Let's find out how to do that.
|
63
|
+
|
39
64
|
### Inputs
|
40
65
|
|
41
|
-
|
66
|
+
To accept arguments, use `input` to create a method named after this input:
|
42
67
|
|
43
68
|
```rb
|
44
69
|
class GreetUser < Actor
|
@@ -50,7 +75,7 @@ class GreetUser < Actor
|
|
50
75
|
end
|
51
76
|
```
|
52
77
|
|
53
|
-
|
78
|
+
You can now call your actor by providing the correct arguments:
|
54
79
|
|
55
80
|
```rb
|
56
81
|
GreetUser.call(user: User.first)
|
@@ -58,20 +83,20 @@ GreetUser.call(user: User.first)
|
|
58
83
|
|
59
84
|
### Outputs
|
60
85
|
|
61
|
-
|
62
|
-
|
86
|
+
An actor can return multiple arguments. Declare them using `output`, which adds
|
87
|
+
a setter method to let you modify the result from your actor:
|
63
88
|
|
64
89
|
```rb
|
65
90
|
class BuildGreeting < Actor
|
66
91
|
output :greeting
|
67
92
|
|
68
93
|
def call
|
69
|
-
|
94
|
+
self.greeting = 'Have a wonderful day!'
|
70
95
|
end
|
71
96
|
end
|
72
97
|
```
|
73
98
|
|
74
|
-
|
99
|
+
The result you get from calling an actor will include the outputs you set:
|
75
100
|
|
76
101
|
```rb
|
77
102
|
result = BuildGreeting.call
|
@@ -80,17 +105,18 @@ result.greeting # => "Have a wonderful day!"
|
|
80
105
|
|
81
106
|
### Defaults
|
82
107
|
|
83
|
-
Inputs can
|
108
|
+
Inputs can be marked as optional by providing a default:
|
84
109
|
|
85
110
|
```rb
|
86
111
|
class BuildGreeting < Actor
|
87
|
-
input :
|
88
|
-
input :
|
112
|
+
input :name
|
113
|
+
input :adjective, default: 'wonderful'
|
114
|
+
input :length_of_time, default: -> { ['day', 'week', 'month'].sample }
|
89
115
|
|
90
116
|
output :greeting
|
91
117
|
|
92
118
|
def call
|
93
|
-
|
119
|
+
self.greeting = "Have a #{adjective} #{length_of_time} #{name}!"
|
94
120
|
end
|
95
121
|
end
|
96
122
|
```
|
@@ -98,52 +124,73 @@ end
|
|
98
124
|
This lets you call the actor without specifying those keys:
|
99
125
|
|
100
126
|
```rb
|
101
|
-
BuildGreeting.call
|
127
|
+
result = BuildGreeting.call(name: 'Jim')
|
128
|
+
result.greeting # => "Have a wonderful week Jim!"
|
102
129
|
```
|
103
130
|
|
104
|
-
|
131
|
+
If an input does not have a default, it will raise a error:
|
132
|
+
|
133
|
+
```rb
|
134
|
+
result = BuildGreeting.call
|
135
|
+
=> ServiceActor::ArgumentError: Input name on BuildGreeting is missing.
|
136
|
+
```
|
105
137
|
|
106
|
-
|
138
|
+
### Conditions
|
139
|
+
|
140
|
+
You can add simple conditions that the inputs must verify, with the name of your
|
141
|
+
choice under `must`:
|
107
142
|
|
108
143
|
```rb
|
109
|
-
class
|
110
|
-
input :user,
|
111
|
-
|
144
|
+
class UpdateAdminUser < Actor
|
145
|
+
input :user,
|
146
|
+
must: {
|
147
|
+
be_an_admin: ->(user) { user.admin? }
|
148
|
+
}
|
112
149
|
|
113
150
|
# …
|
114
151
|
end
|
115
152
|
```
|
116
153
|
|
117
|
-
|
154
|
+
In case the input does not match, it will raise an argument error.
|
118
155
|
|
119
|
-
|
156
|
+
### Allow nil
|
157
|
+
|
158
|
+
By default inputs accept `nil` values. To raise an error instead:
|
120
159
|
|
121
160
|
```rb
|
122
161
|
class UpdateUser < Actor
|
123
|
-
input :user,
|
162
|
+
input :user, allow_nil: false
|
124
163
|
|
125
164
|
# …
|
126
165
|
end
|
127
166
|
```
|
128
167
|
|
129
|
-
###
|
168
|
+
### Types
|
130
169
|
|
131
|
-
|
132
|
-
|
170
|
+
Sometimes it can help to have a quick way of making sure we didn't mess up our
|
171
|
+
inputs.
|
172
|
+
|
173
|
+
For that you can use the `type` option and giving a class or an array
|
174
|
+
of possible classes. If the input or output doesn't match is not an instance of
|
175
|
+
these types, an error is raised.
|
133
176
|
|
134
177
|
```rb
|
135
|
-
class
|
136
|
-
input :user,
|
137
|
-
|
138
|
-
|
139
|
-
|
178
|
+
class UpdateUser < Actor
|
179
|
+
input :user, type: User
|
180
|
+
input :age, type: [Integer, Float]
|
181
|
+
|
182
|
+
# …
|
140
183
|
end
|
141
184
|
```
|
142
185
|
|
186
|
+
You may also use strings instead of constants, such as `type: 'User'`.
|
187
|
+
|
188
|
+
When using a type condition, `allow_nil` defaults to `false`.
|
189
|
+
|
143
190
|
### Result
|
144
191
|
|
145
|
-
All actors
|
146
|
-
having failed, use `fail!`:
|
192
|
+
All actors return a successful result by default. To stop the execution and
|
193
|
+
mark an actor as having failed, use `fail!`:
|
147
194
|
|
148
195
|
```rb
|
149
196
|
class UpdateUser
|
@@ -153,17 +200,20 @@ class UpdateUser
|
|
153
200
|
def call
|
154
201
|
user.attributes = attributes
|
155
202
|
|
156
|
-
fail!(error:
|
203
|
+
fail!(error: 'Invalid user') unless user.valid?
|
157
204
|
|
158
205
|
# …
|
159
206
|
end
|
160
207
|
end
|
161
208
|
```
|
162
209
|
|
163
|
-
This will raise an error in your app.
|
210
|
+
This will raise an error in your app with the given data added to the result.
|
164
211
|
|
165
|
-
To test for the success instead of raising
|
166
|
-
`.call`.
|
212
|
+
To test for the success of your actor instead of raising an exception, use
|
213
|
+
`.result` instead of `.call`. This lets you use `success?` and `failure?` on the
|
214
|
+
result.
|
215
|
+
|
216
|
+
For example in a Rails controller:
|
167
217
|
|
168
218
|
```rb
|
169
219
|
# app/controllers/users_controller.rb
|
@@ -179,10 +229,13 @@ class UsersController < ApplicationController
|
|
179
229
|
end
|
180
230
|
```
|
181
231
|
|
182
|
-
|
232
|
+
Any keys you add to `fail!` will be added to the result, for example you could
|
233
|
+
do: `fail!(error_type: "validation", error_code: "uv52", …)`.
|
234
|
+
|
235
|
+
## Play actors in a sequence
|
183
236
|
|
184
|
-
|
185
|
-
|
237
|
+
To help you create actors that are small, single-responsibility actions, an
|
238
|
+
actor can use `play` to call other actors:
|
186
239
|
|
187
240
|
```rb
|
188
241
|
class PlaceOrder < Actor
|
@@ -193,51 +246,56 @@ class PlaceOrder < Actor
|
|
193
246
|
end
|
194
247
|
```
|
195
248
|
|
249
|
+
This creates a `call` method where each actor will be called, taking their
|
250
|
+
arguments from the previous actor's result. In fact, every actor along shares
|
251
|
+
the same result instance to help shape the final result your application needs.
|
252
|
+
|
196
253
|
### Rollback
|
197
254
|
|
198
|
-
When using `play`,
|
199
|
-
|
255
|
+
When using `play`, when an actor calls `fail!`, the following actors will not be
|
256
|
+
called.
|
200
257
|
|
201
|
-
|
202
|
-
|
258
|
+
Instead, all the actors that succeeded will have their `rollback` method called
|
259
|
+
in reverse order. This allows actors a chance to cleanup, for example:
|
203
260
|
|
204
261
|
```rb
|
205
262
|
class CreateOrder < Actor
|
263
|
+
output :order
|
264
|
+
|
206
265
|
def call
|
207
|
-
|
266
|
+
self.order = Order.create!(…)
|
208
267
|
end
|
209
268
|
|
210
269
|
def rollback
|
211
|
-
|
270
|
+
order.destroy
|
212
271
|
end
|
213
272
|
end
|
214
273
|
```
|
215
274
|
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
actors, but still consider the actor to be successful.
|
275
|
+
Rollback is only called on the _previous_ actors in `play` and is not called on
|
276
|
+
the failing actor itself. Actors should be kept to a single purpose and not have
|
277
|
+
anything to clean up if they call `fail!`.
|
220
278
|
|
221
279
|
### Lambdas
|
222
280
|
|
223
|
-
You can use inline actions using lambdas
|
281
|
+
You can use inline actions using lambdas. Inside these lambdas, you don't have
|
282
|
+
getters and setters but have access to the shared result:
|
224
283
|
|
225
284
|
```rb
|
226
|
-
class Pay
|
227
|
-
play ->(
|
285
|
+
class Pay < Actor
|
286
|
+
play ->(result) { result.payment_provider = "stripe" },
|
228
287
|
CreatePayment,
|
229
|
-
->(
|
288
|
+
->(result) { result.user_to_notify = result.payment.user },
|
230
289
|
SendNotification
|
231
290
|
end
|
232
291
|
```
|
233
292
|
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
use `super`. For example:
|
293
|
+
Like in this example, lambdas can be useful for small work or preparing the
|
294
|
+
result for the next actors. If you want to do more work before, or after the
|
295
|
+
whole `play`, you can also override the `call` method. For example:
|
238
296
|
|
239
297
|
```rb
|
240
|
-
class Pay
|
298
|
+
class Pay < Actor
|
241
299
|
# …
|
242
300
|
|
243
301
|
def call
|
@@ -250,63 +308,90 @@ end
|
|
250
308
|
|
251
309
|
### Play conditions
|
252
310
|
|
253
|
-
|
311
|
+
Actors in a play can be called conditionally:
|
254
312
|
|
255
313
|
```rb
|
256
314
|
class PlaceOrder < Actor
|
257
315
|
play CreateOrder,
|
258
316
|
Pay
|
259
|
-
play NotifyAdmins, if: ->(
|
317
|
+
play NotifyAdmins, if: ->(result) { result.order.amount > 42 }
|
260
318
|
end
|
261
319
|
```
|
262
320
|
|
321
|
+
You can use this to trigger an early success.
|
322
|
+
|
323
|
+
## Build your own actor
|
324
|
+
|
325
|
+
If you application already uses an "Actor" class, you can build your own by
|
326
|
+
changing the gem's require statement:
|
327
|
+
|
328
|
+
```rb
|
329
|
+
gem 'service_actor', require: 'service_actor/base'
|
330
|
+
```
|
331
|
+
|
332
|
+
And building your own class to inherit from:
|
333
|
+
|
334
|
+
```rb
|
335
|
+
class ApplicationActor
|
336
|
+
include ServiceActor::Base
|
337
|
+
end
|
338
|
+
```
|
339
|
+
|
340
|
+
## Testing
|
341
|
+
|
342
|
+
In your application, add automated testing to your actors as you would do to any
|
343
|
+
other part of your applications.
|
344
|
+
|
345
|
+
You will find that cutting your business logic into single purpose actors makes
|
346
|
+
your application much simpler to test.
|
347
|
+
|
263
348
|
## Influences
|
264
349
|
|
265
350
|
This gem is heavily influenced by
|
266
351
|
[Interactor](https://github.com/collectiveidea/interactor) ♥.
|
267
|
-
However there
|
268
|
-
|
269
|
-
-
|
270
|
-
|
271
|
-
-
|
272
|
-
-
|
273
|
-
|
274
|
-
|
275
|
-
-
|
276
|
-
-
|
277
|
-
|
278
|
-
-
|
279
|
-
|
280
|
-
-
|
281
|
-
-
|
282
|
-
- No `before`, `after` and `around` hooks
|
283
|
-
|
352
|
+
However there are a few key differences which make `actor` unique:
|
353
|
+
|
354
|
+
- Does not [hide errors when an actor fails inside another
|
355
|
+
actor](https://github.com/collectiveidea/interactor/issues/170).
|
356
|
+
- Requires you to document all arguments with `input` and `output`.
|
357
|
+
- Defaults to raising errors on failures: actor uses `call` and `result`
|
358
|
+
instead of `call!` and `call`. This way, the _default_ is to raise an error
|
359
|
+
and failures are not hidden away because you forgot to use `!`.
|
360
|
+
- Allows defaults, type checking, requirements and conditions on inputs.
|
361
|
+
- Delegates methods on the context: `foo` vs `context.foo`, `self.foo =` vs
|
362
|
+
`context.foo = `, fail!` vs `context.fail!`.
|
363
|
+
- Shorter setup syntax: inherit from `< Actor` vs having to `include Interactor`
|
364
|
+
and `include Interactor::Organizer`.
|
365
|
+
- Organizers allow lambdas, being called multiple times, and having conditions.
|
366
|
+
- Allows early success with conditions inside organizers.
|
367
|
+
- No `before`, `after` and `around` hooks, prefer using `play` with lambdas or
|
368
|
+
overriding `call`.
|
369
|
+
|
370
|
+
Thank you to @nicoolas25, @AnneSottise & @williampollet for the early thoughts
|
371
|
+
and feedback on this gem.
|
284
372
|
|
285
373
|
## Development
|
286
374
|
|
287
375
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
288
|
-
`rake` to run the tests. You can also run `bin/console` for an
|
289
|
-
prompt
|
376
|
+
`rake` to run the tests and linting. You can also run `bin/console` for an
|
377
|
+
interactive prompt.
|
290
378
|
|
291
|
-
To
|
292
|
-
|
293
|
-
run `
|
294
|
-
|
379
|
+
To release a new version, update the version number in `version.rb`, and in the
|
380
|
+
`CHANGELOG.md`, run `rake`, and create a commit for this version. You can then
|
381
|
+
run `rake release`, which will create a git tag for the version, push git
|
382
|
+
commits and tags, and push the gem to [rubygems.org](https://rubygems.org).
|
295
383
|
|
296
384
|
## Contributing
|
297
385
|
|
298
|
-
Bug reports and pull requests are welcome
|
299
|
-
https://github.com/sunny/actor.
|
300
|
-
|
301
|
-
|
386
|
+
Bug reports and pull requests are welcome
|
387
|
+
[on GitHub](https://github.com/sunny/actor).
|
388
|
+
|
389
|
+
This project is intended to be a safe, welcoming space for collaboration, and
|
390
|
+
everyone interacting in the project’s codebase and issue tracker is expected to
|
391
|
+
adhere to the [Contributor Covenant code of
|
392
|
+
conduct](https://github.com/sunny/actor/blob/master/CODE_OF_CONDUCT.md).
|
302
393
|
|
303
394
|
## License
|
304
395
|
|
305
396
|
The gem is available as open source under the terms of the
|
306
397
|
[MIT License](https://opensource.org/licenses/MIT).
|
307
|
-
|
308
|
-
## Code of Conduct
|
309
|
-
|
310
|
-
Everyone interacting in the Test project’s codebases, issue trackers, chat
|
311
|
-
rooms and mailing lists is expected to follow the
|
312
|
-
[code of conduct](https://github.com/sunny/actor/blob/master/CODE_OF_CONDUCT.md).
|