interactor 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +8 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +280 -0
- data/Rakefile +6 -0
- data/interactor.gemspec +20 -0
- data/lib/interactor.rb +59 -0
- data/lib/interactor/context.rb +20 -0
- data/lib/interactor/organizer.rb +46 -0
- data/spec/interactor/context_spec.rb +95 -0
- data/spec/interactor/organizer_spec.rb +132 -0
- data/spec/interactor_spec.rb +5 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/support/expect.rb +3 -0
- data/spec/support/lint.rb +162 -0
- data/spec/support/random.rb +3 -0
- metadata +96 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6832397e4ab11a93ded8ded31c4e47519be8fe34
|
4
|
+
data.tar.gz: 3ffdfed6582a46b97745356133e5b34a2c3d3b0c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e744650c5033bab652b44d122fc7a1b785d7ab8d22e9afc654a2b1f2e1071f8fdf1846e7c3c68e3163cdd2892ea1ad8d98c37ef9bf8b75c8351b21f13d04215f
|
7
|
+
data.tar.gz: 81ba3295b6dc2cf3991919708b46cb7e83d1f964799f9b6262e46bfbc78733c337f823bc4401c3fec6822154ddf7ccc0abdc63167cc15865ca5f01453f5381df
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Collective Idea
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,280 @@
|
|
1
|
+
# Interactor
|
2
|
+
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/interactor.png)](http://badge.fury.io/rb/interactor)
|
4
|
+
[![Build Status](https://travis-ci.org/collectiveidea/interactor.png?branch=master)](https://travis-ci.org/collectiveidea/interactor)
|
5
|
+
[![Code Climate](https://codeclimate.com/github/collectiveidea/interactor.png)](https://codeclimate.com/github/collectiveidea/interactor)
|
6
|
+
[![Coverage Status](https://coveralls.io/repos/collectiveidea/interactor/badge.png?branch=master)](https://coveralls.io/r/collectiveidea/interactor?branch=master)
|
7
|
+
[![Dependency Status](https://gemnasium.com/collectiveidea/interactor.png)](https://gemnasium.com/collectiveidea/interactor)
|
8
|
+
|
9
|
+
Interactor provides a common interface for performing complex interactions in a single request.
|
10
|
+
|
11
|
+
## Problems
|
12
|
+
|
13
|
+
If you're like us at [Collective Idea](http://collectiveidea.com), you've noticed that there seems to be a layer missing between the Controller and the Model.
|
14
|
+
|
15
|
+
### Fat Models
|
16
|
+
|
17
|
+
We've been told time after time to keep our controllers "skinny" but this usually comes at the expense of our models becoming pretty flabby. Oftentimes, much of the excess weight doesn't belong on the model. We're sending emails, making calls to external services and more, all from the model. It's not right.
|
18
|
+
|
19
|
+
*The purpose of the model layer is to be a gatekeeper to the application's data.*
|
20
|
+
|
21
|
+
Consider the following model:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
class User < ActiveRecord::Base
|
25
|
+
validates :name, :email, presence: true
|
26
|
+
|
27
|
+
after_create :send_welcome_email
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def send_welcome_email
|
32
|
+
Notifier.welcome(self).deliver
|
33
|
+
end
|
34
|
+
end
|
35
|
+
```
|
36
|
+
|
37
|
+
We see this pattern all too often. The problem is that *any* time we want to add a user to the application, the welcome email will be sent. That includes creating users in development and in your tests. Is that really what we want?
|
38
|
+
|
39
|
+
Sending a welcome email is business logic. It has nothing to do with the integrity of the application's data, so it belongs somewhere else.
|
40
|
+
|
41
|
+
### Fat Controllers
|
42
|
+
|
43
|
+
Usually, the alternative to fat models is fat controllers.
|
44
|
+
|
45
|
+
While business logic may be more at home in a controller, controllers are typically intermingled with the concept of a request. HTTP requests are complex and that fact makes testing your business logic more difficult than it should be.
|
46
|
+
|
47
|
+
*Your business logic should be unaware of your delivery mechanism.*
|
48
|
+
|
49
|
+
So what if we encapsulated all of our business logic in dead-simple Ruby. One glance at a directory like `app/interactors` could go a long way in answering the question, "What does this app do?".
|
50
|
+
|
51
|
+
```
|
52
|
+
▸ app/
|
53
|
+
▾ interactors/
|
54
|
+
add_product_to_cart.rb
|
55
|
+
authenticate_user.rb
|
56
|
+
place_order.rb
|
57
|
+
register_user.rb
|
58
|
+
remove_product_from_cart.rb
|
59
|
+
```
|
60
|
+
|
61
|
+
## Interactors
|
62
|
+
|
63
|
+
An interactor is an object with a simple interface and a singular purpose.
|
64
|
+
|
65
|
+
Interactors are given a context from the controller and do one thing: perform. When an interactor performs, it may act on models, send emails, make calls to external services and more. The interactor may also modify the given context.
|
66
|
+
|
67
|
+
A simple interactor may look like:
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
class AuthenticateUser
|
71
|
+
include Interactor
|
72
|
+
|
73
|
+
def perform
|
74
|
+
if user = User.authenticate(context[:email], context[:password])
|
75
|
+
context[:user] = user
|
76
|
+
else
|
77
|
+
context.fail!
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
```
|
82
|
+
|
83
|
+
There are a few important things to note about this interactor:
|
84
|
+
|
85
|
+
1. It's simple.
|
86
|
+
2. It's just Ruby.
|
87
|
+
3. It's easily testable.
|
88
|
+
|
89
|
+
It's feasible that a collection of small interactors such as these could encapsulate *all* of your business logic.
|
90
|
+
|
91
|
+
Interactors free up your controllers to simply accept requests and build responses. They free up your models to acts as the gatekeepers to your data.
|
92
|
+
|
93
|
+
## Organizers
|
94
|
+
|
95
|
+
An organizer is just an interactor that's in charge of other interactors. When an organizer is asked to perform, it just asks its interactors to perform, in order.
|
96
|
+
|
97
|
+
Organizers are great for complex interactions. For example, placing an order might involve:
|
98
|
+
|
99
|
+
* checking inventory
|
100
|
+
* calculating tax
|
101
|
+
* charging a credit card
|
102
|
+
* writing an order to the database
|
103
|
+
* sending email notifications
|
104
|
+
* scheduling a follow-up email
|
105
|
+
|
106
|
+
Each of these actions can (and should) have its own interactor and one organizer can perform them all. That organizer may look like:
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
class PlaceOrder
|
110
|
+
include Interactor::Organizer
|
111
|
+
|
112
|
+
organize [
|
113
|
+
CheckInventory,
|
114
|
+
CalculateTax,
|
115
|
+
ChargeCard,
|
116
|
+
CreateOrder,
|
117
|
+
DeliverThankYou,
|
118
|
+
DeliverOrderNotification,
|
119
|
+
ScheduleFollowUp
|
120
|
+
]
|
121
|
+
end
|
122
|
+
```
|
123
|
+
|
124
|
+
Breaking your interactors into bite-sized pieces also gives you the benefit or reusability. In our example above, there may be several scenarios where you may want to check inventory. Encapsulating that logic in one interactor enables you to reuse that interactor, reducing duplication.
|
125
|
+
|
126
|
+
## Examples
|
127
|
+
|
128
|
+
### Interactors
|
129
|
+
|
130
|
+
Take the simple case of authenticating a user.
|
131
|
+
|
132
|
+
Using an interactor, the controller stays very clean, making it very readable and easily testable.
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
class SessionsController < ApplicationController
|
136
|
+
def create
|
137
|
+
result = AuthenticateUser.perform(session_params)
|
138
|
+
|
139
|
+
if result.success?
|
140
|
+
redirect_to result.user
|
141
|
+
else
|
142
|
+
render :new
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
def session_params
|
149
|
+
params.require(:session).permit(:email, :password)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
```
|
153
|
+
|
154
|
+
The `result` above is an instance of the `AuthenticateUser` interactor that has been performed. The magic happens in the interactor, after receiving a *context* from the controller. A context is just a glorified hash that the interactor manipulates.
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
class AuthenticateUser
|
158
|
+
include Interactor
|
159
|
+
|
160
|
+
def perform
|
161
|
+
if user = User.authenticate(context[:email], context[:password])
|
162
|
+
context[:user] = user
|
163
|
+
else
|
164
|
+
context.fail!
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
```
|
169
|
+
|
170
|
+
The interactor also has convenience methods for dealing with its context. Anything added to the context is available via getter method on the interactor instance. The following is equivalent:
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
class AuthenticateUser
|
174
|
+
include Interactor
|
175
|
+
|
176
|
+
def perform
|
177
|
+
if user = User.authenticate(email, password)
|
178
|
+
context[:user] = user
|
179
|
+
else
|
180
|
+
fail!
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
```
|
185
|
+
|
186
|
+
An interactor can fail with an optional hash that is merged into the context.
|
187
|
+
|
188
|
+
```ruby
|
189
|
+
fail!(message: "Uh oh!")
|
190
|
+
```
|
191
|
+
|
192
|
+
Interactors are successful until explicitly failed. Instances respond to `success?` and `failure?`.
|
193
|
+
|
194
|
+
### Organizers
|
195
|
+
|
196
|
+
In the example above, one could argue that the interactor is simple enough that it could be excluded altogether. While that's probably true, in [our](http://collectiveidea.com) experience, these interactions don't stay simple for long. When they get more complex, the `AuthenticateUser` interactor can be converted to an organizer.
|
197
|
+
|
198
|
+
```ruby
|
199
|
+
class AuthenticateUser
|
200
|
+
include Interactor::Organizer
|
201
|
+
|
202
|
+
organize FindUserByEmailAndPassword, SendWelcomeEmail
|
203
|
+
end
|
204
|
+
```
|
205
|
+
|
206
|
+
And your controller doesn't change a bit!
|
207
|
+
|
208
|
+
The `AuthenticateUser` organizer receives its context from the controller and passes it to the interactors, which each manipulate it in turn.
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
class FindUserByEmailAndPassword
|
212
|
+
include Interactor
|
213
|
+
|
214
|
+
def perform
|
215
|
+
if user = User.authenticate(email, password)
|
216
|
+
context[:user] = user
|
217
|
+
else
|
218
|
+
fail!
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
```
|
223
|
+
|
224
|
+
```ruby
|
225
|
+
class SendWelcomeEmail
|
226
|
+
include Interactor
|
227
|
+
|
228
|
+
def perform
|
229
|
+
if user.newly_created?
|
230
|
+
Notifier.welcome(user).deliver
|
231
|
+
context[:new_user] = true
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
```
|
236
|
+
|
237
|
+
#### Inception
|
238
|
+
|
239
|
+
Because interactors and organizers adhere to the same interface, it's trivial for an organizer to organize… organizers!
|
240
|
+
|
241
|
+
#### Rollback
|
242
|
+
|
243
|
+
If an organizer has three interactors and the second one fails, the third one is never called.
|
244
|
+
|
245
|
+
In addition to halting the chain, an organizer will also *rollback* through the interactors that it has performed so that each interactor has the opportunity to undo itself. Just define a `rollback` method. It has all the same access to the context as `perform` does.
|
246
|
+
|
247
|
+
## Conventions
|
248
|
+
|
249
|
+
We love Rails, and we use Interactor with Rails. We put our interactors in `app/interactors` and we name them as verbs:
|
250
|
+
|
251
|
+
* `AddProductToCart`
|
252
|
+
* `AuthenticateUser`
|
253
|
+
* `PlaceOrder`
|
254
|
+
* `RegisterUser`
|
255
|
+
* `RemoveProductFromCart`
|
256
|
+
|
257
|
+
See [Interactor Rails](https://github.com/collectiveidea/interactor-rails)
|
258
|
+
|
259
|
+
## Contributions
|
260
|
+
|
261
|
+
Interactor is open source and contributions from the community are encouraged! No contribution is too small. Please consider:
|
262
|
+
|
263
|
+
* adding an awesome feature
|
264
|
+
* fixing a terrible bug
|
265
|
+
* updating documentation
|
266
|
+
* fixing a not-so-bad bug
|
267
|
+
* fixing typos
|
268
|
+
|
269
|
+
For the best chance of having your changes merged, please:
|
270
|
+
|
271
|
+
1. Ask us! We'd love to hear what you're up to.
|
272
|
+
2. Fork the project.
|
273
|
+
3. Commit your changes and tests (if applicable (they're applicable)).
|
274
|
+
4. Submit a pull request with a thorough explanation and at least one animated GIF.
|
275
|
+
|
276
|
+
## Thanks
|
277
|
+
|
278
|
+
A very special thank you to [Attila Domokos](https://github.com/adomokos) for his fantastic work on [LightService](https://github.com/adomokos/light-service). Interactor is inspired heavily by the concepts put to code by Attila.
|
279
|
+
|
280
|
+
Interactor was born from a desire for a slightly different (in our minds, simplified) interface. We understand that this is a matter of personal preference, so please take a look at LightService as well!
|
data/Rakefile
ADDED
data/interactor.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = "interactor"
|
5
|
+
spec.version = "1.0.0"
|
6
|
+
|
7
|
+
spec.author = "Collective Idea"
|
8
|
+
spec.email = "info@collectiveidea.com"
|
9
|
+
spec.description = "Interactor provides a common interface for performing complex interactions in a single request."
|
10
|
+
spec.summary = "Simple interactor implementation"
|
11
|
+
spec.homepage = "https://github.com/collectiveidea/interactor"
|
12
|
+
spec.license = "MIT"
|
13
|
+
|
14
|
+
spec.files = `git ls-files`.split($/)
|
15
|
+
spec.test_files = spec.files.grep(/^spec/)
|
16
|
+
spec.require_paths = ["lib"]
|
17
|
+
|
18
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
19
|
+
spec.add_development_dependency "rake", "~> 10.1"
|
20
|
+
end
|
data/lib/interactor.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
require "interactor/context"
|
2
|
+
require "interactor/organizer"
|
3
|
+
|
4
|
+
module Interactor
|
5
|
+
def self.included(base)
|
6
|
+
base.class_eval do
|
7
|
+
extend ClassMethods
|
8
|
+
include InstanceMethods
|
9
|
+
|
10
|
+
attr_reader :context
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
def perform(context = {})
|
16
|
+
new(context).tap(&:perform)
|
17
|
+
end
|
18
|
+
|
19
|
+
def rollback(context = {})
|
20
|
+
new(context).tap(&:rollback)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module InstanceMethods
|
25
|
+
def initialize(context = {})
|
26
|
+
@context = Context.build(context)
|
27
|
+
setup
|
28
|
+
end
|
29
|
+
|
30
|
+
def setup
|
31
|
+
end
|
32
|
+
|
33
|
+
def perform
|
34
|
+
end
|
35
|
+
|
36
|
+
def rollback
|
37
|
+
end
|
38
|
+
|
39
|
+
def success?
|
40
|
+
context.success?
|
41
|
+
end
|
42
|
+
|
43
|
+
def failure?
|
44
|
+
context.failure?
|
45
|
+
end
|
46
|
+
|
47
|
+
def fail!(*args)
|
48
|
+
context.fail!(*args)
|
49
|
+
end
|
50
|
+
|
51
|
+
def method_missing(method, *)
|
52
|
+
context.fetch(method) { super }
|
53
|
+
end
|
54
|
+
|
55
|
+
def respond_to_missing?(method, *)
|
56
|
+
context.key?(method) || super
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Interactor
|
2
|
+
class Context < ::Hash
|
3
|
+
def self.build(context = {})
|
4
|
+
self === context ? context : new.replace(context)
|
5
|
+
end
|
6
|
+
|
7
|
+
def success?
|
8
|
+
!failure?
|
9
|
+
end
|
10
|
+
|
11
|
+
def failure?
|
12
|
+
@failure || false
|
13
|
+
end
|
14
|
+
|
15
|
+
def fail!(context = {})
|
16
|
+
update(context)
|
17
|
+
@failure = true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Interactor
|
2
|
+
module Organizer
|
3
|
+
def self.included(base)
|
4
|
+
base.class_eval do
|
5
|
+
include Interactor
|
6
|
+
|
7
|
+
extend ClassMethods
|
8
|
+
include InstanceMethods
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def interactors
|
14
|
+
@interactors ||= []
|
15
|
+
end
|
16
|
+
|
17
|
+
def organize(*interactors)
|
18
|
+
@interactors = interactors.flatten
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module InstanceMethods
|
23
|
+
def interactors
|
24
|
+
self.class.interactors
|
25
|
+
end
|
26
|
+
|
27
|
+
def perform
|
28
|
+
interactors.each do |interactor|
|
29
|
+
performed << interactor
|
30
|
+
interactor.perform(context)
|
31
|
+
rollback && break if context.failure?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def rollback
|
36
|
+
performed.reverse_each do |interactor|
|
37
|
+
interactor.rollback(context)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def performed
|
42
|
+
@performed ||= []
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module Interactor
|
4
|
+
describe Context do
|
5
|
+
describe ".build" do
|
6
|
+
it "converts the given hash to a context" do
|
7
|
+
context = Context.build(foo: "bar")
|
8
|
+
|
9
|
+
expect(context).to be_a(Context)
|
10
|
+
expect(context).to eq(foo: "bar")
|
11
|
+
end
|
12
|
+
|
13
|
+
it "builds an empty context if no hash is given" do
|
14
|
+
context = Context.build
|
15
|
+
|
16
|
+
expect(context).to be_a(Context)
|
17
|
+
expect(context).to eq({})
|
18
|
+
end
|
19
|
+
|
20
|
+
it "preserves an already built context" do
|
21
|
+
context1 = Context.build(foo: "bar")
|
22
|
+
context2 = Context.build(context1)
|
23
|
+
|
24
|
+
expect(context2).to be_a(Context)
|
25
|
+
expect {
|
26
|
+
context2[:foo] = "baz"
|
27
|
+
}.to change {
|
28
|
+
context1[:foo]
|
29
|
+
}.from("bar").to("baz")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "#success?" do
|
34
|
+
let(:context) { Context.build }
|
35
|
+
|
36
|
+
it "is true by default" do
|
37
|
+
expect(context.success?).to eq(true)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "#failure?" do
|
42
|
+
let(:context) { Context.build }
|
43
|
+
|
44
|
+
it "is false by default" do
|
45
|
+
expect(context.failure?).to eq(false)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "#fail!" do
|
50
|
+
let(:context) { Context.build(foo: "bar") }
|
51
|
+
|
52
|
+
it "sets success to false" do
|
53
|
+
expect {
|
54
|
+
context.fail!
|
55
|
+
}.to change {
|
56
|
+
context.success?
|
57
|
+
}.from(true).to(false)
|
58
|
+
end
|
59
|
+
|
60
|
+
it "sets failure to true" do
|
61
|
+
expect {
|
62
|
+
context.fail!
|
63
|
+
}.to change {
|
64
|
+
context.failure?
|
65
|
+
}.from(false).to(true)
|
66
|
+
end
|
67
|
+
|
68
|
+
it "preserves failure" do
|
69
|
+
context.fail!
|
70
|
+
|
71
|
+
expect {
|
72
|
+
context.fail!
|
73
|
+
}.not_to change {
|
74
|
+
context.failure?
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
it "preserves the context" do
|
79
|
+
expect {
|
80
|
+
context.fail!
|
81
|
+
}.not_to change {
|
82
|
+
context[:foo]
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
it "updates the context" do
|
87
|
+
expect {
|
88
|
+
context.fail!(foo: "baz")
|
89
|
+
}.to change {
|
90
|
+
context[:foo]
|
91
|
+
}.from("bar").to("baz")
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module Interactor
|
4
|
+
describe Organizer do
|
5
|
+
include_examples :lint
|
6
|
+
|
7
|
+
let(:interactor) { Class.new.send(:include, Organizer) }
|
8
|
+
|
9
|
+
describe ".interactors" do
|
10
|
+
it "is empty by default" do
|
11
|
+
expect(interactor.interactors).to eq([])
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe ".organize" do
|
16
|
+
let(:interactor2) { double(:interactor2) }
|
17
|
+
let(:interactor3) { double(:interactor3) }
|
18
|
+
|
19
|
+
it "sets interactors given class arguments" do
|
20
|
+
expect {
|
21
|
+
interactor.organize(interactor2, interactor3)
|
22
|
+
}.to change {
|
23
|
+
interactor.interactors
|
24
|
+
}.from([]).to([interactor2, interactor3])
|
25
|
+
end
|
26
|
+
|
27
|
+
it "sets interactors given an array of classes" do
|
28
|
+
expect {
|
29
|
+
interactor.organize([interactor2, interactor3])
|
30
|
+
}.to change {
|
31
|
+
interactor.interactors
|
32
|
+
}.from([]).to([interactor2, interactor3])
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "#interactors" do
|
37
|
+
let(:interactors) { double(:interactors) }
|
38
|
+
let(:instance) { interactor.new }
|
39
|
+
|
40
|
+
before do
|
41
|
+
interactor.stub(:interactors) { interactors }
|
42
|
+
end
|
43
|
+
|
44
|
+
it "defers to the class" do
|
45
|
+
expect(instance.interactors).to eq(interactors)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "#perform" do
|
50
|
+
let(:interactor2) { double(:interactor2) }
|
51
|
+
let(:interactor3) { double(:interactor3) }
|
52
|
+
let(:interactor4) { double(:interactor4) }
|
53
|
+
let(:instance) { interactor.new }
|
54
|
+
let(:context) { instance.context }
|
55
|
+
|
56
|
+
before do
|
57
|
+
interactor.stub(:interactors) { [interactor2, interactor3, interactor4] }
|
58
|
+
end
|
59
|
+
|
60
|
+
it "performs each interactor in order with the context" do
|
61
|
+
expect(interactor2).to receive(:perform).once.with(context).ordered
|
62
|
+
expect(interactor3).to receive(:perform).once.with(context).ordered
|
63
|
+
expect(interactor4).to receive(:perform).once.with(context).ordered
|
64
|
+
|
65
|
+
expect(instance).not_to receive(:rollback)
|
66
|
+
|
67
|
+
instance.perform
|
68
|
+
end
|
69
|
+
|
70
|
+
it "builds up the performed interactors" do
|
71
|
+
interactor2.stub(:perform) do
|
72
|
+
expect(instance.performed).to eq([interactor2])
|
73
|
+
end
|
74
|
+
|
75
|
+
interactor3.stub(:perform) do
|
76
|
+
expect(instance.performed).to eq([interactor2, interactor3])
|
77
|
+
end
|
78
|
+
|
79
|
+
interactor4.stub(:perform) do
|
80
|
+
expect(instance.performed).to eq([interactor2, interactor3, interactor4])
|
81
|
+
end
|
82
|
+
|
83
|
+
expect {
|
84
|
+
instance.perform
|
85
|
+
}.to change {
|
86
|
+
instance.performed
|
87
|
+
}.from([]).to([interactor2, interactor3, interactor4])
|
88
|
+
end
|
89
|
+
|
90
|
+
it "aborts and rolls back on failure" do
|
91
|
+
expect(interactor2).to receive(:perform).once.with(context).ordered
|
92
|
+
expect(interactor3).to receive(:perform).once.with(context).ordered { context.fail! }
|
93
|
+
expect(interactor4).not_to receive(:perform)
|
94
|
+
|
95
|
+
expect(instance).to receive(:rollback).once.ordered do
|
96
|
+
expect(instance.performed).to eq([interactor2, interactor3])
|
97
|
+
end
|
98
|
+
|
99
|
+
instance.perform
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe "#rollback" do
|
104
|
+
let(:interactor2) { double(:interactor2) }
|
105
|
+
let(:interactor3) { double(:interactor3) }
|
106
|
+
let(:interactor4) { double(:interactor4) }
|
107
|
+
let(:instance) { interactor.new }
|
108
|
+
let(:context) { instance.context }
|
109
|
+
|
110
|
+
before do
|
111
|
+
interactor.stub(:interactors) { [interactor2, interactor3, interactor4] }
|
112
|
+
instance.stub(:performed) { [interactor2, interactor3] }
|
113
|
+
end
|
114
|
+
|
115
|
+
it "rolls back each performed interactor in reverse" do
|
116
|
+
expect(interactor4).not_to receive(:rollback)
|
117
|
+
expect(interactor3).to receive(:rollback).once.with(context).ordered
|
118
|
+
expect(interactor2).to receive(:rollback).once.with(context).ordered
|
119
|
+
|
120
|
+
instance.rollback
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe "#performed" do
|
125
|
+
let(:instance) { interactor.new }
|
126
|
+
|
127
|
+
it "is empty by default" do
|
128
|
+
expect(instance.performed).to eq([])
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
shared_examples :lint do
|
2
|
+
let(:interactor) { Class.new.send(:include, described_class) }
|
3
|
+
|
4
|
+
describe ".perform" do
|
5
|
+
let(:instance) { double(:instance) }
|
6
|
+
|
7
|
+
it "performs an instance with the given context" do
|
8
|
+
expect(interactor).to receive(:new).once.with(foo: "bar") { instance }
|
9
|
+
expect(instance).to receive(:perform).once.with(no_args)
|
10
|
+
|
11
|
+
expect(interactor.perform(foo: "bar")).to eq(instance)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "provides a blank context if none is given" do
|
15
|
+
expect(interactor).to receive(:new).once.with({}) { instance }
|
16
|
+
expect(instance).to receive(:perform).once.with(no_args)
|
17
|
+
|
18
|
+
expect(interactor.perform).to eq(instance)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe ".rollback" do
|
23
|
+
let(:instance) { double(:instance) }
|
24
|
+
|
25
|
+
it "rolls back an instance with the given context" do
|
26
|
+
expect(interactor).to receive(:new).once.with(foo: "bar") { instance }
|
27
|
+
expect(instance).to receive(:rollback).once.with(no_args)
|
28
|
+
|
29
|
+
expect(interactor.rollback(foo: "bar")).to eq(instance)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "provides a blank context if none is given" do
|
33
|
+
expect(interactor).to receive(:new).once.with({}) { instance }
|
34
|
+
expect(instance).to receive(:rollback).once.with(no_args)
|
35
|
+
|
36
|
+
expect(interactor.rollback).to eq(instance)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe ".new" do
|
41
|
+
let(:context) { double(:context) }
|
42
|
+
|
43
|
+
it "initializes a context" do
|
44
|
+
expect(Interactor::Context).to receive(:build).once.with(foo: "bar") { context }
|
45
|
+
|
46
|
+
instance = interactor.new(foo: "bar")
|
47
|
+
|
48
|
+
expect(instance).to be_a(interactor)
|
49
|
+
expect(instance.context).to eq(context)
|
50
|
+
end
|
51
|
+
|
52
|
+
it "initializes a blank context if none is given" do
|
53
|
+
expect(Interactor::Context).to receive(:build).once.with({}) { context }
|
54
|
+
|
55
|
+
instance = interactor.new
|
56
|
+
|
57
|
+
expect(instance).to be_a(interactor)
|
58
|
+
expect(instance.context).to eq(context)
|
59
|
+
end
|
60
|
+
|
61
|
+
it "calls setup" do
|
62
|
+
interactor.class_eval do
|
63
|
+
def setup
|
64
|
+
context[:foo] = bar
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
instance = interactor.new(bar: "baz")
|
69
|
+
|
70
|
+
expect(instance.context[:foo]).to eq("baz")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "#setup" do
|
75
|
+
let(:instance) { interactor.new }
|
76
|
+
|
77
|
+
it "exists" do
|
78
|
+
expect(instance).to respond_to(:setup)
|
79
|
+
expect { instance.setup }.not_to raise_error
|
80
|
+
expect { instance.method(:setup) }.not_to raise_error
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe "#perform" do
|
85
|
+
let(:instance) { interactor.new }
|
86
|
+
|
87
|
+
it "exists" do
|
88
|
+
expect(instance).to respond_to(:perform)
|
89
|
+
expect { instance.perform }.not_to raise_error
|
90
|
+
expect { instance.method(:perform) }.not_to raise_error
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe "#rollback" do
|
95
|
+
let(:instance) { interactor.new }
|
96
|
+
|
97
|
+
it "exists" do
|
98
|
+
expect(instance).to respond_to(:rollback)
|
99
|
+
expect { instance.rollback }.not_to raise_error
|
100
|
+
expect { instance.method(:rollback) }.not_to raise_error
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe "#success?" do
|
105
|
+
let(:instance) { interactor.new }
|
106
|
+
let(:context) { instance.context }
|
107
|
+
|
108
|
+
it "defers to the context" do
|
109
|
+
context.stub(success?: true)
|
110
|
+
expect(instance.success?).to eq(true)
|
111
|
+
|
112
|
+
context.stub(success?: false)
|
113
|
+
expect(instance.success?).to eq(false)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
describe "#failure?" do
|
118
|
+
let(:instance) { interactor.new }
|
119
|
+
let(:context) { instance.context }
|
120
|
+
|
121
|
+
it "defers to the context" do
|
122
|
+
context.stub(failure?: true)
|
123
|
+
expect(instance.failure?).to eq(true)
|
124
|
+
|
125
|
+
context.stub(failure?: false)
|
126
|
+
expect(instance.failure?).to eq(false)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
describe "#fail!" do
|
131
|
+
let(:instance) { interactor.new }
|
132
|
+
let(:context) { instance.context }
|
133
|
+
|
134
|
+
it "defers to the context" do
|
135
|
+
expect(context).to receive(:fail!).once.with(no_args)
|
136
|
+
|
137
|
+
instance.fail!
|
138
|
+
end
|
139
|
+
|
140
|
+
it "passes updates to the context" do
|
141
|
+
expect(context).to receive(:fail!).once.with(foo: "bar")
|
142
|
+
|
143
|
+
instance.fail!(foo: "bar")
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
describe "context deferral" do
|
148
|
+
let(:instance) { interactor.new(foo: "bar") }
|
149
|
+
|
150
|
+
it "defers to keys that exist in the context" do
|
151
|
+
expect(instance).to respond_to(:foo)
|
152
|
+
expect(instance.foo).to eq("bar")
|
153
|
+
expect { instance.method(:foo) }.not_to raise_error
|
154
|
+
end
|
155
|
+
|
156
|
+
it "bombs if the key does not exist in the context" do
|
157
|
+
expect(instance).not_to respond_to(:baz)
|
158
|
+
expect { instance.baz }.to raise_error(NoMethodError)
|
159
|
+
expect { instance.method(:baz) }.to raise_error(NameError)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
metadata
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: interactor
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Collective Idea
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-08-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.1'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.1'
|
41
|
+
description: Interactor provides a common interface for performing complex interactions
|
42
|
+
in a single request.
|
43
|
+
email: info@collectiveidea.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- .gitignore
|
49
|
+
- .travis.yml
|
50
|
+
- Gemfile
|
51
|
+
- LICENSE.txt
|
52
|
+
- README.md
|
53
|
+
- Rakefile
|
54
|
+
- interactor.gemspec
|
55
|
+
- lib/interactor.rb
|
56
|
+
- lib/interactor/context.rb
|
57
|
+
- lib/interactor/organizer.rb
|
58
|
+
- spec/interactor/context_spec.rb
|
59
|
+
- spec/interactor/organizer_spec.rb
|
60
|
+
- spec/interactor_spec.rb
|
61
|
+
- spec/spec_helper.rb
|
62
|
+
- spec/support/expect.rb
|
63
|
+
- spec/support/lint.rb
|
64
|
+
- spec/support/random.rb
|
65
|
+
homepage: https://github.com/collectiveidea/interactor
|
66
|
+
licenses:
|
67
|
+
- MIT
|
68
|
+
metadata: {}
|
69
|
+
post_install_message:
|
70
|
+
rdoc_options: []
|
71
|
+
require_paths:
|
72
|
+
- lib
|
73
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
requirements: []
|
84
|
+
rubyforge_project:
|
85
|
+
rubygems_version: 2.0.5
|
86
|
+
signing_key:
|
87
|
+
specification_version: 4
|
88
|
+
summary: Simple interactor implementation
|
89
|
+
test_files:
|
90
|
+
- spec/interactor/context_spec.rb
|
91
|
+
- spec/interactor/organizer_spec.rb
|
92
|
+
- spec/interactor_spec.rb
|
93
|
+
- spec/spec_helper.rb
|
94
|
+
- spec/support/expect.rb
|
95
|
+
- spec/support/lint.rb
|
96
|
+
- spec/support/random.rb
|