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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1f46ecd0093a05e45fe0bc3ba11a69a8b282b82f4945ab05ff9f5cd1cd32e35e
4
- data.tar.gz: 91f2c49e081eba4abbebceaef9abc131c3e8ad1c84a723846d144e2c8a890dff
3
+ metadata.gz: e103cfa965d8a142d4d47ad910bd4411f90392ab4057b8536a265e12febbf42b
4
+ data.tar.gz: cf297af32084c0a7ff7d3f85ecab5f52137516d5fc2d2c0aa1f5a1fb1aceccfd
5
5
  SHA512:
6
- metadata.gz: bee18ffff2ffbf4eb185b28ab16c519ca30e09af6ba61773009d553c563e9dbdf903aa7c5d6a66b3a4c8a49886f00337b474e1e1376f207f81d1cbe268f3f29b
7
- data.tar.gz: 68e99c61d126b23877cfc4497a4c830b343e1170f49b027013da7a68062d85f644f689ad389d4c584e5b39206cac66a4cb6ab9e359ac9f65a309832c14dab060
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 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
+ ## 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
- # Service objects to keep the business logic
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
- Use `.call` to use them in your application:
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
- Actors can accept arguments with `input`:
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
- And receive them as arguments to `call`:
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
- Use `output` to declare what your actor can return, then assign them to your
62
- context.
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
- context.greeting = "Have a wonderful day!"
94
+ self.greeting = 'Have a wonderful day!'
70
95
  end
71
96
  end
72
97
  ```
73
98
 
74
- Calling an actor returns a context:
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 have defaults:
108
+ Inputs can be marked as optional by providing a default:
84
109
 
85
110
  ```rb
86
111
  class BuildGreeting < Actor
87
- input :adjective, default: "wonderful"
88
- input :length_of_time, default: -> { ["day", "week", "month"].sample }
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
- context.greeting = "Have a #{adjective} #{length_of_time}!"
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.greeting # => "Have a wonderful week!"
127
+ result = BuildGreeting.call(name: 'Jim')
128
+ result.greeting # => "Have a wonderful week Jim!"
102
129
  ```
103
130
 
104
- ### Types
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
- Inputs can define a type, or an array of possible types it must match:
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 UpdateUser < Actor
110
- input :user, type: 'User'
111
- input :age, type: %w[Integer Float]
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
- ### Required
154
+ In case the input does not match, it will raise an argument error.
118
155
 
119
- To check that an input must not be `nil`, flag it as required.
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, required: true
162
+ input :user, allow_nil: false
124
163
 
125
164
  # …
126
165
  end
127
166
  ```
128
167
 
129
- ### Conditions
168
+ ### Types
130
169
 
131
- You can also add conditions that the inputs must verify, with the name of your
132
- choice under `must`:
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 UpdateAdminUser < Actor
136
- input :user,
137
- must: {
138
- be_an_admin: ->(user) { user.admin? }
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 are successful by default. To stop the execution and mark an actor as
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: "Invalid user") unless user.valid?
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, you can use `.result` instead of
166
- `.call`. For example in a Rails controller:
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
- ### Play
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
- An actor can call actors in sequence by using `play`. Each actor will hand over
185
- the same context to the next actor.
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`, if one of the actors calls `fail!`, the following actors will
199
- not be called.
255
+ When using `play`, when an actor calls `fail!`, the following actors will not be
256
+ called.
200
257
 
201
- Also, all the _previous_ actors that succeeded will have their `rollback`
202
- method triggered. For example:
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
- context.order = Order.create!(…)
266
+ self.order = Order.create!(…)
208
267
  end
209
268
 
210
269
  def rollback
211
- context.order.destroy
270
+ order.destroy
212
271
  end
213
272
  end
214
273
  ```
215
274
 
216
- ### Early success
217
-
218
- When using `play` you can use `succeed!` to stop the execution of the following
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 ->(ctx) { ctx.payment_provider = "stripe" },
285
+ class Pay < Actor
286
+ play ->(result) { result.payment_provider = "stripe" },
228
287
  CreatePayment,
229
- ->(ctx) { ctx.user_to_notify = ctx.payment.user },
288
+ ->(result) { result.user_to_notify = result.payment.user },
230
289
  SendNotification
231
290
  end
232
291
  ```
233
292
 
234
- ### Before, after and around
235
-
236
- To do actions before or after actors, use lambdas or simply override `call` and
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
- Some actors in a play can be called conditionaly:
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: ->(ctx) { ctx.order.amount > 42 }
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 a a few key differences which make `actor` unique:
268
-
269
- - 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.
270
- - Does not [hide errors when an actor fails inside another actor](https://github.com/collectiveidea/interactor/issues/170).
271
- - You can use lambdas inside organizers.
272
- - Requires you to document the arguments with `input` and `output`.
273
- - Type checking of inputs and outputs.
274
- - Inputs and outputs can be required.
275
- - Defaults for inputs.
276
- - Conditions on inputs.
277
- - Shorter fail syntax: `fail!` vs `context.fail!`.
278
- - Trigger early success in organisers with `succeed!`.
279
- - Shorter setup syntax: inherit from `< Actor` vs having to `include Interactor` or `include Interactor::Organizer`.
280
- - Multiple organizers.
281
- - Conditions inside organizers.
282
- - No `before`, `after` and `around` hooks. Prefer simply overriding `call` with `super` which allows wrapping the whole method.
283
- - [Fixes issues with `OpenStruct`](https://github.com/collectiveidea/interactor/issues/183)
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 interactive
289
- prompt that will allow you to experiment.
376
+ `rake` to run the tests and linting. You can also run `bin/console` for an
377
+ interactive prompt.
290
378
 
291
- To install this gem onto your local machine, run `bundle exec rake install`.
292
- To release a new version, update the version number in `version.rb`, and then
293
- run `bundle exec rake release`, which will create a git tag for the version,
294
- push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
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 on GitHub at
299
- https://github.com/sunny/actor. This project is intended to be a safe,
300
- welcoming space for collaboration, and contributors are expected to adhere to
301
- the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
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).