interactor 2.1.1 → 3.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/.rspec +3 -0
- data/.travis.yml +10 -2
- data/CHANGELOG.md +31 -0
- data/CONTRIBUTING.md +49 -0
- data/Gemfile +2 -3
- data/README.md +453 -163
- data/interactor.gemspec +6 -7
- data/lib/interactor.rb +22 -26
- data/lib/interactor/context.rb +19 -8
- data/lib/interactor/error.rb +10 -0
- data/lib/interactor/hooks.rb +53 -0
- data/lib/interactor/organizer.rb +7 -27
- data/spec/integration_spec.rb +967 -0
- data/spec/interactor/context_spec.rb +75 -34
- data/spec/interactor/organizer_spec.rb +16 -169
- data/spec/interactor_spec.rb +0 -2
- data/spec/spec_helper.rb +3 -3
- data/spec/support/lint.rb +65 -99
- metadata +14 -12
- data/spec/support/expect.rb +0 -3
- data/spec/support/random.rb +0 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f90c2385a676f054e225898505feb66133199adb
|
|
4
|
+
data.tar.gz: f2f66a7c046fe90cd9cc8ee3c54797654f4dd5fd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 224044e19387797e7e2cfc3b930cceb348078eee3b4ff6b2c7f44e54fefb7200ed490468131a31d813c2e0534c517311057aa048e70f90d04b1269cdcbbb5977
|
|
7
|
+
data.tar.gz: 92e8e877cc317445d33b4c7b32b5eae7c88d156a206f2e313d70c6abe01151819eb2b151a580f627ec9b0deeb862d24fae4cd5fd8c3b189018064be91dac3305
|
data/.rspec
ADDED
data/.travis.yml
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
|
+
before_install:
|
|
2
|
+
- gem update bundler rake
|
|
1
3
|
branches:
|
|
2
4
|
only:
|
|
3
5
|
- master
|
|
4
|
-
- v3
|
|
6
|
+
- v3
|
|
7
|
+
env:
|
|
8
|
+
global:
|
|
9
|
+
- secure: | # CODECLIMATE_REPO_TOKEN
|
|
10
|
+
BIemhM273wHZMpuULDMYGPsxYdfw+NMw7IQbOD6gy5r+dha07y9ssTYYE5Gn
|
|
11
|
+
t1ptAb09lhQ4gexXTr83i6angMrnHgQ1ZX2wfeoZ0FvWDHQht9YkXyiNH+R6
|
|
12
|
+
odHUeDIYAlUiqLX9nAkklL89Rc22BrHMGGNyuA8Uc5sktW5P/FE=
|
|
5
13
|
language: ruby
|
|
6
14
|
matrix:
|
|
7
15
|
allow_failures:
|
|
8
16
|
- rvm: ruby-head
|
|
9
17
|
rvm:
|
|
10
18
|
- 1.9.3
|
|
11
|
-
- 2.0
|
|
19
|
+
- "2.0"
|
|
12
20
|
- "2.1"
|
|
13
21
|
- ruby-head
|
|
14
22
|
script: bundle exec rspec
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
## 3.0.0 / 2014-09-07
|
|
2
|
+
|
|
3
|
+
* [FEATURE] Halt performance if the interactor fails prior
|
|
4
|
+
* [ENHANCEMENT] Add support for Ruby 2.1
|
|
5
|
+
* [FEATURE] Remove "magical" access to the context through the interactor
|
|
6
|
+
* [FEATURE] Manage context values via setters/getters rather than hash access
|
|
7
|
+
* [FEATURE] Change the primary interactor API method from "perform" to "call"
|
|
8
|
+
* [FEATURE] Return the mutated context rather than the interactor instance
|
|
9
|
+
* [FEATURE] Replace interactor setup with before and after hooks
|
|
10
|
+
* [FEATURE] Abort execution immediately upon interactor failure
|
|
11
|
+
* [ENHANCEMENT] Build a suite of realistic integration tests
|
|
12
|
+
* [ENHANCEMENT] Move rollback responsibility into the context
|
|
13
|
+
|
|
14
|
+
## 2.1.0 / 2013-09-05
|
|
15
|
+
|
|
16
|
+
* [FEATURE] Roll back when an interactor within an organizer raises an error
|
|
17
|
+
* [BUGFIX] Ensure that context-deferred methods respect string keys
|
|
18
|
+
* [FEATURE] Respect context initialization from an indifferent access hash
|
|
19
|
+
|
|
20
|
+
## 2.0.1 / 2013-08-28
|
|
21
|
+
|
|
22
|
+
* [BUGFIX] Allow YAML (de)serialization by fixing interactor allocation
|
|
23
|
+
|
|
24
|
+
## 2.0.0 / 2013-08-19
|
|
25
|
+
|
|
26
|
+
* [BUGFIX] Fix rollback behavior within nested organizers
|
|
27
|
+
* [BUGFIX] Skip rollback for the failed interactor
|
|
28
|
+
|
|
29
|
+
## 1.0.0 / 2013-08-17
|
|
30
|
+
|
|
31
|
+
* Initial release!
|
data/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Contributing to Interactor
|
|
2
|
+
|
|
3
|
+
Interactor is open source and contributions from the community are encouraged!
|
|
4
|
+
No contribution is too small.
|
|
5
|
+
|
|
6
|
+
Please consider:
|
|
7
|
+
|
|
8
|
+
* adding a feature
|
|
9
|
+
* squashing a bug
|
|
10
|
+
* writing documentation
|
|
11
|
+
* reporting an issue
|
|
12
|
+
* fixing a typo
|
|
13
|
+
* correcting [style](https://github.com/styleguide/ruby)
|
|
14
|
+
|
|
15
|
+
## How do I contribute?
|
|
16
|
+
|
|
17
|
+
For the best chance of having your changes merged, please:
|
|
18
|
+
|
|
19
|
+
1. [Fork](https://github.com/collectiveidea/interactor/fork) the project.
|
|
20
|
+
2. [Write](http://en.wikipedia.org/wiki/Test-driven_development) a failing test.
|
|
21
|
+
3. [Commit](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) changes that fix the tests.
|
|
22
|
+
4. [Submit](https://github.com/collectiveidea/interactor/pulls) a pull request with *at least* one animated GIF.
|
|
23
|
+
5. Be patient.
|
|
24
|
+
|
|
25
|
+
If your proposed changes only affect documentation, include the following on a
|
|
26
|
+
new line in each of your commit messages:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
[ci skip]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This will signal [Travis](https://travis-ci.org) that running the test suite is
|
|
33
|
+
not necessary for these changes.
|
|
34
|
+
|
|
35
|
+
## Bug Reports
|
|
36
|
+
|
|
37
|
+
If you are experiencing unexpected behavior and, after having read Interactor's
|
|
38
|
+
documentation, are convinced this behavior is a bug, please:
|
|
39
|
+
|
|
40
|
+
1. [Search](https://github.com/collectiveidea/interactor/issues) existing issues.
|
|
41
|
+
2. Collect enough information to reproduce the issue:
|
|
42
|
+
* Interactor version
|
|
43
|
+
* Ruby version
|
|
44
|
+
* Rails version (if applicable)
|
|
45
|
+
* Specific setup conditions
|
|
46
|
+
* Description of expected behavior
|
|
47
|
+
* Description of actual behavior
|
|
48
|
+
3. [Submit](https://github.com/collectiveidea/interactor/issues/new) an issue.
|
|
49
|
+
4. Be patient.
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -1,297 +1,592 @@
|
|
|
1
1
|
# Interactor
|
|
2
2
|
|
|
3
|
-
[](http://rubygems.org/gems/interactor)
|
|
4
|
+
[](https://travis-ci.org/collectiveidea/interactor)
|
|
5
|
+
[](https://codeclimate.com/github/collectiveidea/interactor)
|
|
6
|
+
[](https://codeclimate.com/github/collectiveidea/interactor)
|
|
7
|
+
[](https://gemnasium.com/collectiveidea/interactor)
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Getting Started
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Add Interactor to your Gemfile and `bundle install`.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
```ruby
|
|
14
|
+
gem "interactor", "~> 3.0"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## What is an Interactor?
|
|
18
|
+
|
|
19
|
+
An interactor is a simple, single-purpose object.
|
|
20
|
+
|
|
21
|
+
Interactors are used to encapsulate your application's
|
|
22
|
+
[business logic](http://en.wikipedia.org/wiki/Business_logic). Each interactor
|
|
23
|
+
represents one thing that your application *does*.
|
|
24
|
+
|
|
25
|
+
### Context
|
|
14
26
|
|
|
15
|
-
|
|
27
|
+
An interactor is given a *context*. The context contains everything the
|
|
28
|
+
interactor needs to do its work.
|
|
16
29
|
|
|
17
|
-
|
|
30
|
+
When an interactor does its single purpose, it affects its given context.
|
|
18
31
|
|
|
19
|
-
|
|
32
|
+
#### Adding to the Context
|
|
20
33
|
|
|
21
|
-
|
|
34
|
+
As an interactor runs it can add information to the context.
|
|
22
35
|
|
|
23
36
|
```ruby
|
|
24
|
-
|
|
25
|
-
|
|
37
|
+
context.user = user
|
|
38
|
+
```
|
|
26
39
|
|
|
27
|
-
|
|
40
|
+
#### Failing the Context
|
|
28
41
|
|
|
29
|
-
|
|
42
|
+
When something goes wrong in your interactor, you can flag the context as
|
|
43
|
+
failed.
|
|
30
44
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
end
|
|
34
|
-
end
|
|
45
|
+
```ruby
|
|
46
|
+
context.fail!
|
|
35
47
|
```
|
|
36
48
|
|
|
37
|
-
|
|
49
|
+
When given a hash argument, the `fail!` method can also update the context. The
|
|
50
|
+
following are equivalent:
|
|
38
51
|
|
|
39
|
-
|
|
52
|
+
```ruby
|
|
53
|
+
context.error = "Boom!"
|
|
54
|
+
context.fail!
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
context.fail!(error: "Boom!")
|
|
59
|
+
```
|
|
40
60
|
|
|
41
|
-
|
|
61
|
+
You can ask a context if it's a failure:
|
|
42
62
|
|
|
43
|
-
|
|
63
|
+
```ruby
|
|
64
|
+
context.failure? # => false
|
|
65
|
+
context.fail!
|
|
66
|
+
context.failure? # => true
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
or if it's a success.
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
context.success? # => true
|
|
73
|
+
context.fail!
|
|
74
|
+
context.success? # => false
|
|
75
|
+
```
|
|
44
76
|
|
|
45
|
-
|
|
77
|
+
### Hooks
|
|
46
78
|
|
|
47
|
-
|
|
79
|
+
#### Before Hooks
|
|
48
80
|
|
|
49
|
-
|
|
81
|
+
Sometimes an interactor needs to prepare its context before the interactor is
|
|
82
|
+
even run. This can be done with before hooks on the interactor.
|
|
50
83
|
|
|
84
|
+
```ruby
|
|
85
|
+
before do
|
|
86
|
+
context.emails_sent = 0
|
|
87
|
+
end
|
|
51
88
|
```
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
89
|
+
|
|
90
|
+
A symbol argument can also be given, rather than a block.
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
before :zero_emails_sent
|
|
94
|
+
|
|
95
|
+
def zero_email_sent
|
|
96
|
+
context.emails_sent = 0
|
|
97
|
+
end
|
|
59
98
|
```
|
|
60
99
|
|
|
61
|
-
|
|
100
|
+
#### After Hooks
|
|
62
101
|
|
|
63
|
-
|
|
102
|
+
Interactors can also perform teardown operations after the interactor instance
|
|
103
|
+
is run.
|
|
64
104
|
|
|
65
|
-
|
|
105
|
+
```ruby
|
|
106
|
+
after do
|
|
107
|
+
context.user.reload
|
|
108
|
+
end
|
|
109
|
+
```
|
|
66
110
|
|
|
67
|
-
|
|
111
|
+
**NOTE:** An interactor can define multiple before/after hooks, allowing common
|
|
112
|
+
hooks to be extracted into interactor concerns.
|
|
113
|
+
|
|
114
|
+
### An Example Interactor
|
|
115
|
+
|
|
116
|
+
Your application could use an interactor to authenticate a user.
|
|
68
117
|
|
|
69
118
|
```ruby
|
|
70
119
|
class AuthenticateUser
|
|
71
120
|
include Interactor
|
|
72
121
|
|
|
73
|
-
def
|
|
74
|
-
if user = User.authenticate(context
|
|
75
|
-
context
|
|
122
|
+
def call
|
|
123
|
+
if user = User.authenticate(context.email, context.password)
|
|
124
|
+
context.user = user
|
|
125
|
+
context.token = user.secret_token
|
|
76
126
|
else
|
|
77
|
-
context.fail!
|
|
127
|
+
context.fail!(message: "authenticate_user.failure")
|
|
78
128
|
end
|
|
79
129
|
end
|
|
80
130
|
end
|
|
81
131
|
```
|
|
82
132
|
|
|
83
|
-
|
|
133
|
+
To define an interactor, simply create a class that includes the `Interactor`
|
|
134
|
+
module and give it a `call` instance method. The interactor can access its
|
|
135
|
+
`context` from within `call`.
|
|
136
|
+
|
|
137
|
+
## Interactors in the Controller
|
|
138
|
+
|
|
139
|
+
Most of the time, your application will use its interactors from its
|
|
140
|
+
controllers. The following controller:
|
|
84
141
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
142
|
+
```ruby
|
|
143
|
+
class SessionsController < ApplicationController
|
|
144
|
+
def create
|
|
145
|
+
if user = User.authenticate(session_params[:email], session_params[:password])
|
|
146
|
+
session[:user_token] = user.secret_token
|
|
147
|
+
redirect_to user
|
|
148
|
+
else
|
|
149
|
+
flash.now[:message] = "Please try again."
|
|
150
|
+
render :new
|
|
151
|
+
end
|
|
152
|
+
end
|
|
88
153
|
|
|
89
|
-
|
|
154
|
+
private
|
|
90
155
|
|
|
91
|
-
|
|
156
|
+
def session_params
|
|
157
|
+
params.require(:session).permit(:email, :password)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
```
|
|
92
161
|
|
|
93
|
-
|
|
94
|
-
In the above example if you want to add some checking or small operations before the main operation,
|
|
95
|
-
you can just define `setup` and it will be called before `perform`.
|
|
162
|
+
can be refactored to:
|
|
96
163
|
|
|
97
164
|
```ruby
|
|
98
|
-
class
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
165
|
+
class SessionsController < ApplicationController
|
|
166
|
+
def create
|
|
167
|
+
result = AuthenticateUser.call(session_params)
|
|
168
|
+
|
|
169
|
+
if result.success?
|
|
170
|
+
session[:user_token] = result.token
|
|
171
|
+
redirect_to root_path
|
|
172
|
+
else
|
|
173
|
+
flash.now[:message] = t(result.message)
|
|
174
|
+
render :new
|
|
175
|
+
end
|
|
103
176
|
end
|
|
104
177
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
178
|
+
private
|
|
179
|
+
|
|
180
|
+
def session_params
|
|
181
|
+
params.require(:session).permit(:email, :password)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The `call` class method is the proper way to invoke an interactor. The hash
|
|
187
|
+
argument is converted to the interactor instance's context. The `call` instance
|
|
188
|
+
method is invoked along with any hooks that the interactor might define.
|
|
189
|
+
Finally, the context (along with any changes made to it) is returned.
|
|
190
|
+
|
|
191
|
+
## When to Use an Interactor
|
|
192
|
+
|
|
193
|
+
Given the user authentication example, your controller may look like:
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
class SessionsController < ApplicationController
|
|
197
|
+
def create
|
|
198
|
+
result = AuthenticateUser.call(session_params)
|
|
199
|
+
|
|
200
|
+
if result.success?
|
|
201
|
+
session[:user_token] = result.token
|
|
202
|
+
redirect_to root_path
|
|
108
203
|
else
|
|
109
|
-
|
|
204
|
+
flash.now[:message] = t(result.message)
|
|
205
|
+
render :new
|
|
110
206
|
end
|
|
111
207
|
end
|
|
208
|
+
|
|
209
|
+
private
|
|
210
|
+
|
|
211
|
+
def session_params
|
|
212
|
+
params.require(:session).permit(:email, :password)
|
|
213
|
+
end
|
|
112
214
|
end
|
|
113
215
|
```
|
|
114
216
|
|
|
115
|
-
|
|
217
|
+
For such a simple use case, using an interactor can actually require *more*
|
|
218
|
+
code. So why use an interactor?
|
|
116
219
|
|
|
117
|
-
|
|
220
|
+
### Clarity
|
|
118
221
|
|
|
119
|
-
|
|
222
|
+
[We](http://collectiveidea.com) often use interactors right off the bat for all
|
|
223
|
+
of our destructive actions (`POST`, `PUT` and `DELETE` requests) and since we
|
|
224
|
+
put our interactors in `app/interactors`, a glance at that directory gives any
|
|
225
|
+
developer a quick understanding of everything the application *does*.
|
|
120
226
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
227
|
+
```
|
|
228
|
+
▾ app/
|
|
229
|
+
▸ controllers/
|
|
230
|
+
▸ helpers/
|
|
231
|
+
▾ interactors/
|
|
232
|
+
authenticate_user.rb
|
|
233
|
+
cancel_account.rb
|
|
234
|
+
publish_post.rb
|
|
235
|
+
register_user.rb
|
|
236
|
+
remove_post.rb
|
|
237
|
+
▸ mailers/
|
|
238
|
+
▸ models/
|
|
239
|
+
▸ views/
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**TIP:** Name your interactors after your business logic, not your
|
|
243
|
+
implementation. `CancelAccount` will serve you better than `DestroyUser` as the
|
|
244
|
+
account cancellation interaction takes on more responsibility in the future.
|
|
245
|
+
|
|
246
|
+
### The Future™
|
|
247
|
+
|
|
248
|
+
**SPOLIER ALERT:** Your use case won't *stay* so simple.
|
|
249
|
+
|
|
250
|
+
In [our](http://collectiveidea.com) experience, a simple task like
|
|
251
|
+
authenticating a user will eventually take on multiple responsibilities:
|
|
252
|
+
|
|
253
|
+
* Welcoming back a user who hadn't logged in for a while
|
|
254
|
+
* Prompting a user to update his or her password
|
|
255
|
+
* Locking out a user in the case of too many failed attempts
|
|
256
|
+
* Sending the lock-out email notification
|
|
257
|
+
|
|
258
|
+
The list goes on, and as that list grows, so does your controller. This is how
|
|
259
|
+
fat controllers are born.
|
|
260
|
+
|
|
261
|
+
If instead you use an interactor right away, as responsibilities are added, your
|
|
262
|
+
controller (and its tests) change very little or not at all. Choosing the right
|
|
263
|
+
kind of interactor can also prevent simply shifting those added responsibilities
|
|
264
|
+
to the interactor.
|
|
265
|
+
|
|
266
|
+
## Kinds of Interactors
|
|
267
|
+
|
|
268
|
+
There are two kinds of interactors built into the Interactor library: basic
|
|
269
|
+
interactors and organizers.
|
|
270
|
+
|
|
271
|
+
### Interactors
|
|
127
272
|
|
|
128
|
-
|
|
273
|
+
A basic interactor is a class that includes `Interactor` and defines `call`.
|
|
129
274
|
|
|
130
275
|
```ruby
|
|
131
|
-
class
|
|
132
|
-
include Interactor
|
|
276
|
+
class AuthenticateUser
|
|
277
|
+
include Interactor
|
|
133
278
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
]
|
|
279
|
+
def call
|
|
280
|
+
if user = User.authenticate(context.email, context.password)
|
|
281
|
+
context.user = user
|
|
282
|
+
context.token = user.secret_token
|
|
283
|
+
else
|
|
284
|
+
context.fail!(message: "authenticate_user.failure")
|
|
285
|
+
end
|
|
286
|
+
end
|
|
143
287
|
end
|
|
144
288
|
```
|
|
145
289
|
|
|
146
|
-
|
|
290
|
+
Basic interactors are the building blocks. They are your application's
|
|
291
|
+
single-purpose units of work.
|
|
147
292
|
|
|
148
|
-
|
|
293
|
+
### Organizers
|
|
149
294
|
|
|
150
|
-
|
|
295
|
+
An organizer is an important variation on the basic interactor. Its single
|
|
296
|
+
purpose is to run *other* interactors.
|
|
151
297
|
|
|
152
|
-
|
|
298
|
+
```ruby
|
|
299
|
+
class PlaceOrder
|
|
300
|
+
include Interactor::Organizer
|
|
301
|
+
|
|
302
|
+
organize CreateOrder, ChargeCard, SendThankYou
|
|
303
|
+
end
|
|
304
|
+
```
|
|
153
305
|
|
|
154
|
-
|
|
306
|
+
In the controller, you can run the `PlaceOrder` organizer just like you would
|
|
307
|
+
any other interactor:
|
|
155
308
|
|
|
156
309
|
```ruby
|
|
157
|
-
class
|
|
310
|
+
class OrdersController < ApplicationController
|
|
158
311
|
def create
|
|
159
|
-
result =
|
|
312
|
+
result = PlaceOrder.call(order_params: order_params)
|
|
160
313
|
|
|
161
314
|
if result.success?
|
|
162
|
-
redirect_to result.
|
|
315
|
+
redirect_to result.order
|
|
163
316
|
else
|
|
317
|
+
@order = result.order
|
|
164
318
|
render :new
|
|
165
319
|
end
|
|
166
320
|
end
|
|
167
321
|
|
|
168
322
|
private
|
|
169
323
|
|
|
170
|
-
def
|
|
171
|
-
params.require(:
|
|
324
|
+
def order_params
|
|
325
|
+
params.require(:order).permit!
|
|
172
326
|
end
|
|
173
327
|
end
|
|
174
328
|
```
|
|
175
329
|
|
|
176
|
-
The
|
|
330
|
+
The organizer passes its context to the interactors that it organizes, one at a
|
|
331
|
+
time and in order. Each interactor may change that context before it's passed
|
|
332
|
+
along to the next interactor.
|
|
333
|
+
|
|
334
|
+
#### Rollback
|
|
335
|
+
|
|
336
|
+
If any one of the organized interactors fails its context, the organizer stops.
|
|
337
|
+
If the `ChargeCard` interactor fails, `SendThankYou` is never called.
|
|
338
|
+
|
|
339
|
+
In addition, any interactors that had already run are given the chance to undo
|
|
340
|
+
themselves, in reverse order. Simply define the `rollback` method on your
|
|
341
|
+
interactors:
|
|
177
342
|
|
|
178
343
|
```ruby
|
|
179
|
-
class
|
|
344
|
+
class CreateOrder
|
|
180
345
|
include Interactor
|
|
181
346
|
|
|
182
|
-
def
|
|
183
|
-
|
|
184
|
-
|
|
347
|
+
def call
|
|
348
|
+
order = Order.create(order_params)
|
|
349
|
+
|
|
350
|
+
if order.persisted?
|
|
351
|
+
context.order = order
|
|
185
352
|
else
|
|
186
353
|
context.fail!
|
|
187
354
|
end
|
|
188
355
|
end
|
|
356
|
+
|
|
357
|
+
def rollback
|
|
358
|
+
context.order.destroy
|
|
359
|
+
end
|
|
189
360
|
end
|
|
190
361
|
```
|
|
191
362
|
|
|
192
|
-
The interactor
|
|
363
|
+
**NOTE:** The interactor that fails is *not* rolled back. Because every
|
|
364
|
+
interactor should have a single purpose, there should be no need to clean up
|
|
365
|
+
after any failed interactor.
|
|
366
|
+
|
|
367
|
+
## Testing Interactors
|
|
368
|
+
|
|
369
|
+
When written correctly, an interactor is easy to test because it only *does* one
|
|
370
|
+
thing. Take the following interactor:
|
|
193
371
|
|
|
194
372
|
```ruby
|
|
195
373
|
class AuthenticateUser
|
|
196
374
|
include Interactor
|
|
197
375
|
|
|
198
|
-
def
|
|
199
|
-
if user = User.authenticate(email, password)
|
|
200
|
-
context
|
|
376
|
+
def call
|
|
377
|
+
if user = User.authenticate(context.email, context.password)
|
|
378
|
+
context.user. = user
|
|
379
|
+
context.token = user.secret_token
|
|
201
380
|
else
|
|
202
|
-
fail!
|
|
381
|
+
context.fail!(message: "authenticate_user.failure")
|
|
203
382
|
end
|
|
204
383
|
end
|
|
205
384
|
end
|
|
206
385
|
```
|
|
207
386
|
|
|
208
|
-
|
|
387
|
+
You can test just this interactor's single purpose and how it affects the
|
|
388
|
+
context.
|
|
209
389
|
|
|
210
390
|
```ruby
|
|
211
|
-
|
|
212
|
-
|
|
391
|
+
describe AuthenticateUser do
|
|
392
|
+
describe "#call" do
|
|
393
|
+
end
|
|
394
|
+
let(:interactor) { AuthenticateUser.new(email: "john@example.com", password: "secret") }
|
|
395
|
+
let(:context) { interactor.context }
|
|
396
|
+
|
|
397
|
+
context "when given valid credentials" do
|
|
398
|
+
let(:user) { double(:user, secret_token: "token") }
|
|
399
|
+
|
|
400
|
+
before do
|
|
401
|
+
allow(User).to receive(:authenticate).with("john@example.com", "secret").and_return(user)
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
it "succeeds" do
|
|
405
|
+
interactor.call
|
|
406
|
+
|
|
407
|
+
expect(context).to be_a_success
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
it "provides the user" do
|
|
411
|
+
expect {
|
|
412
|
+
interactor.call
|
|
413
|
+
}.to change {
|
|
414
|
+
context.user
|
|
415
|
+
}.from(nil).to(user)
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
it "provides the user's secret token" do
|
|
419
|
+
expect {
|
|
420
|
+
interactor.call
|
|
421
|
+
}.to change {
|
|
422
|
+
context.token
|
|
423
|
+
}.from(nil).to("token")
|
|
424
|
+
end
|
|
425
|
+
end
|
|
213
426
|
|
|
214
|
-
|
|
427
|
+
context "when given invalid credentials" do
|
|
428
|
+
before do
|
|
429
|
+
allow(User).to receive(:authenticate).with("john@example.com", "secret").and_return(nil)
|
|
430
|
+
end
|
|
215
431
|
|
|
216
|
-
|
|
432
|
+
it "fails" do
|
|
433
|
+
interactor.call
|
|
217
434
|
|
|
218
|
-
|
|
435
|
+
expect(context).to be_a_failure
|
|
436
|
+
end
|
|
219
437
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
438
|
+
it "provides a failure message" do
|
|
439
|
+
expect {
|
|
440
|
+
interactor.call
|
|
441
|
+
}.to change {
|
|
442
|
+
context.message
|
|
443
|
+
}.from(nil).to be_present
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
end
|
|
225
447
|
end
|
|
226
448
|
```
|
|
227
449
|
|
|
228
|
-
|
|
450
|
+
[We](http://collectiveidea.com) use RSpec but the same approach applies to any
|
|
451
|
+
testing framework.
|
|
229
452
|
|
|
230
|
-
|
|
453
|
+
### Isolation
|
|
454
|
+
|
|
455
|
+
You may notice that we stub `User.authenticate` in our test rather than creating
|
|
456
|
+
users in the database. That's because our purpose in
|
|
457
|
+
`spec/interactors/authenticate_user_spec.rb` is to test just the
|
|
458
|
+
`AuthenticateUser` interactor. The `User.authenticate` method is put through its
|
|
459
|
+
own paces in `spec/models/user_spec.rb`.
|
|
460
|
+
|
|
461
|
+
It's a good idea to define your own interfaces to your models. Doing so makes it
|
|
462
|
+
easy to draw a line between which responsibilities belong to the interactor and
|
|
463
|
+
which to the model. The `User.authenticate` method is a good, clear line.
|
|
464
|
+
Imagine the interactor otherwise:
|
|
231
465
|
|
|
232
466
|
```ruby
|
|
233
|
-
class
|
|
467
|
+
class AuthenticateUser
|
|
234
468
|
include Interactor
|
|
235
469
|
|
|
236
|
-
def
|
|
237
|
-
|
|
238
|
-
|
|
470
|
+
def call
|
|
471
|
+
user = User.where(email: context.email).first
|
|
472
|
+
|
|
473
|
+
# Yuck!
|
|
474
|
+
if user && BCrypt::Password.new(user.password_digest) == context.password
|
|
475
|
+
context.user = user
|
|
239
476
|
else
|
|
240
|
-
fail!
|
|
477
|
+
context.fail!(message: "authenticate_user.failure")
|
|
241
478
|
end
|
|
242
479
|
end
|
|
243
480
|
end
|
|
244
481
|
```
|
|
245
482
|
|
|
483
|
+
It would be very difficult to test this interactor in isolation and even if you
|
|
484
|
+
did, as soon as you change your ORM or your encryption algorithm (both model
|
|
485
|
+
concerns), your interactors (business concerns) break.
|
|
486
|
+
|
|
487
|
+
*Draw clear lines.*
|
|
488
|
+
|
|
489
|
+
### Integration
|
|
490
|
+
|
|
491
|
+
While it's important to test your interactors in isolation, it's just as
|
|
492
|
+
important to write good integration or acceptance tests.
|
|
493
|
+
|
|
494
|
+
One of the pitfalls of testing in isolation is that when you stub a method, you
|
|
495
|
+
could be hiding the fact that the method is broken, has changed or doesn't even
|
|
496
|
+
exist.
|
|
497
|
+
|
|
498
|
+
When you write full-stack tests that tie all of the pieces together, you can be
|
|
499
|
+
sure that your application's individual pieces are working together as expected.
|
|
500
|
+
That becomes even more important when you add a new layer to your code like
|
|
501
|
+
interactors.
|
|
502
|
+
|
|
503
|
+
**TIP:** If you track your test coverage, try for 100% coverage *before*
|
|
504
|
+
integrations tests. Then keep writing integration tests until you sleep well at
|
|
505
|
+
night.
|
|
506
|
+
|
|
507
|
+
### Controllers
|
|
508
|
+
|
|
509
|
+
One of the advantages of using interactors is how much they simplify controllers
|
|
510
|
+
and their tests. Because you're testing your interactors thoroughly in isolation
|
|
511
|
+
as well as in integration tests (right?), you can remove your business logic
|
|
512
|
+
from your controller tests.
|
|
513
|
+
|
|
246
514
|
```ruby
|
|
247
|
-
class
|
|
248
|
-
|
|
515
|
+
class SessionsController < ApplicationController
|
|
516
|
+
def create
|
|
517
|
+
result = AuthenticateUser.call(session_params)
|
|
249
518
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
519
|
+
if result.success?
|
|
520
|
+
session[:user_token] = result.token
|
|
521
|
+
redirect_to root_path
|
|
522
|
+
else
|
|
523
|
+
flash.now[:message] = t(result.message)
|
|
524
|
+
render :new
|
|
254
525
|
end
|
|
255
526
|
end
|
|
256
|
-
end
|
|
257
|
-
```
|
|
258
527
|
|
|
259
|
-
|
|
528
|
+
private
|
|
260
529
|
|
|
261
|
-
|
|
530
|
+
def session_params
|
|
531
|
+
params.require(:session).permit(:email, :password)
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
```
|
|
262
535
|
|
|
263
|
-
|
|
536
|
+
```ruby
|
|
537
|
+
describe SessionsController do
|
|
538
|
+
describe "#create" do
|
|
539
|
+
before do
|
|
540
|
+
expect(AuthenticateUser).to receive(:call).once.with(email: "john@doe.com", password: "secret").and_return(context)
|
|
541
|
+
end
|
|
264
542
|
|
|
265
|
-
|
|
543
|
+
context "when successful" do
|
|
544
|
+
let(:user) { double(:user) }
|
|
545
|
+
let(:context) { double(:context, success?: true, user: user, token: "token") }
|
|
266
546
|
|
|
267
|
-
|
|
547
|
+
it "saves the user's secret token in the session" do
|
|
548
|
+
expect {
|
|
549
|
+
post :create, session: { email: "john@doe.com", password: "secret" }
|
|
550
|
+
}.to change {
|
|
551
|
+
session[:user_token]
|
|
552
|
+
}.from(nil).to("token")
|
|
553
|
+
end
|
|
268
554
|
|
|
269
|
-
|
|
555
|
+
it "redirects to the homepage" do
|
|
556
|
+
response = post :create, session: { email: "john@doe.com", password: "secret" }
|
|
270
557
|
|
|
271
|
-
|
|
558
|
+
expect(response).to redirect_to(root_path)
|
|
559
|
+
end
|
|
560
|
+
end
|
|
272
561
|
|
|
273
|
-
|
|
562
|
+
context "when unsuccessful" do
|
|
563
|
+
let(:context) { double(:context, success?: false, message: "message") }
|
|
274
564
|
|
|
275
|
-
|
|
565
|
+
it "sets a flash message" do
|
|
566
|
+
expect {
|
|
567
|
+
post :create, session: { email: "john@doe.com", password: "secret" }
|
|
568
|
+
}.to change {
|
|
569
|
+
flash[:message]
|
|
570
|
+
}.from(nil).to(I18n.translate("message"))
|
|
571
|
+
end
|
|
276
572
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
include Interactor
|
|
573
|
+
it "renders the login form" do
|
|
574
|
+
response = post :create, session: { email: "john@doe.com", password: "secret" }
|
|
280
575
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
# Before, context[:user] held a user ID.
|
|
285
|
-
# This is bad.
|
|
576
|
+
expect(response).to render_template(:new)
|
|
577
|
+
end
|
|
578
|
+
end
|
|
286
579
|
end
|
|
287
580
|
end
|
|
288
581
|
```
|
|
289
582
|
|
|
290
|
-
|
|
583
|
+
This controller test will have to change very little during the life of the
|
|
584
|
+
application because all of the magic happens in the interactor.
|
|
291
585
|
|
|
292
586
|
### Rails
|
|
293
587
|
|
|
294
|
-
We love Rails, and we use Interactor with Rails. We
|
|
588
|
+
[We](http://collectiveidea.com) love Rails, and we use Interactor with Rails. We
|
|
589
|
+
put our interactors in `app/interactors` and we name them as verbs:
|
|
295
590
|
|
|
296
591
|
* `AddProductToCart`
|
|
297
592
|
* `AuthenticateUser`
|
|
@@ -299,27 +594,22 @@ We love Rails, and we use Interactor with Rails. We put our interactors in `app/
|
|
|
299
594
|
* `RegisterUser`
|
|
300
595
|
* `RemoveProductFromCart`
|
|
301
596
|
|
|
302
|
-
See [Interactor Rails](https://github.com/collectiveidea/interactor-rails)
|
|
597
|
+
See: [Interactor Rails](https://github.com/collectiveidea/interactor-rails)
|
|
303
598
|
|
|
304
599
|
## Contributions
|
|
305
600
|
|
|
306
|
-
Interactor is open source and contributions from the community are encouraged!
|
|
307
|
-
|
|
308
|
-
* adding an awesome feature
|
|
309
|
-
* fixing a terrible bug
|
|
310
|
-
* updating documentation
|
|
311
|
-
* fixing a not-so-bad bug
|
|
312
|
-
* fixing typos
|
|
313
|
-
|
|
314
|
-
For the best chance of having your changes merged, please:
|
|
601
|
+
Interactor is open source and contributions from the community are encouraged!
|
|
602
|
+
No contribution is too small.
|
|
315
603
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
3. Commit your changes and tests (if applicable (they're applicable)).
|
|
319
|
-
4. Submit a pull request with a thorough explanation and at least one animated GIF.
|
|
604
|
+
See Interactor's
|
|
605
|
+
[contribution guidelines](CONTRIBUTING.md) for more information.
|
|
320
606
|
|
|
321
|
-
##
|
|
607
|
+
## Thank You
|
|
322
608
|
|
|
323
|
-
A very special thank you to [Attila Domokos](https://github.com/adomokos) for
|
|
609
|
+
A very special thank you to [Attila Domokos](https://github.com/adomokos) for
|
|
610
|
+
his fantastic work on [LightService](https://github.com/adomokos/light-service).
|
|
611
|
+
Interactor is inspired heavily by the concepts put to code by Attila.
|
|
324
612
|
|
|
325
|
-
Interactor was born from a desire for a slightly
|
|
613
|
+
Interactor was born from a desire for a slightly simplified interface. We
|
|
614
|
+
understand that this is a matter of personal preference, so please take a look
|
|
615
|
+
at LightService as well!
|