interaktor 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2bd4684e276be7100fb1e6f0bbb1cdd6a45822ddf381a88f48c033d96ddfc1fe
4
+ data.tar.gz: 407d69fd7e1391cbbf1a9542376ce1f7450e47e74cdf4e04c5d0ec041d3d5f5d
5
+ SHA512:
6
+ metadata.gz: e402a703bad7d4f84b9fc2dce342f21a20b31b39464b739b83f086af551450ad93cae8f02a20a2fd24a3cd010b17922f2e6b33a0a6c7e13e7e9a51d3001ebce4
7
+ data.tar.gz: 1e508107fc315471e56d813a145f2f710e26ff8ab3f349acd117b5d99c3b17ab4b5ab094af9e8f99f65f244fc4fb710716a86de5003be15ed3dc71ca799a6dbd
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --color
2
+ --format documentation
3
+ --order random
4
+ --require spec_helper
@@ -0,0 +1,313 @@
1
+ require:
2
+ - rubocop-rspec
3
+ - rubocop-performance
4
+
5
+ AllCops:
6
+ TargetRubyVersion: 2.5
7
+ Exclude:
8
+ - "tmp/**/*"
9
+ - "vendor/**/*"
10
+
11
+ ###########
12
+ # BUNDLER #
13
+ ###########
14
+
15
+ Bundler:
16
+ Enabled: true
17
+
18
+ Bundler/GemComment:
19
+ Enabled: true
20
+ OnlyFor: ["bitbucket", "gist", "git", "github", "source"]
21
+ IgnoredGems: []
22
+
23
+ ###########
24
+ # GEMSPEC #
25
+ ###########
26
+
27
+ Gemspec:
28
+ Enabled: true
29
+
30
+ ##########
31
+ # LAYOUT #
32
+ ##########
33
+
34
+ Layout:
35
+ Enabled: true
36
+
37
+ Layout/HeredocIndentation:
38
+ Enabled: false
39
+
40
+ Layout/ClosingHeredocIndentation:
41
+ Enabled: false
42
+
43
+ Layout/LineLength:
44
+ Enabled: true
45
+ Max: 120
46
+
47
+ Layout/RescueEnsureAlignment:
48
+ Enabled: false
49
+
50
+ Layout/CaseIndentation:
51
+ Enabled: false
52
+
53
+ # The following cops are not yet enabled by default.
54
+
55
+ Layout/EmptyLinesAroundAttributeAccessor:
56
+ Enabled: true
57
+
58
+ Layout/SpaceAroundMethodCallOperator:
59
+ Enabled: true
60
+
61
+ # Rufo already agrees with these
62
+
63
+ Layout/MultilineMethodCallIndentation:
64
+ Enabled: false
65
+
66
+ Layout/ArgumentAlignment:
67
+ Enabled: false
68
+
69
+ Layout/FirstHashElementIndentation:
70
+ Enabled: false
71
+
72
+ Layout/FirstArrayElementIndentation:
73
+ Enabled: false
74
+
75
+ Layout/IndentationWidth:
76
+ Enabled: false
77
+
78
+ Layout/ElseAlignment:
79
+ Enabled: false
80
+
81
+ Layout/EndAlignment:
82
+ Enabled: false
83
+
84
+ Layout/EmptyLinesAroundMethodBody:
85
+ Enabled: false
86
+
87
+ # Make these agree with Rufo
88
+
89
+ Layout/SpaceInsideBlockBraces:
90
+ Enabled: true
91
+ EnforcedStyleForEmptyBraces: space
92
+
93
+ ########
94
+ # LINT #
95
+ ########
96
+
97
+ Lint:
98
+ Enabled: true
99
+
100
+ # The following cops are not yet enabled by default.
101
+
102
+ Lint/DeprecatedOpenSSLConstant:
103
+ Enabled: true
104
+
105
+ Lint/MixedRegexpCaptureTypes:
106
+ Enabled: true
107
+
108
+ Lint/StructNewOverride:
109
+ Enabled: true
110
+
111
+ Lint/RaiseException:
112
+ Enabled: true
113
+
114
+ ###########
115
+ # METRICS #
116
+ ###########
117
+
118
+ Metrics:
119
+ Enabled: true
120
+
121
+ Metrics/MethodLength:
122
+ Enabled: true
123
+ Max: 50
124
+
125
+ Metrics/ClassLength:
126
+ Enabled: false
127
+
128
+ Metrics/BlockLength:
129
+ Enabled: true
130
+ Max: 25
131
+ Exclude:
132
+ - "spec/**/*"
133
+ - "test/**/*"
134
+
135
+ Metrics/CyclomaticComplexity:
136
+ Enabled: true
137
+ Max: 15
138
+
139
+ Metrics/PerceivedComplexity:
140
+ Enabled: true
141
+ Max: 15
142
+
143
+ Metrics/AbcSize:
144
+ Enabled: true
145
+ Max: 30
146
+
147
+ ##########
148
+ # NAMING #
149
+ ##########
150
+
151
+ Naming:
152
+ Enabled: true
153
+
154
+ ############
155
+ # SECURITY #
156
+ ############
157
+
158
+ Security:
159
+ Enabled: true
160
+
161
+ #########
162
+ # STYLE #
163
+ #########
164
+
165
+ Style:
166
+ Enabled: true
167
+
168
+ Style/BlockDelimiters:
169
+ Enabled: true
170
+ EnforcedStyle: braces_for_chaining
171
+ Exclude:
172
+ - "spec/**/*"
173
+ - "test/**/*"
174
+
175
+ Style/SymbolArray:
176
+ Enabled: false
177
+
178
+ Style/WordArray:
179
+ Enabled: false
180
+
181
+ Style/ClassAndModuleChildren:
182
+ Enabled: true
183
+ EnforcedStyle: compact
184
+
185
+ Style/Documentation:
186
+ Enabled: false
187
+
188
+ Style/DocumentationMethod:
189
+ Enabled: true
190
+ RequireForNonPublicMethods: true
191
+
192
+ Style/NumericPredicate:
193
+ Enabled: true
194
+ EnforcedStyle: predicate
195
+
196
+ Style/StringLiteralsInInterpolation:
197
+ Enabled: true
198
+ EnforcedStyle: double_quotes
199
+
200
+ Style/TrailingCommaInHashLiteral:
201
+ Enabled: true
202
+ EnforcedStyleForMultiline: consistent_comma
203
+
204
+ Style/TrailingCommaInArrayLiteral:
205
+ Enabled: true
206
+ EnforcedStyleForMultiline: consistent_comma
207
+
208
+ Style/TrailingCommaInArguments:
209
+ Enabled: true
210
+ EnforcedStyleForMultiline: consistent_comma
211
+
212
+ Style/StringLiterals:
213
+ Enabled: true
214
+ EnforcedStyle: double_quotes
215
+
216
+ Style/FrozenStringLiteralComment:
217
+ Enabled: false
218
+
219
+ Style/RedundantReturn:
220
+ Enabled: true
221
+
222
+ Style/TernaryParentheses:
223
+ Enabled: true
224
+
225
+ Style/RedundantParentheses:
226
+ Enabled: true
227
+
228
+ # The following cops are not yet enabled or disabled by default.
229
+
230
+ Style/AccessorGrouping:
231
+ Enabled: false
232
+
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
+ #########
264
+ # RSPEC #
265
+ #########
266
+
267
+ RSpec:
268
+ Enabled: true
269
+
270
+ RSpec/ExampleLength:
271
+ Enabled: false
272
+
273
+ RSpec/MultipleExpectations:
274
+ Enabled: false
275
+
276
+ RSpec/AnyInstance:
277
+ Enabled: false
278
+
279
+ RSpec/MessageSpies:
280
+ Enabled: false
281
+
282
+ ###############
283
+ # PERFORMANCE #
284
+ ###############
285
+
286
+ Performance:
287
+ 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
@@ -0,0 +1,25 @@
1
+ after_success:
2
+ - bundle exec codeclimate-test-reporter
3
+ before_install:
4
+ - gem update bundler rake
5
+ branches:
6
+ only:
7
+ - master
8
+ - v4
9
+ cache: bundler
10
+ language: ruby
11
+ matrix:
12
+ allow_failures:
13
+ - rvm: ruby-head
14
+ notifications:
15
+ webhooks:
16
+ on_start: always
17
+ urls:
18
+ - http://buildlight.collectiveidea.com/
19
+ rvm:
20
+ - "2.5"
21
+ - "2.6"
22
+ - "2.7"
23
+ - ruby-head
24
+ script: bundle exec rake
25
+ sudo: false
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ gem "rubocop", "~> 0.87.1"
6
+ gem "rubocop-performance", "~> 1.7.0"
7
+ gem "rubocop-rspec", "~> 1.42.0"
8
+ gem "rufo", "~> 0.12.0"
9
+ gem "solargraph", "~> 0.39.11"
10
+
11
+ group :test do
12
+ gem "codeclimate-test-reporter", "~> 1.0.9", require: false
13
+ gem "rspec", "~> 3.9.0"
14
+ end
@@ -0,0 +1,707 @@
1
+ This is a fork of Interactor. This README has not yet been updated to reflect that.
2
+
3
+ ---
4
+
5
+ # Interactor
6
+
7
+ [![Gem Version](https://img.shields.io/gem/v/interactor.svg)](http://rubygems.org/gems/interactor)
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)
12
+
13
+ ## Getting Started
14
+
15
+ Add Interactor to your Gemfile and `bundle install`.
16
+
17
+ ```ruby
18
+ gem "interactor", "~> 4.0"
19
+ ```
20
+
21
+ ## What is an Interactor?
22
+
23
+ An interactor is a simple, single-purpose object.
24
+
25
+ Interactors are used to encapsulate your application's
26
+ [business logic](http://en.wikipedia.org/wiki/Business_logic). Each interactor
27
+ represents one thing that your application *does*.
28
+
29
+ ### Context
30
+
31
+ An interactor is given a *context*. The context contains everything the
32
+ interactor needs to do its work.
33
+
34
+ When an interactor does its single purpose, it affects its given context.
35
+
36
+ #### Adding to the Context
37
+
38
+ As an interactor runs it can add information to the context.
39
+
40
+ ```ruby
41
+ context.user = user
42
+ ```
43
+
44
+ #### Failing the Context
45
+
46
+ When something goes wrong in your interactor, you can flag the context as
47
+ failed.
48
+
49
+ ```ruby
50
+ context.fail!
51
+ ```
52
+
53
+ When given a hash argument, the `fail!` method can also update the context. The
54
+ following are equivalent:
55
+
56
+ ```ruby
57
+ context.error = "Boom!"
58
+ context.fail!
59
+ ```
60
+
61
+ ```ruby
62
+ context.fail!(error: "Boom!")
63
+ ```
64
+
65
+ You can ask a context if it's a failure:
66
+
67
+ ```ruby
68
+ context.failure? # => false
69
+ context.fail!
70
+ context.failure? # => true
71
+ ```
72
+
73
+ or if it's a success.
74
+
75
+ ```ruby
76
+ context.success? # => true
77
+ context.fail!
78
+ context.success? # => false
79
+ ```
80
+
81
+ #### Dealing with Failure
82
+
83
+ `context.fail!` always throws an exception of type `Interactor::Failure`.
84
+
85
+ Normally, however, these exceptions are not seen. In the recommended usage, the controller invokes the interactor using the class method `call`, then checks the `success?` method of the context.
86
+
87
+ This works because the `call` class method swallows exceptions. When unit testing an interactor, if calling custom business logic methods directly and bypassing `call`, be aware that `fail!` will generate such exceptions.
88
+
89
+ See *Interactors in the Controller*, below, for the recommended usage of `call` and `success?`.
90
+
91
+ ### Hooks
92
+
93
+ #### Before Hooks
94
+
95
+ Sometimes an interactor needs to prepare its context before the interactor is
96
+ even run. This can be done with before hooks on the interactor.
97
+
98
+ ```ruby
99
+ before do
100
+ context.emails_sent = 0
101
+ end
102
+ ```
103
+
104
+ A symbol argument can also be given, rather than a block.
105
+
106
+ ```ruby
107
+ before :zero_emails_sent
108
+
109
+ def zero_emails_sent
110
+ context.emails_sent = 0
111
+ end
112
+ ```
113
+
114
+ #### After Hooks
115
+
116
+ Interactors can also perform teardown operations after the interactor instance
117
+ is run.
118
+
119
+ ```ruby
120
+ after do
121
+ context.user.reload
122
+ end
123
+ ```
124
+
125
+ NB: After hooks are only run on success. If the `fail!` method is called, the interactor's after hooks are not run.
126
+
127
+ #### Around Hooks
128
+
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:
133
+
134
+ ```ruby
135
+ around do |interactor|
136
+ context.start_time = Time.now
137
+ interactor.call
138
+ context.finish_time = Time.now
139
+ end
140
+ ```
141
+
142
+ With a method:
143
+
144
+ ```ruby
145
+ around :time_execution
146
+
147
+ def time_execution(interactor)
148
+ context.start_time = Time.now
149
+ interactor.call
150
+ context.finish_time = Time.now
151
+ end
152
+ ```
153
+
154
+ NB: If the `fail!` method is called, all of the interactor's around hooks cease execution, and no code after `interactor.call` will be run.
155
+
156
+ #### Hook Sequence
157
+
158
+ Before hooks are invoked in the order in which they were defined while after
159
+ hooks are invoked in the opposite order. Around hooks are invoked outside of any
160
+ defined before and after hooks. For example:
161
+
162
+ ```ruby
163
+ around do |interactor|
164
+ puts "around before 1"
165
+ interactor.call
166
+ puts "around after 1"
167
+ end
168
+
169
+ around do |interactor|
170
+ puts "around before 2"
171
+ interactor.call
172
+ puts "around after 2"
173
+ end
174
+
175
+ before do
176
+ puts "before 1"
177
+ end
178
+
179
+ before do
180
+ puts "before 2"
181
+ end
182
+
183
+ after do
184
+ puts "after 1"
185
+ end
186
+
187
+ after do
188
+ puts "after 2"
189
+ end
190
+ ```
191
+
192
+ will output:
193
+
194
+ ```
195
+ around before 1
196
+ around before 2
197
+ before 1
198
+ before 2
199
+ after 2
200
+ after 1
201
+ around after 2
202
+ around after 1
203
+ ```
204
+
205
+ #### Interactor Concerns
206
+
207
+ An interactor can define multiple before/after hooks, allowing common hooks to
208
+ be extracted into interactor concerns.
209
+
210
+ ```ruby
211
+ module InteractorTimer
212
+ extend ActiveSupport::Concern
213
+
214
+ included do
215
+ around do |interactor|
216
+ context.start_time = Time.now
217
+ interactor.call
218
+ context.finish_time = Time.now
219
+ end
220
+ end
221
+ end
222
+ ```
223
+
224
+ ### An Example Interactor
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.
375
+
376
+ ## Kinds of Interactors
377
+
378
+ There are two kinds of interactors built into the Interactor library: basic
379
+ interactors and organizers.
380
+
381
+ ### Interactors
382
+
383
+ A basic interactor is a class that includes `Interactor` and defines `call`.
384
+
385
+ ```ruby
386
+ class AuthenticateUser
387
+ include Interactor
388
+
389
+ def call
390
+ if user = User.authenticate(context.email, context.password)
391
+ context.user = user
392
+ context.token = user.secret_token
393
+ else
394
+ context.fail!(message: "authenticate_user.failure")
395
+ end
396
+ end
397
+ end
398
+ ```
399
+
400
+ Basic interactors are the building blocks. They are your application's
401
+ single-purpose units of work.
402
+
403
+ ### Organizers
404
+
405
+ An organizer is an important variation on the basic interactor. Its single
406
+ purpose is to run *other* interactors.
407
+
408
+ ```ruby
409
+ class PlaceOrder
410
+ include Interactor::Organizer
411
+
412
+ organize CreateOrder, ChargeCard, SendThankYou
413
+ end
414
+ ```
415
+
416
+ In the controller, you can run the `PlaceOrder` organizer just like you would
417
+ any other interactor:
418
+
419
+ ```ruby
420
+ class OrdersController < ApplicationController
421
+ def create
422
+ result = PlaceOrder.call(order_params: order_params)
423
+
424
+ if result.success?
425
+ redirect_to result.order
426
+ else
427
+ @order = result.order
428
+ render :new
429
+ end
430
+ end
431
+
432
+ private
433
+
434
+ def order_params
435
+ params.require(:order).permit!
436
+ end
437
+ end
438
+ ```
439
+
440
+ The organizer passes its context to the interactors that it organizes, one at a
441
+ time and in order. Each interactor may change that context before it's passed
442
+ along to the next interactor.
443
+
444
+ #### Rollback
445
+
446
+ If any one of the organized interactors fails its context, the organizer stops.
447
+ If the `ChargeCard` interactor fails, `SendThankYou` is never called.
448
+
449
+ In addition, any interactors that had already run are given the chance to undo
450
+ themselves, in reverse order. Simply define the `rollback` method on your
451
+ interactors:
452
+
453
+ ```ruby
454
+ class CreateOrder
455
+ include Interactor
456
+
457
+ def call
458
+ order = Order.create(order_params)
459
+
460
+ if order.persisted?
461
+ context.order = order
462
+ else
463
+ context.fail!
464
+ end
465
+ end
466
+
467
+ def rollback
468
+ context.order.destroy
469
+ end
470
+ end
471
+ ```
472
+
473
+ **NOTE:** The interactor that fails is *not* rolled back. Because every
474
+ interactor should have a single purpose, there should be no need to clean up
475
+ after any failed interactor.
476
+
477
+ ## Testing Interactors
478
+
479
+ When written correctly, an interactor is easy to test because it only *does* one
480
+ thing. Take the following interactor:
481
+
482
+ ```ruby
483
+ class AuthenticateUser
484
+ include Interactor
485
+
486
+ def call
487
+ if user = User.authenticate(context.email, context.password)
488
+ context.user = user
489
+ context.token = user.secret_token
490
+ else
491
+ context.fail!(message: "authenticate_user.failure")
492
+ end
493
+ end
494
+ end
495
+ ```
496
+
497
+ You can test just this interactor's single purpose and how it affects the
498
+ context.
499
+
500
+ ```ruby
501
+ describe AuthenticateUser do
502
+ subject(:context) { AuthenticateUser.call(email: "john@example.com", password: "secret") }
503
+
504
+ describe ".call" do
505
+ context "when given valid credentials" do
506
+ let(:user) { double(:user, secret_token: "token") }
507
+
508
+ before do
509
+ allow(User).to receive(:authenticate).with("john@example.com", "secret").and_return(user)
510
+ end
511
+
512
+ it "succeeds" do
513
+ expect(context).to be_a_success
514
+ end
515
+
516
+ it "provides the user" do
517
+ expect(context.user).to eq(user)
518
+ end
519
+
520
+ it "provides the user's secret token" do
521
+ expect(context.token).to eq("token")
522
+ end
523
+ end
524
+
525
+ context "when given invalid credentials" do
526
+ before do
527
+ allow(User).to receive(:authenticate).with("john@example.com", "secret").and_return(nil)
528
+ end
529
+
530
+ it "fails" do
531
+ expect(context).to be_a_failure
532
+ end
533
+
534
+ it "provides a failure message" do
535
+ expect(context.message).to be_present
536
+ end
537
+ end
538
+ end
539
+ end
540
+ ```
541
+
542
+ [We](http://collectiveidea.com) use RSpec but the same approach applies to any
543
+ testing framework.
544
+
545
+ ### Isolation
546
+
547
+ You may notice that we stub `User.authenticate` in our test rather than creating
548
+ users in the database. That's because our purpose in
549
+ `spec/interactors/authenticate_user_spec.rb` is to test just the
550
+ `AuthenticateUser` interactor. The `User.authenticate` method is put through its
551
+ own paces in `spec/models/user_spec.rb`.
552
+
553
+ 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 interactor and
555
+ which to the model. The `User.authenticate` method is a good, clear line.
556
+ Imagine the interactor otherwise:
557
+
558
+ ```ruby
559
+ class AuthenticateUser
560
+ include Interactor
561
+
562
+ def call
563
+ user = User.where(email: context.email).first
564
+
565
+ # Yuck!
566
+ if user && BCrypt::Password.new(user.password_digest) == context.password
567
+ context.user = user
568
+ else
569
+ context.fail!(message: "authenticate_user.failure")
570
+ end
571
+ end
572
+ end
573
+ ```
574
+
575
+ It would be very difficult to test this interactor in isolation and even if you
576
+ did, as soon as you change your ORM or your encryption algorithm (both model
577
+ concerns), your interactors (business concerns) break.
578
+
579
+ *Draw clear lines.*
580
+
581
+ ### Integration
582
+
583
+ While it's important to test your interactors in isolation, it's just as
584
+ important to write good integration or acceptance tests.
585
+
586
+ One of the pitfalls of testing in isolation is that when you stub a method, you
587
+ could be hiding the fact that the method is broken, has changed or doesn't even
588
+ exist.
589
+
590
+ When you write full-stack tests that tie all of the pieces together, you can be
591
+ sure that your application's individual pieces are working together as expected.
592
+ That becomes even more important when you add a new layer to your code like
593
+ interactors.
594
+
595
+ **TIP:** If you track your test coverage, try for 100% coverage *before*
596
+ integrations tests. Then keep writing integration tests until you sleep well at
597
+ night.
598
+
599
+ ### Controllers
600
+
601
+ One of the advantages of using interactors is how much they simplify controllers
602
+ and their tests. Because you're testing your interactors thoroughly in isolation
603
+ as well as in integration tests (right?), you can remove your business logic
604
+ from your controller tests.
605
+
606
+ ```ruby
607
+ class SessionsController < ApplicationController
608
+ def create
609
+ result = AuthenticateUser.call(session_params)
610
+
611
+ if result.success?
612
+ session[:user_token] = result.token
613
+ redirect_to result.user
614
+ else
615
+ flash.now[:message] = t(result.message)
616
+ render :new
617
+ end
618
+ end
619
+
620
+ private
621
+
622
+ def session_params
623
+ params.require(:session).permit(:email, :password)
624
+ end
625
+ end
626
+ ```
627
+
628
+ ```ruby
629
+ describe SessionsController do
630
+ describe "#create" do
631
+ before do
632
+ expect(AuthenticateUser).to receive(:call).once.with(email: "john@doe.com", password: "secret").and_return(context)
633
+ end
634
+
635
+ context "when successful" do
636
+ let(:user) { double(:user, id: 1) }
637
+ let(:context) { double(:context, success?: true, user: user, token: "token") }
638
+
639
+ it "saves the user's secret token in the session" do
640
+ expect {
641
+ post :create, session: { email: "john@doe.com", password: "secret" }
642
+ }.to change {
643
+ session[:user_token]
644
+ }.from(nil).to("token")
645
+ end
646
+
647
+ it "redirects to the homepage" do
648
+ response = post :create, session: { email: "john@doe.com", password: "secret" }
649
+
650
+ expect(response).to redirect_to(user_path(user))
651
+ end
652
+ end
653
+
654
+ context "when unsuccessful" do
655
+ let(:context) { double(:context, success?: false, message: "message") }
656
+
657
+ it "sets a flash message" do
658
+ expect {
659
+ post :create, session: { email: "john@doe.com", password: "secret" }
660
+ }.to change {
661
+ flash[:message]
662
+ }.from(nil).to(I18n.translate("message"))
663
+ end
664
+
665
+ it "renders the login form" do
666
+ response = post :create, session: { email: "john@doe.com", password: "secret" }
667
+
668
+ expect(response).to render_template(:new)
669
+ end
670
+ end
671
+ end
672
+ end
673
+ ```
674
+
675
+ This controller test will have to change very little during the life of the
676
+ application because all of the magic happens in the interactor.
677
+
678
+ ### Rails
679
+
680
+ [We](http://collectiveidea.com) love Rails, and we use Interactor with Rails. We
681
+ put our interactors in `app/interactors` and we name them as verbs:
682
+
683
+ * `AddProductToCart`
684
+ * `AuthenticateUser`
685
+ * `PlaceOrder`
686
+ * `RegisterUser`
687
+ * `RemoveProductFromCart`
688
+
689
+ See: [Interactor Rails](https://github.com/collectiveidea/interactor-rails)
690
+
691
+ ## Contributions
692
+
693
+ Interactor is open source and contributions from the community are encouraged!
694
+ No contribution is too small.
695
+
696
+ See Interactor's
697
+ [contribution guidelines](CONTRIBUTING.md) for more information.
698
+
699
+ ## Thank You
700
+
701
+ A very special thank you to [Attila Domokos](https://github.com/adomokos) for
702
+ his fantastic work on [LightService](https://github.com/adomokos/light-service).
703
+ Interactor is inspired heavily by the concepts put to code by Attila.
704
+
705
+ Interactor was born from a desire for a slightly simplified interface. We
706
+ understand that this is a matter of personal preference, so please take a look
707
+ at LightService as well!