interactor 2.1.1 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e0ff87a1f072528adc491c5082fa8e350bb43e1a
4
- data.tar.gz: 9cb58e0d245e18bcd4f7bd690a022a99ac490d21
3
+ metadata.gz: f90c2385a676f054e225898505feb66133199adb
4
+ data.tar.gz: f2f66a7c046fe90cd9cc8ee3c54797654f4dd5fd
5
5
  SHA512:
6
- metadata.gz: dcc8d7986ae90cf681d35af66c5190ce4066ebfc7820539ec993c952daf7aa3ba8f3090f4f35ef77ffd8440593579f516e54298d1412d71127a2b6ed4d8bc007
7
- data.tar.gz: 9e06eacaeb9dd63f34e9b5684a08bf1738ea659cf2cf9d58b490239f70cecfdcf902202a99684fb702bc5a0991aa70a7eb0499b021d5bce89482d9700cf2bf3d
6
+ metadata.gz: 224044e19387797e7e2cfc3b930cceb348078eee3b4ff6b2c7f44e54fefb7200ed490468131a31d813c2e0534c517311057aa048e70f90d04b1269cdcbbb5977
7
+ data.tar.gz: 92e8e877cc317445d33b4c7b32b5eae7c88d156a206f2e313d70c6abe01151819eb2b151a580f627ec9b0deeb862d24fae4cd5fd8c3b189018064be91dac3305
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --order random
3
+ --require spec_helper
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-new
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.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
@@ -3,7 +3,6 @@ source "https://rubygems.org"
3
3
  gemspec
4
4
 
5
5
  group :test do
6
- gem "activesupport", "~> 4.0", require: false
7
- gem "coveralls", "~> 0.6.9", require: false
8
- gem "rspec", "~> 2.14"
6
+ gem "codeclimate-test-reporter", require: false
7
+ gem "rspec", "~> 3.1"
9
8
  end
data/README.md CHANGED
@@ -1,297 +1,592 @@
1
1
  # Interactor
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/interactor.png)](http://badge.fury.io/rb/interactor)
4
- [![Build Status](https://travis-ci.org/collectiveidea/interactor.png?branch=master)](https://travis-ci.org/collectiveidea/interactor)
5
- [![Code Climate](https://codeclimate.com/github/collectiveidea/interactor.png)](https://codeclimate.com/github/collectiveidea/interactor)
6
- [![Coverage Status](https://coveralls.io/repos/collectiveidea/interactor/badge.png?branch=master)](https://coveralls.io/r/collectiveidea/interactor?branch=master)
7
- [![Dependency Status](https://gemnasium.com/collectiveidea/interactor.png)](https://gemnasium.com/collectiveidea/interactor)
3
+ [![Gem Version](https://img.shields.io/gem/v/interactor.svg?style=flat-square)](http://rubygems.org/gems/interactor)
4
+ [![Build Status](https://img.shields.io/travis/collectiveidea/interactor/master.svg?style=flat-square)](https://travis-ci.org/collectiveidea/interactor)
5
+ [![Code Climate](https://img.shields.io/codeclimate/github/collectiveidea/interactor.svg?style=flat-square)](https://codeclimate.com/github/collectiveidea/interactor)
6
+ [![Test Coverage](http://img.shields.io/codeclimate/coverage/github/collectiveidea/interactor.svg?style=flat-square)](https://codeclimate.com/github/collectiveidea/interactor)
7
+ [![Dependency Status](https://img.shields.io/gemnasium/collectiveidea/interactor.svg?style=flat-square)](https://gemnasium.com/collectiveidea/interactor)
8
8
 
9
- Interactor provides a common interface for performing complex interactions in a single request.
9
+ ## Getting Started
10
10
 
11
- ## Problems
11
+ Add Interactor to your Gemfile and `bundle install`.
12
12
 
13
- If you're like us at [Collective Idea](http://collectiveidea.com), you've noticed that there seems to be a layer missing between the Controller and the Model.
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
- ### Fat Models
27
+ An interactor is given a *context*. The context contains everything the
28
+ interactor needs to do its work.
16
29
 
17
- We've been told time after time to keep our controllers "skinny" but this usually comes at the expense of our models becoming pretty flabby. Oftentimes, much of the excess weight doesn't belong on the model. We're sending emails, making calls to external services and more, all from the model. It's not right.
30
+ When an interactor does its single purpose, it affects its given context.
18
31
 
19
- *The purpose of the model layer is to be a gatekeeper to the application's data.*
32
+ #### Adding to the Context
20
33
 
21
- Consider the following model:
34
+ As an interactor runs it can add information to the context.
22
35
 
23
36
  ```ruby
24
- class User < ActiveRecord::Base
25
- validates :name, :email, presence: true
37
+ context.user = user
38
+ ```
26
39
 
27
- after_create :send_welcome_email
40
+ #### Failing the Context
28
41
 
29
- private
42
+ When something goes wrong in your interactor, you can flag the context as
43
+ failed.
30
44
 
31
- def send_welcome_email
32
- Notifier.welcome(self).deliver
33
- end
34
- end
45
+ ```ruby
46
+ context.fail!
35
47
  ```
36
48
 
37
- We see this pattern all too often. The problem is that *any* time we want to add a user to the application, the welcome email will be sent. That includes creating users in development and in your tests. Is that really what we want?
49
+ When given a hash argument, the `fail!` method can also update the context. The
50
+ following are equivalent:
38
51
 
39
- Sending a welcome email is business logic. It has nothing to do with the integrity of the application's data, so it belongs somewhere else.
52
+ ```ruby
53
+ context.error = "Boom!"
54
+ context.fail!
55
+ ```
56
+
57
+ ```ruby
58
+ context.fail!(error: "Boom!")
59
+ ```
40
60
 
41
- ### Fat Controllers
61
+ You can ask a context if it's a failure:
42
62
 
43
- Usually, the alternative to fat models is fat controllers.
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
- While business logic may be more at home in a controller, controllers are typically intermingled with the concept of a request. HTTP requests are complex and that fact makes testing your business logic more difficult than it should be.
77
+ ### Hooks
46
78
 
47
- *Your business logic should be unaware of your delivery mechanism.*
79
+ #### Before Hooks
48
80
 
49
- So what if we encapsulated all of our business logic in dead-simple Ruby. One glance at a directory like `app/interactors` could go a long way in answering the question, "What does this app do?".
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
- ▸ app/
53
- interactors/
54
- add_product_to_cart.rb
55
- authenticate_user.rb
56
- place_order.rb
57
- register_user.rb
58
- remove_product_from_cart.rb
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
- ## Interactors
100
+ #### After Hooks
62
101
 
63
- An interactor is an object with a simple interface and a singular purpose.
102
+ Interactors can also perform teardown operations after the interactor instance
103
+ is run.
64
104
 
65
- Interactors are given a context from the controller and do one thing: perform. When an interactor performs, it may act on models, send emails, make calls to external services and more. The interactor may also modify the given context.
105
+ ```ruby
106
+ after do
107
+ context.user.reload
108
+ end
109
+ ```
66
110
 
67
- A simple interactor may look like:
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 perform
74
- if user = User.authenticate(context[:email], context[:password])
75
- context[:user] = user
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
- There are a few important things to note about this interactor:
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
- 1. It's simple.
86
- 2. It's just Ruby.
87
- 3. It's easily testable.
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
- It's feasible that a collection of small interactors such as these could encapsulate *all* of your business logic.
154
+ private
90
155
 
91
- Interactors free up your controllers to simply accept requests and build responses. They free up your models to acts as the gatekeepers to your data.
156
+ def session_params
157
+ params.require(:session).permit(:email, :password)
158
+ end
159
+ end
160
+ ```
92
161
 
93
- ### Pre-perform operation
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 AuthenticateUser
99
- include Interactor
100
-
101
- def setup
102
- context.fail! unless context[:email].present? && context[:password].present?
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
- def perform
106
- if user = User.authenticate(context[:email], context[:password])
107
- context[:user] = user
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
- context.fail!
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
- ## Organizers
217
+ For such a simple use case, using an interactor can actually require *more*
218
+ code. So why use an interactor?
116
219
 
117
- An organizer is just an interactor that's in charge of other interactors. When an organizer is asked to perform, it just asks its interactors to perform, in order.
220
+ ### Clarity
118
221
 
119
- Organizers are great for complex interactions. For example, placing an order might involve:
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
- * checking inventory
122
- * calculating tax
123
- * charging a credit card
124
- * writing an order to the database
125
- * sending email notifications
126
- * scheduling a follow-up email
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
- Each of these actions can (and should) have its own interactor and one organizer can perform them all. That organizer may look like:
273
+ A basic interactor is a class that includes `Interactor` and defines `call`.
129
274
 
130
275
  ```ruby
131
- class PlaceOrder
132
- include Interactor::Organizer
276
+ class AuthenticateUser
277
+ include Interactor
133
278
 
134
- organize [
135
- CheckInventory,
136
- CalculateTax,
137
- ChargeCard,
138
- CreateOrder,
139
- DeliverThankYou,
140
- DeliverOrderNotification,
141
- ScheduleFollowUp
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
- Breaking your interactors into bite-sized pieces also gives you the benefit of reusability. In our example above, there may be several scenarios where you may want to check inventory. Encapsulating that logic in one interactor enables you to reuse that interactor, reducing duplication.
290
+ Basic interactors are the building blocks. They are your application's
291
+ single-purpose units of work.
147
292
 
148
- ## Examples
293
+ ### Organizers
149
294
 
150
- ### Interactors
295
+ An organizer is an important variation on the basic interactor. Its single
296
+ purpose is to run *other* interactors.
151
297
 
152
- Take the simple case of authenticating a user.
298
+ ```ruby
299
+ class PlaceOrder
300
+ include Interactor::Organizer
301
+
302
+ organize CreateOrder, ChargeCard, SendThankYou
303
+ end
304
+ ```
153
305
 
154
- Using an interactor, the controller stays very clean, making it very readable and easily testable.
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 SessionsController < ApplicationController
310
+ class OrdersController < ApplicationController
158
311
  def create
159
- result = AuthenticateUser.perform(session_params)
312
+ result = PlaceOrder.call(order_params: order_params)
160
313
 
161
314
  if result.success?
162
- redirect_to result.user
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 session_params
171
- params.require(:session).permit(:email, :password)
324
+ def order_params
325
+ params.require(:order).permit!
172
326
  end
173
327
  end
174
328
  ```
175
329
 
176
- The `result` above is an instance of the `AuthenticateUser` interactor that has been performed. The magic happens in the interactor, after receiving a *context* from the controller. A context is just a glorified hash that the interactor manipulates.
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 AuthenticateUser
344
+ class CreateOrder
180
345
  include Interactor
181
346
 
182
- def perform
183
- if user = User.authenticate(context[:email], context[:password])
184
- context[:user] = user
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 also has convenience methods for dealing with its context. Anything added to the context is available via getter method on the interactor instance. The following is equivalent:
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 perform
199
- if user = User.authenticate(email, password)
200
- context[:user] = user
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
- An interactor can fail with an optional hash that is merged into the context.
387
+ You can test just this interactor's single purpose and how it affects the
388
+ context.
209
389
 
210
390
  ```ruby
211
- fail!(message: "Uh oh!")
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
- Interactors are successful until explicitly failed. Instances respond to `success?` and `failure?`.
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
- ### Organizers
432
+ it "fails" do
433
+ interactor.call
217
434
 
218
- In the example above, one could argue that the interactor is simple enough that it could be excluded altogether. While that's probably true, in [our](http://collectiveidea.com) experience, these interactions don't stay simple for long. When they get more complex, the `AuthenticateUser` interactor can be converted to an organizer.
435
+ expect(context).to be_a_failure
436
+ end
219
437
 
220
- ```ruby
221
- class AuthenticateUser
222
- include Interactor::Organizer
223
-
224
- organize FindUserByEmailAndPassword, SendWelcomeEmail
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
- And your controller doesn't change a bit!
450
+ [We](http://collectiveidea.com) use RSpec but the same approach applies to any
451
+ testing framework.
229
452
 
230
- The `AuthenticateUser` organizer receives its context from the controller and passes it to the interactors, which each manipulate it in turn.
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 FindUserByEmailAndPassword
467
+ class AuthenticateUser
234
468
  include Interactor
235
469
 
236
- def perform
237
- if user = User.authenticate(email, password)
238
- context[:user] = user
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 SendWelcomeEmail
248
- include Interactor
515
+ class SessionsController < ApplicationController
516
+ def create
517
+ result = AuthenticateUser.call(session_params)
249
518
 
250
- def perform
251
- if user.newly_created?
252
- Notifier.welcome(user).deliver
253
- context[:new_user] = true
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
- #### Inception
528
+ private
260
529
 
261
- Because interactors and organizers adhere to the same interface, it's trivial for an organizer to organize… organizers!
530
+ def session_params
531
+ params.require(:session).permit(:email, :password)
532
+ end
533
+ end
534
+ ```
262
535
 
263
- #### Rollback
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
- If an organizer has three interactors and the second one fails, the third one is never called.
543
+ context "when successful" do
544
+ let(:user) { double(:user) }
545
+ let(:context) { double(:context, success?: true, user: user, token: "token") }
266
546
 
267
- In addition to halting the chain, an organizer will also *rollback* through the interactors that it has successfully performed so that each interactor has the opportunity to undo itself. Just define a `rollback` method. It has all the same access to the context as `perform` does.
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
- Note that the the failed interactor itself will not be rolled back. Interactors are expected to be single-purpose, so there should be nothing to undo if the interactor fails.
555
+ it "redirects to the homepage" do
556
+ response = post :create, session: { email: "john@doe.com", password: "secret" }
270
557
 
271
- ## Conventions
558
+ expect(response).to redirect_to(root_path)
559
+ end
560
+ end
272
561
 
273
- ### Good Practice
562
+ context "when unsuccessful" do
563
+ let(:context) { double(:context, success?: false, message: "message") }
274
564
 
275
- To allow rollbacks to work without fuss in organizers, interactors should only *add* to the context. They should not transform any values already in the context. For example, the following is a bad idea:
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
- ```ruby
278
- class FindUser
279
- include Interactor
573
+ it "renders the login form" do
574
+ response = post :create, session: { email: "john@doe.com", password: "secret" }
280
575
 
281
- def perform
282
- context[:user] = User.find(context[:user])
283
- # Now, context[:user] contains a User object.
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
- If an organizer rolls back, any interactor before `FindUser` will now see a `User` object during the rollback when they were probably expecting a simple ID. This could cause problems.
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 put our interactors in `app/interactors` and we name them as verbs:
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! No contribution is too small. Please consider:
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
- 1. Ask us! We'd love to hear what you're up to.
317
- 2. Fork the project.
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
- ## Thanks
607
+ ## Thank You
322
608
 
323
- A very special thank you to [Attila Domokos](https://github.com/adomokos) for his fantastic work on [LightService](https://github.com/adomokos/light-service). Interactor is inspired heavily by the concepts put to code by Attila.
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 different (in our minds, simplified) interface. We understand that this is a matter of personal preference, so please take a look at LightService as well!
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!