interaktor 0.1.3 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +25 -93
- data/Gemfile +5 -4
- data/README.md +154 -298
- data/interaktor.gemspec +2 -2
- data/lib/interaktor.rb +54 -15
- data/lib/interaktor/context.rb +10 -0
- data/lib/interaktor/organizer.rb +1 -1
- data/spec/integration_spec.rb +238 -228
- data/spec/interactor/context_spec.rb +21 -21
- data/spec/interactor/hooks_spec.rb +4 -0
- data/spec/interactor/organizer_spec.rb +13 -14
- data/spec/{interactor_spec.rb → interaktor_spec.rb} +0 -0
- data/spec/support/lint.rb +31 -20
- metadata +9 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6a783c3017cd4cb48ee61f7d68869a1b7b2e3a716657c3c1bc8ca230b8e7f483
|
4
|
+
data.tar.gz: d5966a7b86cbfe7af7892002e8eee10cc10872743717b7501fee850a30ebc9a5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1f0df7c0eb75c1a3594c5f9e2c53fa7561fa8349c2cec2408f7d2a38be4e9c8ab80403fc63d6dee36f8f9e449c27fd9842912569bb80a30cf302ab398ccc5ddd
|
7
|
+
data.tar.gz: f6333614072980b7bee34734ba34f8ee0d1881dc6aa756eaaa42e2ba4058cfcc85580fe2d2c83188a060dd185b1b322c62be188ae137acb9b38adbebcc9b7762
|
data/.rubocop.yml
CHANGED
@@ -4,7 +4,9 @@ require:
|
|
4
4
|
|
5
5
|
AllCops:
|
6
6
|
TargetRubyVersion: 2.5
|
7
|
+
NewCops: enable
|
7
8
|
Exclude:
|
9
|
+
- "bin/**/*"
|
8
10
|
- "tmp/**/*"
|
9
11
|
- "vendor/**/*"
|
10
12
|
|
@@ -34,12 +36,6 @@ Gemspec:
|
|
34
36
|
Layout:
|
35
37
|
Enabled: true
|
36
38
|
|
37
|
-
Layout/HeredocIndentation:
|
38
|
-
Enabled: false
|
39
|
-
|
40
|
-
Layout/ClosingHeredocIndentation:
|
41
|
-
Enabled: false
|
42
|
-
|
43
39
|
Layout/LineLength:
|
44
40
|
Enabled: true
|
45
41
|
Max: 120
|
@@ -50,13 +46,9 @@ Layout/RescueEnsureAlignment:
|
|
50
46
|
Layout/CaseIndentation:
|
51
47
|
Enabled: false
|
52
48
|
|
53
|
-
|
54
|
-
|
55
|
-
Layout/EmptyLinesAroundAttributeAccessor:
|
56
|
-
Enabled: true
|
57
|
-
|
58
|
-
Layout/SpaceAroundMethodCallOperator:
|
49
|
+
Layout/SpaceAroundOperators:
|
59
50
|
Enabled: true
|
51
|
+
EnforcedStyleForExponentOperator: space
|
60
52
|
|
61
53
|
# Rufo already agrees with these
|
62
54
|
|
@@ -90,6 +82,12 @@ Layout/SpaceInsideBlockBraces:
|
|
90
82
|
Enabled: true
|
91
83
|
EnforcedStyleForEmptyBraces: space
|
92
84
|
|
85
|
+
Layout/HeredocIndentation:
|
86
|
+
Enabled: false
|
87
|
+
|
88
|
+
Layout/ClosingHeredocIndentation:
|
89
|
+
Enabled: false
|
90
|
+
|
93
91
|
########
|
94
92
|
# LINT #
|
95
93
|
########
|
@@ -97,19 +95,9 @@ Layout/SpaceInsideBlockBraces:
|
|
97
95
|
Lint:
|
98
96
|
Enabled: true
|
99
97
|
|
100
|
-
#
|
101
|
-
|
102
|
-
|
103
|
-
Enabled: true
|
104
|
-
|
105
|
-
Lint/MixedRegexpCaptureTypes:
|
106
|
-
Enabled: true
|
107
|
-
|
108
|
-
Lint/StructNewOverride:
|
109
|
-
Enabled: true
|
110
|
-
|
111
|
-
Lint/RaiseException:
|
112
|
-
Enabled: true
|
98
|
+
# This one was giving a false positive
|
99
|
+
Lint/RedundantCopDisableDirective:
|
100
|
+
Enabled: false
|
113
101
|
|
114
102
|
###########
|
115
103
|
# METRICS #
|
@@ -125,6 +113,9 @@ Metrics/MethodLength:
|
|
125
113
|
Metrics/ClassLength:
|
126
114
|
Enabled: false
|
127
115
|
|
116
|
+
Metrics/ModuleLength:
|
117
|
+
Enabled: false
|
118
|
+
|
128
119
|
Metrics/BlockLength:
|
129
120
|
Enabled: true
|
130
121
|
Max: 25
|
@@ -179,8 +170,7 @@ Style/WordArray:
|
|
179
170
|
Enabled: false
|
180
171
|
|
181
172
|
Style/ClassAndModuleChildren:
|
182
|
-
Enabled:
|
183
|
-
EnforcedStyle: compact
|
173
|
+
Enabled: false
|
184
174
|
|
185
175
|
Style/Documentation:
|
186
176
|
Enabled: false
|
@@ -197,17 +187,17 @@ Style/StringLiteralsInInterpolation:
|
|
197
187
|
Enabled: true
|
198
188
|
EnforcedStyle: double_quotes
|
199
189
|
|
190
|
+
# Can never agree with Rufo
|
191
|
+
|
200
192
|
Style/TrailingCommaInHashLiteral:
|
201
|
-
Enabled:
|
202
|
-
EnforcedStyleForMultiline: consistent_comma
|
193
|
+
Enabled: false
|
203
194
|
|
204
195
|
Style/TrailingCommaInArrayLiteral:
|
205
|
-
Enabled:
|
206
|
-
EnforcedStyleForMultiline: consistent_comma
|
196
|
+
Enabled: false
|
207
197
|
|
208
198
|
Style/TrailingCommaInArguments:
|
209
199
|
Enabled: true
|
210
|
-
EnforcedStyleForMultiline:
|
200
|
+
EnforcedStyleForMultiline: comma
|
211
201
|
|
212
202
|
Style/StringLiterals:
|
213
203
|
Enabled: true
|
@@ -217,49 +207,17 @@ Style/FrozenStringLiteralComment:
|
|
217
207
|
Enabled: false
|
218
208
|
|
219
209
|
Style/RedundantReturn:
|
220
|
-
Enabled:
|
210
|
+
Enabled: false
|
221
211
|
|
222
212
|
Style/TernaryParentheses:
|
223
|
-
Enabled:
|
213
|
+
Enabled: false
|
224
214
|
|
225
215
|
Style/RedundantParentheses:
|
226
|
-
Enabled:
|
227
|
-
|
228
|
-
# The following cops are not yet enabled or disabled by default.
|
216
|
+
Enabled: false
|
229
217
|
|
230
218
|
Style/AccessorGrouping:
|
231
219
|
Enabled: false
|
232
220
|
|
233
|
-
Style/BisectedAttrAccessor:
|
234
|
-
Enabled: true
|
235
|
-
|
236
|
-
Style/RedundantAssignment:
|
237
|
-
Enabled: true
|
238
|
-
|
239
|
-
Style/ExponentialNotation:
|
240
|
-
Enabled: true
|
241
|
-
|
242
|
-
Style/HashEachMethods:
|
243
|
-
Enabled: true
|
244
|
-
|
245
|
-
Style/HashTransformKeys:
|
246
|
-
Enabled: true
|
247
|
-
|
248
|
-
Style/HashTransformValues:
|
249
|
-
Enabled: true
|
250
|
-
|
251
|
-
Style/RedundantFetchBlock:
|
252
|
-
Enabled: true
|
253
|
-
|
254
|
-
Style/RedundantRegexpCharacterClass:
|
255
|
-
Enabled: true
|
256
|
-
|
257
|
-
Style/RedundantRegexpEscape:
|
258
|
-
Enabled: true
|
259
|
-
|
260
|
-
Style/SlicingWithRange:
|
261
|
-
Enabled: true
|
262
|
-
|
263
221
|
#########
|
264
222
|
# RSPEC #
|
265
223
|
#########
|
@@ -285,29 +243,3 @@ RSpec/MessageSpies:
|
|
285
243
|
|
286
244
|
Performance:
|
287
245
|
Enabled: true
|
288
|
-
|
289
|
-
# These cops are not enabled or disabled by default.
|
290
|
-
|
291
|
-
Performance/AncestorsInclude:
|
292
|
-
Enabled: true
|
293
|
-
|
294
|
-
Performance/BigDecimalWithNumericArgument:
|
295
|
-
Enabled: true
|
296
|
-
|
297
|
-
Performance/RedundantSortBlock:
|
298
|
-
Enabled: true
|
299
|
-
|
300
|
-
Performance/RedundantStringChars:
|
301
|
-
Enabled: true
|
302
|
-
|
303
|
-
Performance/ReverseFirst:
|
304
|
-
Enabled: true
|
305
|
-
|
306
|
-
Performance/SortReverse:
|
307
|
-
Enabled: true
|
308
|
-
|
309
|
-
Performance/Squeeze:
|
310
|
-
Enabled: true
|
311
|
-
|
312
|
-
Performance/StringInclude:
|
313
|
-
Enabled: true
|
data/Gemfile
CHANGED
@@ -2,13 +2,14 @@ source "https://rubygems.org"
|
|
2
2
|
|
3
3
|
gemspec
|
4
4
|
|
5
|
-
gem "rubocop"
|
6
|
-
gem "rubocop-performance"
|
7
|
-
gem "rubocop-rspec"
|
5
|
+
gem "rubocop"
|
6
|
+
gem "rubocop-performance"
|
7
|
+
gem "rubocop-rspec"
|
8
8
|
gem "rufo", "~> 0.12.0"
|
9
|
-
gem "solargraph"
|
9
|
+
gem "solargraph"
|
10
10
|
|
11
11
|
group :test do
|
12
12
|
gem "codeclimate-test-reporter", "~> 1.0.9", require: false
|
13
|
+
gem "pry-byebug"
|
13
14
|
gem "rspec", "~> 3.9.0"
|
14
15
|
end
|
data/README.md
CHANGED
@@ -1,120 +1,131 @@
|
|
1
|
-
|
1
|
+
# Interaktor
|
2
2
|
|
3
|
-
|
3
|
+
[![Gem Version](https://img.shields.io/gem/v/interaktor.svg)](http://rubygems.org/gems/interaktor)
|
4
|
+
[![Build Status](https://img.shields.io/travis/collectiveidea/interaktor/master.svg)](https://travis-ci.org/taylorthurlow/interaktor)
|
4
5
|
|
5
|
-
|
6
|
+
**Interaktor** is a fork of [Interaktor by collectiveidea](https://github.com/collectiveidea/interaktor). While Interaktor is still used by collectiveidea internally, communication and progress has been slow in adapting to pull requests and issues. This inactivity combined with my desire to dial back on the Interaktor's inherent permissivity led me to fork it and create Interaktor.
|
6
7
|
|
7
|
-
|
8
|
-
[![Build Status](https://img.shields.io/travis/collectiveidea/interactor/master.svg)](https://travis-ci.org/collectiveidea/interactor)
|
9
|
-
[![Maintainability](https://img.shields.io/codeclimate/maintainability/collectiveidea/interactor.svg)](https://codeclimate.com/github/collectiveidea/interactor)
|
10
|
-
[![Test Coverage](https://img.shields.io/codeclimate/coverage-letter/collectiveidea/interactor.svg)](https://codeclimate.com/github/collectiveidea/interactor)
|
11
|
-
[![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
|
8
|
+
Fundamentally, Interaktor is the same as Interactor, but with a small DSL which is used to define attributes passed into the interaktor, such as:
|
12
9
|
|
13
|
-
|
10
|
+
- Required attributes
|
11
|
+
- Optional attributes
|
12
|
+
- Attributes required on interaktor failure
|
13
|
+
- Attributes required on interaktor success
|
14
14
|
|
15
|
-
|
15
|
+
## Getting started
|
16
|
+
|
17
|
+
Add `interaktor` to your Gemfile and `bundle install`.
|
16
18
|
|
17
19
|
```ruby
|
18
|
-
gem "
|
20
|
+
gem "interaktor", "~> 0.1"
|
19
21
|
```
|
20
22
|
|
21
|
-
## What is an
|
22
|
-
|
23
|
-
An interactor is a simple, single-purpose object.
|
23
|
+
## What is an interaktor?
|
24
24
|
|
25
|
-
|
26
|
-
[business logic](http://en.wikipedia.org/wiki/Business_logic). Each interactor
|
27
|
-
represents one thing that your application *does*.
|
25
|
+
An interaktor is a simple, single-purpose object.
|
28
26
|
|
29
|
-
|
27
|
+
Interaktors are used to encapsulate your application's [business logic](http://en.wikipedia.org/wiki/Business_logic). Each interaktor represents one thing that your application _does_.
|
30
28
|
|
31
|
-
|
32
|
-
interactor needs to do its work.
|
29
|
+
### Attributes
|
33
30
|
|
34
|
-
|
31
|
+
#### Input attributes
|
35
32
|
|
36
|
-
|
33
|
+
Depending on its definition, an interaktor may require attributes to be passed in when it is invoked. These attributes contain everything the interaktor needs to do its work.
|
37
34
|
|
38
|
-
|
35
|
+
You may define `required` or `optional` attributes.
|
39
36
|
|
40
37
|
```ruby
|
41
|
-
|
42
|
-
|
38
|
+
class CreateUser
|
39
|
+
include Interaktor
|
43
40
|
|
44
|
-
|
41
|
+
required :name
|
45
42
|
|
46
|
-
|
47
|
-
failed.
|
43
|
+
optional :email
|
48
44
|
|
49
|
-
|
50
|
-
|
51
|
-
|
45
|
+
def call
|
46
|
+
User.create!(
|
47
|
+
name: name,
|
48
|
+
email: email,
|
49
|
+
)
|
50
|
+
end
|
51
|
+
end
|
52
52
|
|
53
|
-
|
54
|
-
following are equivalent:
|
53
|
+
CreateUser.call(name: "Foo Bar")
|
55
54
|
|
56
|
-
```ruby
|
57
|
-
context.error = "Boom!"
|
58
|
-
context.fail!
|
59
55
|
```
|
60
56
|
|
61
|
-
|
62
|
-
|
63
|
-
|
57
|
+
#### Output attributes
|
58
|
+
|
59
|
+
Based on the outcome of the interaktor's work, we can require certain attributes. In the example below, we must succeed with a `user_id` attribute, and if we fail, we must provide an `error_messages` attribute.
|
64
60
|
|
65
|
-
|
61
|
+
The use of `#success!` allows you to early-return from an interaktor's work. If no `success` attribute is provided, and the `call` method finishes execution normally, then the interaktor is considered to be in a successful state.
|
66
62
|
|
67
63
|
```ruby
|
68
|
-
|
69
|
-
|
70
|
-
context.failure? # => true
|
71
|
-
```
|
64
|
+
class CreateUser
|
65
|
+
include Interaktor
|
72
66
|
|
73
|
-
|
67
|
+
required :name
|
74
68
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
69
|
+
success :user_id
|
70
|
+
|
71
|
+
failure :error_messages
|
72
|
+
|
73
|
+
def call
|
74
|
+
user = User.new(name: name)
|
75
|
+
|
76
|
+
if user.save
|
77
|
+
success!(user_id: user.id)
|
78
|
+
else
|
79
|
+
fail!(error_messages: user.errors.full_messages)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
result = CreateUser.call(name: "Foo Bar")
|
85
|
+
|
86
|
+
if result.success?
|
87
|
+
puts "The new user ID is: #{result.user_id}".
|
88
|
+
else
|
89
|
+
puts "Creating the user failed: #{result.error_messages.join(", ")}".
|
90
|
+
end
|
79
91
|
```
|
80
92
|
|
81
|
-
#### Dealing with
|
93
|
+
#### Dealing with sailure
|
82
94
|
|
83
|
-
`context.fail!` always throws an exception of type `
|
95
|
+
`context.fail!` always throws an exception of type `Interaktor::Failure`.
|
84
96
|
|
85
|
-
Normally, however, these exceptions are not seen. In the recommended usage, the controller invokes the
|
97
|
+
Normally, however, these exceptions are not seen. In the recommended usage, the controller invokes the interaktor using the class method `.call`, then checks the `#success?` method of the context.
|
86
98
|
|
87
|
-
This works because the `call` class method swallows exceptions.
|
99
|
+
This works because the `call` class method swallows exceptions. When unit testing an interaktor, if calling custom business logic methods directly and bypassing `call`, be aware that `fail!` will generate such exceptions.
|
88
100
|
|
89
|
-
See
|
101
|
+
See _Interaktors in the controller_, below, for the recommended usage of `call` and `success?`.
|
90
102
|
|
91
103
|
### Hooks
|
92
104
|
|
93
|
-
#### Before
|
105
|
+
#### Before hooks
|
94
106
|
|
95
|
-
Sometimes an
|
96
|
-
even run. This can be done with before hooks on the interactor.
|
107
|
+
Sometimes an interaktor needs to prepare its context before the interaktor is even run. This can be done with before hooks on the interaktor.
|
97
108
|
|
98
109
|
```ruby
|
99
110
|
before do
|
100
|
-
|
111
|
+
# Do some stuff
|
101
112
|
end
|
102
113
|
```
|
103
114
|
|
104
115
|
A symbol argument can also be given, rather than a block.
|
105
116
|
|
106
117
|
```ruby
|
107
|
-
before :
|
118
|
+
before :do_some_stuff
|
108
119
|
|
109
|
-
def
|
110
|
-
|
120
|
+
def do_some_stuff
|
121
|
+
# Do some stuff
|
111
122
|
end
|
112
123
|
```
|
113
124
|
|
114
|
-
#### After
|
125
|
+
#### After hooks
|
115
126
|
|
116
|
-
|
117
|
-
is run.
|
127
|
+
Interaktors can also perform teardown operations after the interaktor instance
|
128
|
+
is run. They are only run on success.
|
118
129
|
|
119
130
|
```ruby
|
120
131
|
after do
|
@@ -122,53 +133,48 @@ after do
|
|
122
133
|
end
|
123
134
|
```
|
124
135
|
|
125
|
-
|
126
|
-
|
127
|
-
#### Around Hooks
|
136
|
+
#### Around hooks
|
128
137
|
|
129
|
-
You can also define around hooks in the same way as before or after hooks, using
|
130
|
-
either a block or a symbol method name. The difference is that an around block
|
131
|
-
or method accepts a single argument. Invoking the `call` method on that argument
|
132
|
-
will continue invocation of the interactor. For example, with a block:
|
138
|
+
You can also define around hooks in the same way as before or after hooks, using either a block or a symbol method name. The difference is that an around block or method accepts a single argument. Invoking the `call` method on that argument will continue invocation of the interaktor. For example, with a block:
|
133
139
|
|
134
140
|
```ruby
|
135
|
-
around do |
|
136
|
-
|
137
|
-
|
138
|
-
|
141
|
+
around do |interaktor|
|
142
|
+
# Do stuff before
|
143
|
+
interaktor.call
|
144
|
+
# Do stuff after
|
139
145
|
end
|
140
146
|
```
|
141
147
|
|
142
148
|
With a method:
|
143
149
|
|
144
150
|
```ruby
|
145
|
-
around :
|
151
|
+
around :do_stuff_around
|
146
152
|
|
147
|
-
def
|
148
|
-
|
149
|
-
|
150
|
-
|
153
|
+
def do_stuff_around(interaktor)
|
154
|
+
# Do stuff before
|
155
|
+
interaktor.call
|
156
|
+
# Do stuff after
|
151
157
|
end
|
152
158
|
```
|
153
159
|
|
154
|
-
|
160
|
+
If `#fail!` is called, any code defined in the hook after the call to the interaktor will not be run.
|
155
161
|
|
156
|
-
#### Hook
|
162
|
+
#### Hook sequence
|
157
163
|
|
158
164
|
Before hooks are invoked in the order in which they were defined while after
|
159
165
|
hooks are invoked in the opposite order. Around hooks are invoked outside of any
|
160
166
|
defined before and after hooks. For example:
|
161
167
|
|
162
168
|
```ruby
|
163
|
-
around do |
|
169
|
+
around do |interaktor|
|
164
170
|
puts "around before 1"
|
165
|
-
|
171
|
+
interaktor.call
|
166
172
|
puts "around after 1"
|
167
173
|
end
|
168
174
|
|
169
|
-
around do |
|
175
|
+
around do |interaktor|
|
170
176
|
puts "around before 2"
|
171
|
-
|
177
|
+
interaktor.call
|
172
178
|
puts "around after 2"
|
173
179
|
end
|
174
180
|
|
@@ -202,189 +208,39 @@ around after 2
|
|
202
208
|
around after 1
|
203
209
|
```
|
204
210
|
|
205
|
-
####
|
211
|
+
#### Interaktor concerns
|
206
212
|
|
207
|
-
An
|
208
|
-
be extracted into
|
213
|
+
An interaktor can define multiple before/after hooks, allowing common hooks to
|
214
|
+
be extracted into interaktor concerns.
|
209
215
|
|
210
216
|
```ruby
|
211
|
-
module
|
217
|
+
module InteraktorDoStuff
|
212
218
|
extend ActiveSupport::Concern
|
213
219
|
|
214
220
|
included do
|
215
|
-
around do |
|
216
|
-
|
217
|
-
|
218
|
-
|
221
|
+
around do |interaktor|
|
222
|
+
# Do stuff before
|
223
|
+
interaktor.call
|
224
|
+
# Do stuff after
|
219
225
|
end
|
220
226
|
end
|
221
227
|
end
|
222
228
|
```
|
223
229
|
|
224
|
-
|
225
|
-
|
226
|
-
Your application could use an interactor to authenticate a user.
|
227
|
-
|
228
|
-
```ruby
|
229
|
-
class AuthenticateUser
|
230
|
-
include Interactor
|
231
|
-
|
232
|
-
def call
|
233
|
-
if user = User.authenticate(context.email, context.password)
|
234
|
-
context.user = user
|
235
|
-
context.token = user.secret_token
|
236
|
-
else
|
237
|
-
context.fail!(message: "authenticate_user.failure")
|
238
|
-
end
|
239
|
-
end
|
240
|
-
end
|
241
|
-
```
|
242
|
-
|
243
|
-
To define an interactor, simply create a class that includes the `Interactor`
|
244
|
-
module and give it a `call` instance method. The interactor can access its
|
245
|
-
`context` from within `call`.
|
246
|
-
|
247
|
-
## Interactors in the Controller
|
248
|
-
|
249
|
-
Most of the time, your application will use its interactors from its
|
250
|
-
controllers. The following controller:
|
251
|
-
|
252
|
-
```ruby
|
253
|
-
class SessionsController < ApplicationController
|
254
|
-
def create
|
255
|
-
if user = User.authenticate(session_params[:email], session_params[:password])
|
256
|
-
session[:user_token] = user.secret_token
|
257
|
-
redirect_to user
|
258
|
-
else
|
259
|
-
flash.now[:message] = "Please try again."
|
260
|
-
render :new
|
261
|
-
end
|
262
|
-
end
|
263
|
-
|
264
|
-
private
|
265
|
-
|
266
|
-
def session_params
|
267
|
-
params.require(:session).permit(:email, :password)
|
268
|
-
end
|
269
|
-
end
|
270
|
-
```
|
271
|
-
|
272
|
-
can be refactored to:
|
273
|
-
|
274
|
-
```ruby
|
275
|
-
class SessionsController < ApplicationController
|
276
|
-
def create
|
277
|
-
result = AuthenticateUser.call(session_params)
|
278
|
-
|
279
|
-
if result.success?
|
280
|
-
session[:user_token] = result.token
|
281
|
-
redirect_to result.user
|
282
|
-
else
|
283
|
-
flash.now[:message] = t(result.message)
|
284
|
-
render :new
|
285
|
-
end
|
286
|
-
end
|
287
|
-
|
288
|
-
private
|
289
|
-
|
290
|
-
def session_params
|
291
|
-
params.require(:session).permit(:email, :password)
|
292
|
-
end
|
293
|
-
end
|
294
|
-
```
|
295
|
-
|
296
|
-
The `call` class method is the proper way to invoke an interactor. The hash
|
297
|
-
argument is converted to the interactor instance's context. The `call` instance
|
298
|
-
method is invoked along with any hooks that the interactor might define.
|
299
|
-
Finally, the context (along with any changes made to it) is returned.
|
300
|
-
|
301
|
-
## When to Use an Interactor
|
302
|
-
|
303
|
-
Given the user authentication example, your controller may look like:
|
304
|
-
|
305
|
-
```ruby
|
306
|
-
class SessionsController < ApplicationController
|
307
|
-
def create
|
308
|
-
result = AuthenticateUser.call(session_params)
|
309
|
-
|
310
|
-
if result.success?
|
311
|
-
session[:user_token] = result.token
|
312
|
-
redirect_to result.user
|
313
|
-
else
|
314
|
-
flash.now[:message] = t(result.message)
|
315
|
-
render :new
|
316
|
-
end
|
317
|
-
end
|
318
|
-
|
319
|
-
private
|
320
|
-
|
321
|
-
def session_params
|
322
|
-
params.require(:session).permit(:email, :password)
|
323
|
-
end
|
324
|
-
end
|
325
|
-
```
|
326
|
-
|
327
|
-
For such a simple use case, using an interactor can actually require *more*
|
328
|
-
code. So why use an interactor?
|
329
|
-
|
330
|
-
### Clarity
|
331
|
-
|
332
|
-
[We](http://collectiveidea.com) often use interactors right off the bat for all
|
333
|
-
of our destructive actions (`POST`, `PUT` and `DELETE` requests) and since we
|
334
|
-
put our interactors in `app/interactors`, a glance at that directory gives any
|
335
|
-
developer a quick understanding of everything the application *does*.
|
336
|
-
|
337
|
-
```
|
338
|
-
▾ app/
|
339
|
-
▸ controllers/
|
340
|
-
▸ helpers/
|
341
|
-
▾ interactors/
|
342
|
-
authenticate_user.rb
|
343
|
-
cancel_account.rb
|
344
|
-
publish_post.rb
|
345
|
-
register_user.rb
|
346
|
-
remove_post.rb
|
347
|
-
▸ mailers/
|
348
|
-
▸ models/
|
349
|
-
▸ views/
|
350
|
-
```
|
351
|
-
|
352
|
-
**TIP:** Name your interactors after your business logic, not your
|
353
|
-
implementation. `CancelAccount` will serve you better than `DestroyUser` as the
|
354
|
-
account cancellation interaction takes on more responsibility in the future.
|
355
|
-
|
356
|
-
### The Future™
|
357
|
-
|
358
|
-
**SPOILER ALERT:** Your use case won't *stay* so simple.
|
359
|
-
|
360
|
-
In [our](http://collectiveidea.com) experience, a simple task like
|
361
|
-
authenticating a user will eventually take on multiple responsibilities:
|
362
|
-
|
363
|
-
* Welcoming back a user who hadn't logged in for a while
|
364
|
-
* Prompting a user to update his or her password
|
365
|
-
* Locking out a user in the case of too many failed attempts
|
366
|
-
* Sending the lock-out email notification
|
367
|
-
|
368
|
-
The list goes on, and as that list grows, so does your controller. This is how
|
369
|
-
fat controllers are born.
|
370
|
-
|
371
|
-
If instead you use an interactor right away, as responsibilities are added, your
|
372
|
-
controller (and its tests) change very little or not at all. Choosing the right
|
373
|
-
kind of interactor can also prevent simply shifting those added responsibilities
|
374
|
-
to the interactor.
|
230
|
+
# All documentation below this line has not been updated to reflect the fork from Interactor.
|
375
231
|
|
376
|
-
## Kinds of
|
232
|
+
## Kinds of interaktors
|
377
233
|
|
378
|
-
There are two kinds of
|
379
|
-
|
234
|
+
There are two kinds of interaktors built into the Interaktor library: basic
|
235
|
+
interaktors and organizers.
|
380
236
|
|
381
|
-
###
|
237
|
+
### Interaktors
|
382
238
|
|
383
|
-
A basic
|
239
|
+
A basic interaktor is a class that includes `Interaktor` and defines `call`.
|
384
240
|
|
385
241
|
```ruby
|
386
242
|
class AuthenticateUser
|
387
|
-
include
|
243
|
+
include Interaktor
|
388
244
|
|
389
245
|
def call
|
390
246
|
if user = User.authenticate(context.email, context.password)
|
@@ -397,24 +253,24 @@ class AuthenticateUser
|
|
397
253
|
end
|
398
254
|
```
|
399
255
|
|
400
|
-
Basic
|
256
|
+
Basic interaktors are the building blocks. They are your application's
|
401
257
|
single-purpose units of work.
|
402
258
|
|
403
259
|
### Organizers
|
404
260
|
|
405
|
-
An organizer is an important variation on the basic
|
406
|
-
purpose is to run
|
261
|
+
An organizer is an important variation on the basic interaktor. Its single
|
262
|
+
purpose is to run _other_ interaktors.
|
407
263
|
|
408
264
|
```ruby
|
409
265
|
class PlaceOrder
|
410
|
-
include
|
266
|
+
include Interaktor::Organizer
|
411
267
|
|
412
268
|
organize CreateOrder, ChargeCard, SendThankYou
|
413
269
|
end
|
414
270
|
```
|
415
271
|
|
416
272
|
In the controller, you can run the `PlaceOrder` organizer just like you would
|
417
|
-
any other
|
273
|
+
any other interaktor:
|
418
274
|
|
419
275
|
```ruby
|
420
276
|
class OrdersController < ApplicationController
|
@@ -437,22 +293,22 @@ class OrdersController < ApplicationController
|
|
437
293
|
end
|
438
294
|
```
|
439
295
|
|
440
|
-
The organizer passes its context to the
|
441
|
-
time and in order. Each
|
442
|
-
along to the next
|
296
|
+
The organizer passes its context to the interaktors that it organizes, one at a
|
297
|
+
time and in order. Each interaktor may change that context before it's passed
|
298
|
+
along to the next interaktor.
|
443
299
|
|
444
300
|
#### Rollback
|
445
301
|
|
446
|
-
If any one of the organized
|
447
|
-
If the `ChargeCard`
|
302
|
+
If any one of the organized interaktors fails its context, the organizer stops.
|
303
|
+
If the `ChargeCard` interaktor fails, `SendThankYou` is never called.
|
448
304
|
|
449
|
-
In addition, any
|
305
|
+
In addition, any interaktors that had already run are given the chance to undo
|
450
306
|
themselves, in reverse order. Simply define the `rollback` method on your
|
451
|
-
|
307
|
+
interaktors:
|
452
308
|
|
453
309
|
```ruby
|
454
310
|
class CreateOrder
|
455
|
-
include
|
311
|
+
include Interaktor
|
456
312
|
|
457
313
|
def call
|
458
314
|
order = Order.create(order_params)
|
@@ -470,18 +326,18 @@ class CreateOrder
|
|
470
326
|
end
|
471
327
|
```
|
472
328
|
|
473
|
-
**NOTE:** The
|
474
|
-
|
475
|
-
after any failed
|
329
|
+
**NOTE:** The interaktor that fails is _not_ rolled back. Because every
|
330
|
+
interaktor should have a single purpose, there should be no need to clean up
|
331
|
+
after any failed interaktor.
|
476
332
|
|
477
|
-
## Testing
|
333
|
+
## Testing interaktors
|
478
334
|
|
479
|
-
When written correctly, an
|
480
|
-
thing. Take the following
|
335
|
+
When written correctly, an interaktor is easy to test because it only _does_ one
|
336
|
+
thing. Take the following interaktor:
|
481
337
|
|
482
338
|
```ruby
|
483
339
|
class AuthenticateUser
|
484
|
-
include
|
340
|
+
include Interaktor
|
485
341
|
|
486
342
|
def call
|
487
343
|
if user = User.authenticate(context.email, context.password)
|
@@ -494,7 +350,7 @@ class AuthenticateUser
|
|
494
350
|
end
|
495
351
|
```
|
496
352
|
|
497
|
-
You can test just this
|
353
|
+
You can test just this interaktor's single purpose and how it affects the
|
498
354
|
context.
|
499
355
|
|
500
356
|
```ruby
|
@@ -546,18 +402,18 @@ testing framework.
|
|
546
402
|
|
547
403
|
You may notice that we stub `User.authenticate` in our test rather than creating
|
548
404
|
users in the database. That's because our purpose in
|
549
|
-
`spec/
|
550
|
-
`AuthenticateUser`
|
405
|
+
`spec/interaktors/authenticate_user_spec.rb` is to test just the
|
406
|
+
`AuthenticateUser` interaktor. The `User.authenticate` method is put through its
|
551
407
|
own paces in `spec/models/user_spec.rb`.
|
552
408
|
|
553
409
|
It's a good idea to define your own interfaces to your models. Doing so makes it
|
554
|
-
easy to draw a line between which responsibilities belong to the
|
410
|
+
easy to draw a line between which responsibilities belong to the interaktor and
|
555
411
|
which to the model. The `User.authenticate` method is a good, clear line.
|
556
|
-
Imagine the
|
412
|
+
Imagine the interaktor otherwise:
|
557
413
|
|
558
414
|
```ruby
|
559
415
|
class AuthenticateUser
|
560
|
-
include
|
416
|
+
include Interaktor
|
561
417
|
|
562
418
|
def call
|
563
419
|
user = User.where(email: context.email).first
|
@@ -572,15 +428,15 @@ class AuthenticateUser
|
|
572
428
|
end
|
573
429
|
```
|
574
430
|
|
575
|
-
It would be very difficult to test this
|
431
|
+
It would be very difficult to test this interaktor in isolation and even if you
|
576
432
|
did, as soon as you change your ORM or your encryption algorithm (both model
|
577
|
-
concerns), your
|
433
|
+
concerns), your interaktors (business concerns) break.
|
578
434
|
|
579
|
-
|
435
|
+
_Draw clear lines._
|
580
436
|
|
581
437
|
### Integration
|
582
438
|
|
583
|
-
While it's important to test your
|
439
|
+
While it's important to test your interaktors in isolation, it's just as
|
584
440
|
important to write good integration or acceptance tests.
|
585
441
|
|
586
442
|
One of the pitfalls of testing in isolation is that when you stub a method, you
|
@@ -590,16 +446,16 @@ exist.
|
|
590
446
|
When you write full-stack tests that tie all of the pieces together, you can be
|
591
447
|
sure that your application's individual pieces are working together as expected.
|
592
448
|
That becomes even more important when you add a new layer to your code like
|
593
|
-
|
449
|
+
interaktors.
|
594
450
|
|
595
|
-
**TIP:** If you track your test coverage, try for 100% coverage
|
451
|
+
**TIP:** If you track your test coverage, try for 100% coverage _before_
|
596
452
|
integrations tests. Then keep writing integration tests until you sleep well at
|
597
453
|
night.
|
598
454
|
|
599
455
|
### Controllers
|
600
456
|
|
601
|
-
One of the advantages of using
|
602
|
-
and their tests. Because you're testing your
|
457
|
+
One of the advantages of using interaktors is how much they simplify controllers
|
458
|
+
and their tests. Because you're testing your interaktors thoroughly in isolation
|
603
459
|
as well as in integration tests (right?), you can remove your business logic
|
604
460
|
from your controller tests.
|
605
461
|
|
@@ -673,35 +529,35 @@ end
|
|
673
529
|
```
|
674
530
|
|
675
531
|
This controller test will have to change very little during the life of the
|
676
|
-
application because all of the magic happens in the
|
532
|
+
application because all of the magic happens in the interaktor.
|
677
533
|
|
678
534
|
### Rails
|
679
535
|
|
680
|
-
[We](http://collectiveidea.com) love Rails, and we use
|
681
|
-
put our
|
536
|
+
[We](http://collectiveidea.com) love Rails, and we use Interaktor with Rails. We
|
537
|
+
put our interaktors in `app/interaktors` and we name them as verbs:
|
682
538
|
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
539
|
+
- `AddProductToCart`
|
540
|
+
- `AuthenticateUser`
|
541
|
+
- `PlaceOrder`
|
542
|
+
- `RegisterUser`
|
543
|
+
- `RemoveProductFromCart`
|
688
544
|
|
689
|
-
See: [
|
545
|
+
See: [Interaktor Rails](https://github.com/collectiveidea/interaktor-rails)
|
690
546
|
|
691
547
|
## Contributions
|
692
548
|
|
693
|
-
|
549
|
+
Interaktor is open source and contributions from the community are encouraged!
|
694
550
|
No contribution is too small.
|
695
551
|
|
696
|
-
See
|
552
|
+
See Interaktor's
|
697
553
|
[contribution guidelines](CONTRIBUTING.md) for more information.
|
698
554
|
|
699
555
|
## Thank You
|
700
556
|
|
701
557
|
A very special thank you to [Attila Domokos](https://github.com/adomokos) for
|
702
558
|
his fantastic work on [LightService](https://github.com/adomokos/light-service).
|
703
|
-
|
559
|
+
Interaktor is inspired heavily by the concepts put to code by Attila.
|
704
560
|
|
705
|
-
|
561
|
+
Interaktor was born from a desire for a slightly simplified interface. We
|
706
562
|
understand that this is a matter of personal preference, so please take a look
|
707
563
|
at LightService as well!
|