From 7326e7728b035c29e761e98c387bd86fc736e4be Mon Sep 17 00:00:00 2001 From: Anton Chuchkalov Date: Sat, 25 Mar 2017 12:28:58 +0300 Subject: [PATCH] replace raise with throw to handle context failure (#126) --- lib/interactor.rb | 21 ++++---- lib/interactor/context.rb | 2 +- lib/interactor/organizer.rb | 5 +- spec/interactor/context_spec.rb | 12 +---- spec/interactor/organizer_spec.rb | 23 ++++++--- spec/support/lint.rb | 80 +++++++++++++++++++++++-------- 6 files changed, 94 insertions(+), 49 deletions(-) diff --git a/lib/interactor.rb b/lib/interactor.rb index 2423630..8bb0c06 100644 --- a/lib/interactor.rb +++ b/lib/interactor.rb @@ -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 @@ -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 diff --git a/lib/interactor/context.rb b/lib/interactor/context.rb index cb269d2..0d011e3 100644 --- a/lib/interactor/context.rb +++ b/lib/interactor/context.rb @@ -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 diff --git a/lib/interactor/organizer.rb b/lib/interactor/organizer.rb index fcba5bc..cf31ffc 100644 --- a/lib/interactor/organizer.rb +++ b/lib/interactor/organizer.rb @@ -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. @@ -76,7 +76,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 diff --git a/spec/interactor/context_spec.rb b/spec/interactor/context_spec.rb index eb8b905..1f18d14 100644 --- a/spec/interactor/context_spec.rb +++ b/spec/interactor/context_spec.rb @@ -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 diff --git a/spec/interactor/organizer_spec.rb b/spec/interactor/organizer_spec.rb index 5b02aaa..8c385ea 100644 --- a/spec/interactor/organizer_spec.rb +++ b/spec/interactor/organizer_spec.rb @@ -33,25 +33,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 diff --git a/spec/support/lint.rb b/spec/support/lint.rb index 73346e5..ffb18f2 100644 --- a/spec/support/lint.rb +++ b/spec/support/lint.rb @@ -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) } @@ -66,25 +74,45 @@ 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 @@ -92,26 +120,38 @@ 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