From 5a683455286c7117a13e776220779f444fac5ef3 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 | 22 +++++---- 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, 96 insertions(+), 48 deletions(-) diff --git a/lib/interactor.rb b/lib/interactor.rb index 0109494..974bdb9 100644 --- a/lib/interactor.rb +++ b/lib/interactor.rb @@ -112,8 +112,17 @@ def initialize(context = {}) # # Returns nothing. def run - run! - rescue Failure + catch(:early_return) do + with_hooks do + call(*arguments_for_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 +148,8 @@ def run # Returns nothing. # Raises Interactor::Failure if the context is failed. def run! - with_hooks do - call(*arguments_for_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 57f0087..699b414 100644 --- a/lib/interactor/context.rb +++ b/lib/interactor/context.rb @@ -123,7 +123,7 @@ def failure? def fail!(context = {}) context.each { |key, value| self[key] = 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 1d27992..6e22d64 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. @@ -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 diff --git a/spec/interactor/context_spec.rb b/spec/interactor/context_spec.rb index 86f1fb1..a479757 100644 --- a/spec/interactor/context_spec.rb +++ b/spec/interactor/context_spec.rb @@ -16,7 +16,7 @@ module Interactor end it "doesn't affect the original hash" do - hash = {foo: "bar"} + hash = { foo: "bar" } context = Context.build(hash) expect(context).to be_a(Context) @@ -137,16 +137,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 - context.fail! - rescue Failure => error - expect(error.context).to eq(context) + }.to throw_symbol(:early_return) end end diff --git a/spec/interactor/organizer_spec.rb b/spec/interactor/organizer_spec.rb index a664c7b..825b693 100644 --- a/spec/interactor/organizer_spec.rb +++ b/spec/interactor/organizer_spec.rb @@ -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 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