Skip to content

Commit

Permalink
replace raise with throw to handle context failure (collectiveidea#126)
Browse files Browse the repository at this point in the history
  • Loading branch information
hedgesky committed Apr 25, 2017
1 parent 15e70dd commit 8909470
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 49 deletions.
21 changes: 12 additions & 9 deletions lib/interactor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,16 @@ def initialize(context = {})
#
# Returns nothing.
def run
run!
rescue Failure
catch(:early_return) do
with_hooks do
call
context.called!(self)
end
end
context.rollback! if context.failure?
rescue
context.rollback!
raise
end

# Internal: Invoke an Interactor instance along with all defined hooks. The
Expand All @@ -139,13 +147,8 @@ def run
# Returns nothing.
# Raises Interactor::Failure if the context is failed.
def run!
with_hooks do
call
context.called!(self)
end
rescue
context.rollback!
raise
run
raise(Failure, context) if context.failure?
end

# Public: Invoke an Interactor instance without any hooks, tracking, or
Expand Down
2 changes: 1 addition & 1 deletion lib/interactor/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ def failure?
def fail!(context = {})
context.each { |key, value| modifiable[key.to_sym] = value }
@failure = true
raise Failure, self
throw :early_return
end

# Internal: Track that an Interactor has been called. The "called!" method
Expand Down
5 changes: 3 additions & 2 deletions lib/interactor/organizer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ module Interactor
# class MyOrganizer
# include Interactor::Organizer
#
# organizer InteractorOne, InteractorTwo
# organize InteractorOne, InteractorTwo
# end
module Organizer
# Internal: Install Interactor::Organizer's behavior in the given class.
Expand Down Expand Up @@ -77,7 +77,8 @@ module InstanceMethods
# Returns nothing.
def call
self.class.organized.each do |interactor|
interactor.call!(context)
throw(:early_return) if context.failure?
interactor.call(context)
end
end
end
Expand Down
12 changes: 2 additions & 10 deletions spec/interactor/context_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -109,18 +109,10 @@ module Interactor
}.from("bar").to("baz")
end

it "raises failure" do
it "throws :early_return" do
expect {
context.fail!
}.to raise_error(Failure)
end

it "makes the context available from the failure" do
begin
context.fail!
rescue Failure => error
expect(error.context).to eq(context)
end
}.to throw_symbol(:early_return)
end
end

Expand Down
23 changes: 16 additions & 7 deletions spec/interactor/organizer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,25 +43,34 @@ module Interactor

describe "#call" do
let(:instance) { organizer.new }
let(:context) { double(:context) }
let(:context) { double(:context, failure?: false) }
let(:interactor2) { double(:interactor2) }
let(:interactor3) { double(:interactor3) }
let(:interactor4) { double(:interactor4) }
let(:organized_interactors) { [interactor2, interactor3, interactor4] }

before do
allow(instance).to receive(:context) { context }
allow(organizer).to receive(:organized) {
[interactor2, interactor3, interactor4]
}
allow(organizer).to receive(:organized) { organized_interactors }
organized_interactors.each do |organized_interactor|
allow(organized_interactor).to receive(:call)
end
end

it "calls each interactor in order with the context" do
expect(interactor2).to receive(:call!).once.with(context).ordered
expect(interactor3).to receive(:call!).once.with(context).ordered
expect(interactor4).to receive(:call!).once.with(context).ordered
expect(interactor2).to receive(:call).once.with(context).ordered
expect(interactor3).to receive(:call).once.with(context).ordered
expect(interactor4).to receive(:call).once.with(context).ordered

instance.call
end

it "throws :early_return on failure of one of organizers" do
allow(context).to receive(:failure?).and_return(false, true)
expect {
instance.call
}.to throw_symbol(:early_return)
end
end
end
end
80 changes: 60 additions & 20 deletions spec/support/lint.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
shared_examples :lint do
let(:interactor) { Class.new.send(:include, described_class) }

let(:context_double) do
double(:double, failure?: false, called!: nil, rollback!: nil)
end

let(:failed_context_double) do
double(:failed_context_double, failure?: true, called!: nil, rollback!: nil)
end

describe ".call" do
let(:context) { double(:context) }
let(:instance) { double(:instance, context: context) }
Expand Down Expand Up @@ -66,52 +74,84 @@
let(:instance) { interactor.new }

it "runs the interactor" do
expect(instance).to receive(:run!).once.with(no_args)
expect(instance).to receive(:call).once.with(no_args)

instance.run
end

it "rescues failure" do
expect(instance).to receive(:run!).and_raise(Interactor::Failure)

it "catches :early_return" do
allow(instance).to receive(:call).and_throw(:early_return)
expect {
instance.run
}.not_to raise_error
}.not_to throw_symbol
end

it "raises other errors" do
expect(instance).to receive(:run!).and_raise("foo")
context "when error is raised inside #call" do
it "propagates it and rollbacks context" do
allow(instance).to receive(:context) { context_double }
allow(instance).to receive(:call).and_raise("foo")

expect {
expect(instance.context).to receive(:rollback!)
expect {
instance.run
}.to raise_error("foo")
end
end

context "on call failure" do
before do
allow(instance).to receive(:context) { failed_context_double }
end

it "doesn't raise Failure" do
expect {
instance.run
}.not_to raise_error
end

it "rollbacks context on error" do
expect(instance.context).to receive(:rollback!)
instance.run
}.to raise_error("foo")
end
end
end

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

it "calls the interactor" do
expect(instance).to receive(:call).once.with(no_args)
expect(instance).to receive(:run).once.with(no_args)

instance.run!
end

it "raises failure" do
expect(instance).to receive(:run!).and_raise(Interactor::Failure)

expect {
instance.run!
}.to raise_error(Interactor::Failure)
end

it "raises other errors" do
expect(instance).to receive(:run!).and_raise("foo")
it "propagates errors" do
expect(instance).to receive(:run).and_raise("foo")

expect {
instance.run
}.to raise_error("foo")
end

context "on failure" do
before do
allow(instance).to receive(:context) { failed_context_double }
end

it "raises Interactor::Failure" do
expect {
instance.run!
}.to raise_error(Interactor::Failure)
end

it "makes context available from the error" do
begin
instance.run!
rescue Interactor::Failure => error
expect(error.context).to be(instance.context)
end
end
end
end

describe "#call" do
Expand Down

0 comments on commit 8909470

Please sign in to comment.