interactor 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6832397e4ab11a93ded8ded31c4e47519be8fe34
4
+ data.tar.gz: 3ffdfed6582a46b97745356133e5b34a2c3d3b0c
5
+ SHA512:
6
+ metadata.gz: e744650c5033bab652b44d122fc7a1b785d7ab8d22e9afc654a2b1f2e1071f8fdf1846e7c3c68e3163cdd2892ea1ad8d98c37ef9bf8b75c8351b21f13d04215f
7
+ data.tar.gz: 81ba3295b6dc2cf3991919708b46cb7e83d1f964799f9b6262e46bfbc78733c337f823bc4401c3fec6822154ddf7ccc0abdc63167cc15865ca5f01453f5381df
data/.gitignore ADDED
@@ -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/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ branches:
2
+ only:
3
+ - master
4
+ language: ruby
5
+ rvm:
6
+ - 1.9.3
7
+ - 2.0.0
8
+ script: rspec
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ group :test do
6
+ gem "coveralls", "~> 0.6.7", require: false
7
+ gem "rspec", "~> 2.14"
8
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Collective Idea
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,280 @@
1
+ # Interactor
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)
8
+
9
+ Interactor provides a common interface for performing complex interactions in a single request.
10
+
11
+ ## Problems
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.
14
+
15
+ ### Fat Models
16
+
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.
18
+
19
+ *The purpose of the model layer is to be a gatekeeper to the application's data.*
20
+
21
+ Consider the following model:
22
+
23
+ ```ruby
24
+ class User < ActiveRecord::Base
25
+ validates :name, :email, presence: true
26
+
27
+ after_create :send_welcome_email
28
+
29
+ private
30
+
31
+ def send_welcome_email
32
+ Notifier.welcome(self).deliver
33
+ end
34
+ end
35
+ ```
36
+
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?
38
+
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.
40
+
41
+ ### Fat Controllers
42
+
43
+ Usually, the alternative to fat models is fat controllers.
44
+
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.
46
+
47
+ *Your business logic should be unaware of your delivery mechanism.*
48
+
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?".
50
+
51
+ ```
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
59
+ ```
60
+
61
+ ## Interactors
62
+
63
+ An interactor is an object with a simple interface and a singular purpose.
64
+
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.
66
+
67
+ A simple interactor may look like:
68
+
69
+ ```ruby
70
+ class AuthenticateUser
71
+ include Interactor
72
+
73
+ def perform
74
+ if user = User.authenticate(context[:email], context[:password])
75
+ context[:user] = user
76
+ else
77
+ context.fail!
78
+ end
79
+ end
80
+ end
81
+ ```
82
+
83
+ There are a few important things to note about this interactor:
84
+
85
+ 1. It's simple.
86
+ 2. It's just Ruby.
87
+ 3. It's easily testable.
88
+
89
+ It's feasible that a collection of small interactors such as these could encapsulate *all* of your business logic.
90
+
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.
92
+
93
+ ## Organizers
94
+
95
+ 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.
96
+
97
+ Organizers are great for complex interactions. For example, placing an order might involve:
98
+
99
+ * checking inventory
100
+ * calculating tax
101
+ * charging a credit card
102
+ * writing an order to the database
103
+ * sending email notifications
104
+ * scheduling a follow-up email
105
+
106
+ Each of these actions can (and should) have its own interactor and one organizer can perform them all. That organizer may look like:
107
+
108
+ ```ruby
109
+ class PlaceOrder
110
+ include Interactor::Organizer
111
+
112
+ organize [
113
+ CheckInventory,
114
+ CalculateTax,
115
+ ChargeCard,
116
+ CreateOrder,
117
+ DeliverThankYou,
118
+ DeliverOrderNotification,
119
+ ScheduleFollowUp
120
+ ]
121
+ end
122
+ ```
123
+
124
+ Breaking your interactors into bite-sized pieces also gives you the benefit or 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.
125
+
126
+ ## Examples
127
+
128
+ ### Interactors
129
+
130
+ Take the simple case of authenticating a user.
131
+
132
+ Using an interactor, the controller stays very clean, making it very readable and easily testable.
133
+
134
+ ```ruby
135
+ class SessionsController < ApplicationController
136
+ def create
137
+ result = AuthenticateUser.perform(session_params)
138
+
139
+ if result.success?
140
+ redirect_to result.user
141
+ else
142
+ render :new
143
+ end
144
+ end
145
+
146
+ private
147
+
148
+ def session_params
149
+ params.require(:session).permit(:email, :password)
150
+ end
151
+ end
152
+ ```
153
+
154
+ 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.
155
+
156
+ ```ruby
157
+ class AuthenticateUser
158
+ include Interactor
159
+
160
+ def perform
161
+ if user = User.authenticate(context[:email], context[:password])
162
+ context[:user] = user
163
+ else
164
+ context.fail!
165
+ end
166
+ end
167
+ end
168
+ ```
169
+
170
+ 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:
171
+
172
+ ```ruby
173
+ class AuthenticateUser
174
+ include Interactor
175
+
176
+ def perform
177
+ if user = User.authenticate(email, password)
178
+ context[:user] = user
179
+ else
180
+ fail!
181
+ end
182
+ end
183
+ end
184
+ ```
185
+
186
+ An interactor can fail with an optional hash that is merged into the context.
187
+
188
+ ```ruby
189
+ fail!(message: "Uh oh!")
190
+ ```
191
+
192
+ Interactors are successful until explicitly failed. Instances respond to `success?` and `failure?`.
193
+
194
+ ### Organizers
195
+
196
+ 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.
197
+
198
+ ```ruby
199
+ class AuthenticateUser
200
+ include Interactor::Organizer
201
+
202
+ organize FindUserByEmailAndPassword, SendWelcomeEmail
203
+ end
204
+ ```
205
+
206
+ And your controller doesn't change a bit!
207
+
208
+ The `AuthenticateUser` organizer receives its context from the controller and passes it to the interactors, which each manipulate it in turn.
209
+
210
+ ```ruby
211
+ class FindUserByEmailAndPassword
212
+ include Interactor
213
+
214
+ def perform
215
+ if user = User.authenticate(email, password)
216
+ context[:user] = user
217
+ else
218
+ fail!
219
+ end
220
+ end
221
+ end
222
+ ```
223
+
224
+ ```ruby
225
+ class SendWelcomeEmail
226
+ include Interactor
227
+
228
+ def perform
229
+ if user.newly_created?
230
+ Notifier.welcome(user).deliver
231
+ context[:new_user] = true
232
+ end
233
+ end
234
+ end
235
+ ```
236
+
237
+ #### Inception
238
+
239
+ Because interactors and organizers adhere to the same interface, it's trivial for an organizer to organize… organizers!
240
+
241
+ #### Rollback
242
+
243
+ If an organizer has three interactors and the second one fails, the third one is never called.
244
+
245
+ In addition to halting the chain, an organizer will also *rollback* through the interactors that it has 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.
246
+
247
+ ## Conventions
248
+
249
+ We love Rails, and we use Interactor with Rails. We put our interactors in `app/interactors` and we name them as verbs:
250
+
251
+ * `AddProductToCart`
252
+ * `AuthenticateUser`
253
+ * `PlaceOrder`
254
+ * `RegisterUser`
255
+ * `RemoveProductFromCart`
256
+
257
+ See [Interactor Rails](https://github.com/collectiveidea/interactor-rails)
258
+
259
+ ## Contributions
260
+
261
+ Interactor is open source and contributions from the community are encouraged! No contribution is too small. Please consider:
262
+
263
+ * adding an awesome feature
264
+ * fixing a terrible bug
265
+ * updating documentation
266
+ * fixing a not-so-bad bug
267
+ * fixing typos
268
+
269
+ For the best chance of having your changes merged, please:
270
+
271
+ 1. Ask us! We'd love to hear what you're up to.
272
+ 2. Fork the project.
273
+ 3. Commit your changes and tests (if applicable (they're applicable)).
274
+ 4. Submit a pull request with a thorough explanation and at least one animated GIF.
275
+
276
+ ## Thanks
277
+
278
+ 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.
279
+
280
+ 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!
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,20 @@
1
+ # encoding: utf-8
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "interactor"
5
+ spec.version = "1.0.0"
6
+
7
+ spec.author = "Collective Idea"
8
+ spec.email = "info@collectiveidea.com"
9
+ spec.description = "Interactor provides a common interface for performing complex interactions in a single request."
10
+ spec.summary = "Simple interactor implementation"
11
+ spec.homepage = "https://github.com/collectiveidea/interactor"
12
+ spec.license = "MIT"
13
+
14
+ spec.files = `git ls-files`.split($/)
15
+ spec.test_files = spec.files.grep(/^spec/)
16
+ spec.require_paths = ["lib"]
17
+
18
+ spec.add_development_dependency "bundler", "~> 1.3"
19
+ spec.add_development_dependency "rake", "~> 10.1"
20
+ end
data/lib/interactor.rb ADDED
@@ -0,0 +1,59 @@
1
+ require "interactor/context"
2
+ require "interactor/organizer"
3
+
4
+ module Interactor
5
+ def self.included(base)
6
+ base.class_eval do
7
+ extend ClassMethods
8
+ include InstanceMethods
9
+
10
+ attr_reader :context
11
+ end
12
+ end
13
+
14
+ module ClassMethods
15
+ def perform(context = {})
16
+ new(context).tap(&:perform)
17
+ end
18
+
19
+ def rollback(context = {})
20
+ new(context).tap(&:rollback)
21
+ end
22
+ end
23
+
24
+ module InstanceMethods
25
+ def initialize(context = {})
26
+ @context = Context.build(context)
27
+ setup
28
+ end
29
+
30
+ def setup
31
+ end
32
+
33
+ def perform
34
+ end
35
+
36
+ def rollback
37
+ end
38
+
39
+ def success?
40
+ context.success?
41
+ end
42
+
43
+ def failure?
44
+ context.failure?
45
+ end
46
+
47
+ def fail!(*args)
48
+ context.fail!(*args)
49
+ end
50
+
51
+ def method_missing(method, *)
52
+ context.fetch(method) { super }
53
+ end
54
+
55
+ def respond_to_missing?(method, *)
56
+ context.key?(method) || super
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,20 @@
1
+ module Interactor
2
+ class Context < ::Hash
3
+ def self.build(context = {})
4
+ self === context ? context : new.replace(context)
5
+ end
6
+
7
+ def success?
8
+ !failure?
9
+ end
10
+
11
+ def failure?
12
+ @failure || false
13
+ end
14
+
15
+ def fail!(context = {})
16
+ update(context)
17
+ @failure = true
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,46 @@
1
+ module Interactor
2
+ module Organizer
3
+ def self.included(base)
4
+ base.class_eval do
5
+ include Interactor
6
+
7
+ extend ClassMethods
8
+ include InstanceMethods
9
+ end
10
+ end
11
+
12
+ module ClassMethods
13
+ def interactors
14
+ @interactors ||= []
15
+ end
16
+
17
+ def organize(*interactors)
18
+ @interactors = interactors.flatten
19
+ end
20
+ end
21
+
22
+ module InstanceMethods
23
+ def interactors
24
+ self.class.interactors
25
+ end
26
+
27
+ def perform
28
+ interactors.each do |interactor|
29
+ performed << interactor
30
+ interactor.perform(context)
31
+ rollback && break if context.failure?
32
+ end
33
+ end
34
+
35
+ def rollback
36
+ performed.reverse_each do |interactor|
37
+ interactor.rollback(context)
38
+ end
39
+ end
40
+
41
+ def performed
42
+ @performed ||= []
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,95 @@
1
+ require "spec_helper"
2
+
3
+ module Interactor
4
+ describe Context do
5
+ describe ".build" do
6
+ it "converts the given hash to a context" do
7
+ context = Context.build(foo: "bar")
8
+
9
+ expect(context).to be_a(Context)
10
+ expect(context).to eq(foo: "bar")
11
+ end
12
+
13
+ it "builds an empty context if no hash is given" do
14
+ context = Context.build
15
+
16
+ expect(context).to be_a(Context)
17
+ expect(context).to eq({})
18
+ end
19
+
20
+ it "preserves an already built context" do
21
+ context1 = Context.build(foo: "bar")
22
+ context2 = Context.build(context1)
23
+
24
+ expect(context2).to be_a(Context)
25
+ expect {
26
+ context2[:foo] = "baz"
27
+ }.to change {
28
+ context1[:foo]
29
+ }.from("bar").to("baz")
30
+ end
31
+ end
32
+
33
+ describe "#success?" do
34
+ let(:context) { Context.build }
35
+
36
+ it "is true by default" do
37
+ expect(context.success?).to eq(true)
38
+ end
39
+ end
40
+
41
+ describe "#failure?" do
42
+ let(:context) { Context.build }
43
+
44
+ it "is false by default" do
45
+ expect(context.failure?).to eq(false)
46
+ end
47
+ end
48
+
49
+ describe "#fail!" do
50
+ let(:context) { Context.build(foo: "bar") }
51
+
52
+ it "sets success to false" do
53
+ expect {
54
+ context.fail!
55
+ }.to change {
56
+ context.success?
57
+ }.from(true).to(false)
58
+ end
59
+
60
+ it "sets failure to true" do
61
+ expect {
62
+ context.fail!
63
+ }.to change {
64
+ context.failure?
65
+ }.from(false).to(true)
66
+ end
67
+
68
+ it "preserves failure" do
69
+ context.fail!
70
+
71
+ expect {
72
+ context.fail!
73
+ }.not_to change {
74
+ context.failure?
75
+ }
76
+ end
77
+
78
+ it "preserves the context" do
79
+ expect {
80
+ context.fail!
81
+ }.not_to change {
82
+ context[:foo]
83
+ }
84
+ end
85
+
86
+ it "updates the context" do
87
+ expect {
88
+ context.fail!(foo: "baz")
89
+ }.to change {
90
+ context[:foo]
91
+ }.from("bar").to("baz")
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,132 @@
1
+ require "spec_helper"
2
+
3
+ module Interactor
4
+ describe Organizer do
5
+ include_examples :lint
6
+
7
+ let(:interactor) { Class.new.send(:include, Organizer) }
8
+
9
+ describe ".interactors" do
10
+ it "is empty by default" do
11
+ expect(interactor.interactors).to eq([])
12
+ end
13
+ end
14
+
15
+ describe ".organize" do
16
+ let(:interactor2) { double(:interactor2) }
17
+ let(:interactor3) { double(:interactor3) }
18
+
19
+ it "sets interactors given class arguments" do
20
+ expect {
21
+ interactor.organize(interactor2, interactor3)
22
+ }.to change {
23
+ interactor.interactors
24
+ }.from([]).to([interactor2, interactor3])
25
+ end
26
+
27
+ it "sets interactors given an array of classes" do
28
+ expect {
29
+ interactor.organize([interactor2, interactor3])
30
+ }.to change {
31
+ interactor.interactors
32
+ }.from([]).to([interactor2, interactor3])
33
+ end
34
+ end
35
+
36
+ describe "#interactors" do
37
+ let(:interactors) { double(:interactors) }
38
+ let(:instance) { interactor.new }
39
+
40
+ before do
41
+ interactor.stub(:interactors) { interactors }
42
+ end
43
+
44
+ it "defers to the class" do
45
+ expect(instance.interactors).to eq(interactors)
46
+ end
47
+ end
48
+
49
+ describe "#perform" do
50
+ let(:interactor2) { double(:interactor2) }
51
+ let(:interactor3) { double(:interactor3) }
52
+ let(:interactor4) { double(:interactor4) }
53
+ let(:instance) { interactor.new }
54
+ let(:context) { instance.context }
55
+
56
+ before do
57
+ interactor.stub(:interactors) { [interactor2, interactor3, interactor4] }
58
+ end
59
+
60
+ it "performs each interactor in order with the context" do
61
+ expect(interactor2).to receive(:perform).once.with(context).ordered
62
+ expect(interactor3).to receive(:perform).once.with(context).ordered
63
+ expect(interactor4).to receive(:perform).once.with(context).ordered
64
+
65
+ expect(instance).not_to receive(:rollback)
66
+
67
+ instance.perform
68
+ end
69
+
70
+ it "builds up the performed interactors" do
71
+ interactor2.stub(:perform) do
72
+ expect(instance.performed).to eq([interactor2])
73
+ end
74
+
75
+ interactor3.stub(:perform) do
76
+ expect(instance.performed).to eq([interactor2, interactor3])
77
+ end
78
+
79
+ interactor4.stub(:perform) do
80
+ expect(instance.performed).to eq([interactor2, interactor3, interactor4])
81
+ end
82
+
83
+ expect {
84
+ instance.perform
85
+ }.to change {
86
+ instance.performed
87
+ }.from([]).to([interactor2, interactor3, interactor4])
88
+ end
89
+
90
+ it "aborts and rolls back on failure" do
91
+ expect(interactor2).to receive(:perform).once.with(context).ordered
92
+ expect(interactor3).to receive(:perform).once.with(context).ordered { context.fail! }
93
+ expect(interactor4).not_to receive(:perform)
94
+
95
+ expect(instance).to receive(:rollback).once.ordered do
96
+ expect(instance.performed).to eq([interactor2, interactor3])
97
+ end
98
+
99
+ instance.perform
100
+ end
101
+ end
102
+
103
+ describe "#rollback" do
104
+ let(:interactor2) { double(:interactor2) }
105
+ let(:interactor3) { double(:interactor3) }
106
+ let(:interactor4) { double(:interactor4) }
107
+ let(:instance) { interactor.new }
108
+ let(:context) { instance.context }
109
+
110
+ before do
111
+ interactor.stub(:interactors) { [interactor2, interactor3, interactor4] }
112
+ instance.stub(:performed) { [interactor2, interactor3] }
113
+ end
114
+
115
+ it "rolls back each performed interactor in reverse" do
116
+ expect(interactor4).not_to receive(:rollback)
117
+ expect(interactor3).to receive(:rollback).once.with(context).ordered
118
+ expect(interactor2).to receive(:rollback).once.with(context).ordered
119
+
120
+ instance.rollback
121
+ end
122
+ end
123
+
124
+ describe "#performed" do
125
+ let(:instance) { interactor.new }
126
+
127
+ it "is empty by default" do
128
+ expect(instance.performed).to eq([])
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,5 @@
1
+ require "spec_helper"
2
+
3
+ describe Interactor do
4
+ include_examples :lint
5
+ end
@@ -0,0 +1,6 @@
1
+ require "coveralls"
2
+ Coveralls.wear!
3
+
4
+ require "interactor"
5
+
6
+ Dir[File.expand_path("../support/*.rb", __FILE__)].each { |f| require f }
@@ -0,0 +1,3 @@
1
+ RSpec.configure do |config|
2
+ config.expect_with(:rspec) { |c| c.syntax = :expect }
3
+ end
@@ -0,0 +1,162 @@
1
+ shared_examples :lint do
2
+ let(:interactor) { Class.new.send(:include, described_class) }
3
+
4
+ describe ".perform" do
5
+ let(:instance) { double(:instance) }
6
+
7
+ it "performs an instance with the given context" do
8
+ expect(interactor).to receive(:new).once.with(foo: "bar") { instance }
9
+ expect(instance).to receive(:perform).once.with(no_args)
10
+
11
+ expect(interactor.perform(foo: "bar")).to eq(instance)
12
+ end
13
+
14
+ it "provides a blank context if none is given" do
15
+ expect(interactor).to receive(:new).once.with({}) { instance }
16
+ expect(instance).to receive(:perform).once.with(no_args)
17
+
18
+ expect(interactor.perform).to eq(instance)
19
+ end
20
+ end
21
+
22
+ describe ".rollback" do
23
+ let(:instance) { double(:instance) }
24
+
25
+ it "rolls back an instance with the given context" do
26
+ expect(interactor).to receive(:new).once.with(foo: "bar") { instance }
27
+ expect(instance).to receive(:rollback).once.with(no_args)
28
+
29
+ expect(interactor.rollback(foo: "bar")).to eq(instance)
30
+ end
31
+
32
+ it "provides a blank context if none is given" do
33
+ expect(interactor).to receive(:new).once.with({}) { instance }
34
+ expect(instance).to receive(:rollback).once.with(no_args)
35
+
36
+ expect(interactor.rollback).to eq(instance)
37
+ end
38
+ end
39
+
40
+ describe ".new" do
41
+ let(:context) { double(:context) }
42
+
43
+ it "initializes a context" do
44
+ expect(Interactor::Context).to receive(:build).once.with(foo: "bar") { context }
45
+
46
+ instance = interactor.new(foo: "bar")
47
+
48
+ expect(instance).to be_a(interactor)
49
+ expect(instance.context).to eq(context)
50
+ end
51
+
52
+ it "initializes a blank context if none is given" do
53
+ expect(Interactor::Context).to receive(:build).once.with({}) { context }
54
+
55
+ instance = interactor.new
56
+
57
+ expect(instance).to be_a(interactor)
58
+ expect(instance.context).to eq(context)
59
+ end
60
+
61
+ it "calls setup" do
62
+ interactor.class_eval do
63
+ def setup
64
+ context[:foo] = bar
65
+ end
66
+ end
67
+
68
+ instance = interactor.new(bar: "baz")
69
+
70
+ expect(instance.context[:foo]).to eq("baz")
71
+ end
72
+ end
73
+
74
+ describe "#setup" do
75
+ let(:instance) { interactor.new }
76
+
77
+ it "exists" do
78
+ expect(instance).to respond_to(:setup)
79
+ expect { instance.setup }.not_to raise_error
80
+ expect { instance.method(:setup) }.not_to raise_error
81
+ end
82
+ end
83
+
84
+ describe "#perform" do
85
+ let(:instance) { interactor.new }
86
+
87
+ it "exists" do
88
+ expect(instance).to respond_to(:perform)
89
+ expect { instance.perform }.not_to raise_error
90
+ expect { instance.method(:perform) }.not_to raise_error
91
+ end
92
+ end
93
+
94
+ describe "#rollback" do
95
+ let(:instance) { interactor.new }
96
+
97
+ it "exists" do
98
+ expect(instance).to respond_to(:rollback)
99
+ expect { instance.rollback }.not_to raise_error
100
+ expect { instance.method(:rollback) }.not_to raise_error
101
+ end
102
+ end
103
+
104
+ describe "#success?" do
105
+ let(:instance) { interactor.new }
106
+ let(:context) { instance.context }
107
+
108
+ it "defers to the context" do
109
+ context.stub(success?: true)
110
+ expect(instance.success?).to eq(true)
111
+
112
+ context.stub(success?: false)
113
+ expect(instance.success?).to eq(false)
114
+ end
115
+ end
116
+
117
+ describe "#failure?" do
118
+ let(:instance) { interactor.new }
119
+ let(:context) { instance.context }
120
+
121
+ it "defers to the context" do
122
+ context.stub(failure?: true)
123
+ expect(instance.failure?).to eq(true)
124
+
125
+ context.stub(failure?: false)
126
+ expect(instance.failure?).to eq(false)
127
+ end
128
+ end
129
+
130
+ describe "#fail!" do
131
+ let(:instance) { interactor.new }
132
+ let(:context) { instance.context }
133
+
134
+ it "defers to the context" do
135
+ expect(context).to receive(:fail!).once.with(no_args)
136
+
137
+ instance.fail!
138
+ end
139
+
140
+ it "passes updates to the context" do
141
+ expect(context).to receive(:fail!).once.with(foo: "bar")
142
+
143
+ instance.fail!(foo: "bar")
144
+ end
145
+ end
146
+
147
+ describe "context deferral" do
148
+ let(:instance) { interactor.new(foo: "bar") }
149
+
150
+ it "defers to keys that exist in the context" do
151
+ expect(instance).to respond_to(:foo)
152
+ expect(instance.foo).to eq("bar")
153
+ expect { instance.method(:foo) }.not_to raise_error
154
+ end
155
+
156
+ it "bombs if the key does not exist in the context" do
157
+ expect(instance).not_to respond_to(:baz)
158
+ expect { instance.baz }.to raise_error(NoMethodError)
159
+ expect { instance.method(:baz) }.to raise_error(NameError)
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,3 @@
1
+ RSpec.configure do |config|
2
+ config.order = "random"
3
+ end
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: interactor
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Collective Idea
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-08-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '10.1'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '10.1'
41
+ description: Interactor provides a common interface for performing complex interactions
42
+ in a single request.
43
+ email: info@collectiveidea.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - .gitignore
49
+ - .travis.yml
50
+ - Gemfile
51
+ - LICENSE.txt
52
+ - README.md
53
+ - Rakefile
54
+ - interactor.gemspec
55
+ - lib/interactor.rb
56
+ - lib/interactor/context.rb
57
+ - lib/interactor/organizer.rb
58
+ - spec/interactor/context_spec.rb
59
+ - spec/interactor/organizer_spec.rb
60
+ - spec/interactor_spec.rb
61
+ - spec/spec_helper.rb
62
+ - spec/support/expect.rb
63
+ - spec/support/lint.rb
64
+ - spec/support/random.rb
65
+ homepage: https://github.com/collectiveidea/interactor
66
+ licenses:
67
+ - MIT
68
+ metadata: {}
69
+ post_install_message:
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ requirements: []
84
+ rubyforge_project:
85
+ rubygems_version: 2.0.5
86
+ signing_key:
87
+ specification_version: 4
88
+ summary: Simple interactor implementation
89
+ test_files:
90
+ - spec/interactor/context_spec.rb
91
+ - spec/interactor/organizer_spec.rb
92
+ - spec/interactor_spec.rb
93
+ - spec/spec_helper.rb
94
+ - spec/support/expect.rb
95
+ - spec/support/lint.rb
96
+ - spec/support/random.rb