service_actor 1.1.0 → 2.0.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 +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
|

|
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).
|