Skip to content

Commit 7326e77

Browse files
committed
replace raise with throw to handle context failure (#126)
1 parent 294d19d commit 7326e77

File tree

6 files changed

+94
-49
lines changed

6 files changed

+94
-49
lines changed

lib/interactor.rb

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,16 @@ def initialize(context = {})
112112
#
113113
# Returns nothing.
114114
def run
115-
run!
116-
rescue Failure
115+
catch(:early_return) do
116+
with_hooks do
117+
call
118+
context.called!(self)
119+
end
120+
end
121+
context.rollback! if context.failure?
122+
rescue
123+
context.rollback!
124+
raise
117125
end
118126

119127
# Internal: Invoke an Interactor instance along with all defined hooks. The
@@ -139,13 +147,8 @@ def run
139147
# Returns nothing.
140148
# Raises Interactor::Failure if the context is failed.
141149
def run!
142-
with_hooks do
143-
call
144-
context.called!(self)
145-
end
146-
rescue
147-
context.rollback!
148-
raise
150+
run
151+
raise(Failure, context) if context.failure?
149152
end
150153

151154
# Public: Invoke an Interactor instance without any hooks, tracking, or

lib/interactor/context.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ def failure?
123123
def fail!(context = {})
124124
context.each { |key, value| modifiable[key.to_sym] = value }
125125
@failure = true
126-
raise Failure, self
126+
throw :early_return
127127
end
128128

129129
# Internal: Track that an Interactor has been called. The "called!" method

lib/interactor/organizer.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ module Interactor
88
# class MyOrganizer
99
# include Interactor::Organizer
1010
#
11-
# organizer InteractorOne, InteractorTwo
11+
# organize InteractorOne, InteractorTwo
1212
# end
1313
module Organizer
1414
# Internal: Install Interactor::Organizer's behavior in the given class.
@@ -76,7 +76,8 @@ module InstanceMethods
7676
# Returns nothing.
7777
def call
7878
self.class.organized.each do |interactor|
79-
interactor.call!(context)
79+
throw(:early_return) if context.failure?
80+
interactor.call(context)
8081
end
8182
end
8283
end

spec/interactor/context_spec.rb

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -109,18 +109,10 @@ module Interactor
109109
}.from("bar").to("baz")
110110
end
111111

112-
it "raises failure" do
112+
it "throws :early_return" do
113113
expect {
114114
context.fail!
115-
}.to raise_error(Failure)
116-
end
117-
118-
it "makes the context available from the failure" do
119-
begin
120-
context.fail!
121-
rescue Failure => error
122-
expect(error.context).to eq(context)
123-
end
115+
}.to throw_symbol(:early_return)
124116
end
125117
end
126118

spec/interactor/organizer_spec.rb

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,25 +33,34 @@ module Interactor
3333

3434
describe "#call" do
3535
let(:instance) { organizer.new }
36-
let(:context) { double(:context) }
36+
let(:context) { double(:context, failure?: false) }
3737
let(:interactor2) { double(:interactor2) }
3838
let(:interactor3) { double(:interactor3) }
3939
let(:interactor4) { double(:interactor4) }
40+
let(:organized_interactors) { [interactor2, interactor3, interactor4] }
4041

4142
before do
4243
allow(instance).to receive(:context) { context }
43-
allow(organizer).to receive(:organized) {
44-
[interactor2, interactor3, interactor4]
45-
}
44+
allow(organizer).to receive(:organized) { organized_interactors }
45+
organized_interactors.each do |organized_interactor|
46+
allow(organized_interactor).to receive(:call)
47+
end
4648
end
4749

4850
it "calls each interactor in order with the context" do
49-
expect(interactor2).to receive(:call!).once.with(context).ordered
50-
expect(interactor3).to receive(:call!).once.with(context).ordered
51-
expect(interactor4).to receive(:call!).once.with(context).ordered
51+
expect(interactor2).to receive(:call).once.with(context).ordered
52+
expect(interactor3).to receive(:call).once.with(context).ordered
53+
expect(interactor4).to receive(:call).once.with(context).ordered
5254

5355
instance.call
5456
end
57+
58+
it "throws :early_return on failure of one of organizers" do
59+
allow(context).to receive(:failure?).and_return(false, true)
60+
expect {
61+
instance.call
62+
}.to throw_symbol(:early_return)
63+
end
5564
end
5665
end
5766
end

spec/support/lint.rb

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
shared_examples :lint do
22
let(:interactor) { Class.new.send(:include, described_class) }
33

4+
let(:context_double) do
5+
double(:double, failure?: false, called!: nil, rollback!: nil)
6+
end
7+
8+
let(:failed_context_double) do
9+
double(:failed_context_double, failure?: true, called!: nil, rollback!: nil)
10+
end
11+
412
describe ".call" do
513
let(:context) { double(:context) }
614
let(:instance) { double(:instance, context: context) }
@@ -66,52 +74,84 @@
6674
let(:instance) { interactor.new }
6775

6876
it "runs the interactor" do
69-
expect(instance).to receive(:run!).once.with(no_args)
77+
expect(instance).to receive(:call).once.with(no_args)
7078

7179
instance.run
7280
end
7381

74-
it "rescues failure" do
75-
expect(instance).to receive(:run!).and_raise(Interactor::Failure)
76-
82+
it "catches :early_return" do
83+
allow(instance).to receive(:call).and_throw(:early_return)
7784
expect {
7885
instance.run
79-
}.not_to raise_error
86+
}.not_to throw_symbol
8087
end
8188

82-
it "raises other errors" do
83-
expect(instance).to receive(:run!).and_raise("foo")
89+
context "when error is raised inside #call" do
90+
it "propagates it and rollbacks context" do
91+
allow(instance).to receive(:context) { context_double }
92+
allow(instance).to receive(:call).and_raise("foo")
8493

85-
expect {
94+
expect(instance.context).to receive(:rollback!)
95+
expect {
96+
instance.run
97+
}.to raise_error("foo")
98+
end
99+
end
100+
101+
context "on call failure" do
102+
before do
103+
allow(instance).to receive(:context) { failed_context_double }
104+
end
105+
106+
it "doesn't raise Failure" do
107+
expect {
108+
instance.run
109+
}.not_to raise_error
110+
end
111+
112+
it "rollbacks context on error" do
113+
expect(instance.context).to receive(:rollback!)
86114
instance.run
87-
}.to raise_error("foo")
115+
end
88116
end
89117
end
90118

91119
describe "#run!" do
92120
let(:instance) { interactor.new }
93121

94122
it "calls the interactor" do
95-
expect(instance).to receive(:call).once.with(no_args)
123+
expect(instance).to receive(:run).once.with(no_args)
96124

97125
instance.run!
98126
end
99127

100-
it "raises failure" do
101-
expect(instance).to receive(:run!).and_raise(Interactor::Failure)
102-
103-
expect {
104-
instance.run!
105-
}.to raise_error(Interactor::Failure)
106-
end
107-
108-
it "raises other errors" do
109-
expect(instance).to receive(:run!).and_raise("foo")
128+
it "propagates errors" do
129+
expect(instance).to receive(:run).and_raise("foo")
110130

111131
expect {
112132
instance.run
113133
}.to raise_error("foo")
114134
end
135+
136+
context "on failure" do
137+
before do
138+
allow(instance).to receive(:context) { failed_context_double }
139+
end
140+
141+
it "raises Interactor::Failure" do
142+
expect {
143+
instance.run!
144+
}.to raise_error(Interactor::Failure)
145+
end
146+
147+
it "makes context available from the error" do
148+
begin
149+
instance.run!
150+
rescue Interactor::Failure => error
151+
expect(error.context).to be(instance.context)
152+
end
153+
end
154+
end
115155
end
116156

117157
describe "#call" do

0 commit comments

Comments
 (0)