interaktor 0.1.3

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.
@@ -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!