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