From 61747dcd5561b2539a30b475db14b776b44040ff Mon Sep 17 00:00:00 2001 From: Guillermo Guerrero Date: Mon, 16 May 2022 14:06:41 +0200 Subject: [PATCH 01/10] Added ensure organizers. --- README.md | 4 +- lib/interactor/organizer.rb | 56 +++++++++++++++++++++++++- spec/interactor/organizer_spec.rb | 66 ++++++++++++++++++++++++++++--- 3 files changed, 117 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 143b397..8fd40c8 100644 --- a/README.md +++ b/README.md @@ -405,7 +405,9 @@ purpose is to run *other* interactors. class PlaceOrder include Interactor::Organizer - organize CreateOrder, ChargeCard, SendThankYou + organize CreateOrder, ChargeCard + + ensure_do SendThankYou end ``` diff --git a/lib/interactor/organizer.rb b/lib/interactor/organizer.rb index fcba5bc..f030692 100644 --- a/lib/interactor/organizer.rb +++ b/lib/interactor/organizer.rb @@ -48,6 +48,33 @@ def organize(*interactors) @organized = interactors.flatten end + # Public: Declare Interactors to be invoked as part of the + # Interactor::Organizer's invocation in an ensured block. These + # interactors are invoked in the order in which they are declared. + # + # interactors - Zero or more (or an Array of) Interactor classes. + # + # Examples + # + # class MyFirstOrganizer + # include Interactor::Organizer + # + # organize InteractorOne, InteractorTwo + # ensure_do InteractorThree, InteractorFour + # end + # + # class MySecondOrganizer + # include Interactor::Organizer + # + # organize [InteractorThree, InteractorFour] + # ensure_do [InteractorFive, InteractorSix] + # end + # + # Returns nothing. + def ensure_do(*interactors) + @ensured = interactors.flatten + end + # Internal: An Array of declared Interactors to be invoked. # # Examples @@ -65,6 +92,25 @@ def organize(*interactors) def organized @organized ||= [] end + + # Internal: An Array of declared Interactors to be invoked in an ensure block. + # + # Examples + # + # class MyOrganizer + # include Interactor::Organizer + # + # organize InteractorOne, InteractorTwo + # ensure_do InteractorThree, InteractorFour + # end + # + # MyOrganizer.ensured + # # => [InteractorThree, InteractorFour] + # + # Returns an Array of Interactor classes or an empty Array. + def ensured + @ensured ||= [] + end end # Internal: Interactor::Organizer instance methods. @@ -75,8 +121,14 @@ module InstanceMethods # # Returns nothing. def call - self.class.organized.each do |interactor| - interactor.call!(context) + begin + self.class.organized.each do |interactor| + interactor.call!(context) + end + ensure + self.class.ensured.each do |interactor| + interactor.call(context) + end end end end diff --git a/spec/interactor/organizer_spec.rb b/spec/interactor/organizer_spec.rb index 5b02aaa..d7b6f62 100644 --- a/spec/interactor/organizer_spec.rb +++ b/spec/interactor/organizer_spec.rb @@ -4,10 +4,11 @@ module Interactor let(:organizer) { Class.new.send(:include, Organizer) } - describe ".organize" do - let(:interactor2) { double(:interactor2) } - let(:interactor3) { double(:interactor3) } + let(:interactor2) { double(:interactor2) } + let(:interactor3) { double(:interactor3) } + let(:interactor4) { double(:interactor4) } + describe ".organize" do it "sets interactors given class arguments" do expect { organizer.organize(interactor2, interactor3) @@ -31,27 +32,80 @@ module Interactor end end + describe "#call" do + let(:instance) { organizer.new } + let(:context) { double(:context) } + + before do + allow(instance).to receive(:context) { context } + allow(organizer).to receive(:organized) { + [interactor2, interactor3, interactor4] + } + 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 + + instance.call + end + end + + describe ".ensure_do" do + it "sets interactors given class arguments" do + expect { + organizer.ensure_do(interactor2, interactor3) + }.to change { + organizer.ensured + }.from([]).to([interactor2, interactor3]) + end + + it "sets interactors given an array of classes" do + expect { + organizer.ensure_do([interactor2, interactor3]) + }.to change { + organizer.ensured + }.from([]).to([interactor2, interactor3]) + end + end + + describe ".ensured" do + it "is empty by default" do + expect(organizer.ensured).to eq([]) + end + end + describe "#call" do let(:instance) { organizer.new } let(:context) { double(:context) } - let(:interactor2) { double(:interactor2) } - let(:interactor3) { double(:interactor3) } - let(:interactor4) { double(:interactor4) } + let(:interactor5) { double(:interactor2) } before do allow(instance).to receive(:context) { context } allow(organizer).to receive(:organized) { [interactor2, interactor3, interactor4] } + allow(organizer).to receive(:ensured) { + [interactor5] + } 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(interactor5).to receive(:call).once.with(context).ordered instance.call end + + it "calls the ensure interactor when there is an error" do + expect(interactor2).to receive(:call!).and_raise(Failure) + expect(interactor5).to receive(:call).once.with(context) + + expect { instance.call }.to raise_error(Failure) + end end end end From 700963c54884ec6765f05635eaabb34c0a7c2db2 Mon Sep 17 00:00:00 2001 From: Guillermo Guerrero Date: Mon, 16 May 2022 14:15:14 +0200 Subject: [PATCH 02/10] silly change. --- spec/interactor/organizer_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/interactor/organizer_spec.rb b/spec/interactor/organizer_spec.rb index d7b6f62..d926521 100644 --- a/spec/interactor/organizer_spec.rb +++ b/spec/interactor/organizer_spec.rb @@ -100,7 +100,7 @@ module Interactor instance.call end - it "calls the ensure interactor when there is an error" do + it "calls the ensure interactor when there is an error in one organized interactor" do expect(interactor2).to receive(:call!).and_raise(Failure) expect(interactor5).to receive(:call).once.with(context) From 2a594c433fdf24a9a32ba164474e45a62cdb779d Mon Sep 17 00:00:00 2001 From: Guillermo Guerrero Date: Mon, 16 May 2022 14:19:15 +0200 Subject: [PATCH 03/10] Added call! --- lib/interactor/organizer.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/interactor/organizer.rb b/lib/interactor/organizer.rb index f030692..d119934 100644 --- a/lib/interactor/organizer.rb +++ b/lib/interactor/organizer.rb @@ -121,6 +121,10 @@ module InstanceMethods # # Returns nothing. def call + call! + end + + def call! begin self.class.organized.each do |interactor| interactor.call!(context) From 1197588541002762237436c3b0092b743a419173 Mon Sep 17 00:00:00 2001 From: Guillermo Guerrero Date: Mon, 16 May 2022 14:29:41 +0200 Subject: [PATCH 04/10] polish --- spec/interactor/organizer_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/interactor/organizer_spec.rb b/spec/interactor/organizer_spec.rb index d926521..8b383ec 100644 --- a/spec/interactor/organizer_spec.rb +++ b/spec/interactor/organizer_spec.rb @@ -34,7 +34,7 @@ module Interactor describe "#call" do let(:instance) { organizer.new } - let(:context) { double(:context) } + let(:context) { double(:context) } before do allow(instance).to receive(:context) { context } @@ -79,7 +79,7 @@ module Interactor describe "#call" do let(:instance) { organizer.new } let(:context) { double(:context) } - let(:interactor5) { double(:interactor2) } + let(:interactor5) { double(:interactor5) } before do allow(instance).to receive(:context) { context } From 26bff18f414ad36000c92c3c230d319a63544bac Mon Sep 17 00:00:00 2001 From: Guillermo Guerrero Date: Mon, 16 May 2022 15:27:34 +0200 Subject: [PATCH 05/10] Fixed tests. --- spec/support/lint.rb | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/spec/support/lint.rb b/spec/support/lint.rb index 55b84d5..bffcae6 100644 --- a/spec/support/lint.rb +++ b/spec/support/lint.rb @@ -1,4 +1,7 @@ shared_examples :lint do + let(:foo_params) do + { foo: 'bar' } + end let(:interactor) { Class.new.send(:include, described_class) } describe ".call" do @@ -6,10 +9,10 @@ let(:instance) { double(:instance, context: context) } it "calls an instance with the given context" do - expect(interactor).to receive(:new).once.with(foo: "bar") { instance } + expect(interactor).to receive(:new).once.with(foo_params) { instance } expect(instance).to receive(:run).once.with(no_args) - expect(interactor.call(foo: "bar")).to eq(context) + expect(interactor.call(foo_params)).to eq(context) end it "provides a blank context if none is given" do @@ -25,10 +28,10 @@ let(:instance) { double(:instance, context: context) } it "calls an instance with the given context" do - expect(interactor).to receive(:new).once.with(foo: "bar") { instance } + expect(interactor).to receive(:new).once.with(foo_params) { instance } expect(instance).to receive(:run!).once.with(no_args) - expect(interactor.call!(foo: "bar")).to eq(context) + expect(interactor.call!(foo_params)).to eq(context) end it "provides a blank context if none is given" do @@ -43,9 +46,9 @@ let(:context) { double(:context) } it "initializes a context" do - expect(Interactor::Context).to receive(:build).once.with(foo: "bar") { context } + expect(Interactor::Context).to receive(:build).once.with(foo_params) { context } - instance = interactor.new(foo: "bar") + instance = interactor.new(foo_params) expect(instance).to be_a(interactor) expect(instance.context).to eq(context) From 79cd708d7b0f2020a5b6f98beb11dd1deddf74c3 Mon Sep 17 00:00:00 2001 From: Guillermo Guerrero Date: Mon, 16 May 2022 15:42:57 +0200 Subject: [PATCH 06/10] Added current_interactor. --- lib/interactor/organizer.rb | 1 + spec/interactor/organizer_spec.rb | 7 ++----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/interactor/organizer.rb b/lib/interactor/organizer.rb index d119934..9356423 100644 --- a/lib/interactor/organizer.rb +++ b/lib/interactor/organizer.rb @@ -127,6 +127,7 @@ def call def call! begin self.class.organized.each do |interactor| + context._current_interactor_class = interactor interactor.call!(context) end ensure diff --git a/spec/interactor/organizer_spec.rb b/spec/interactor/organizer_spec.rb index 8b383ec..c6c940a 100644 --- a/spec/interactor/organizer_spec.rb +++ b/spec/interactor/organizer_spec.rb @@ -3,6 +3,8 @@ module Interactor include_examples :lint let(:organizer) { Class.new.send(:include, Organizer) } + let(:instance) { organizer.new } + let(:context) { Interactor::Context.new } let(:interactor2) { double(:interactor2) } let(:interactor3) { double(:interactor3) } @@ -33,9 +35,6 @@ module Interactor end describe "#call" do - let(:instance) { organizer.new } - let(:context) { double(:context) } - before do allow(instance).to receive(:context) { context } allow(organizer).to receive(:organized) { @@ -77,8 +76,6 @@ module Interactor end describe "#call" do - let(:instance) { organizer.new } - let(:context) { double(:context) } let(:interactor5) { double(:interactor5) } before do From b496af726ff5d7c6a09b6f5661d7677e9e269593 Mon Sep 17 00:00:00 2001 From: Guillermo Guerrero Ibarra Date: Mon, 16 May 2022 20:33:21 +0200 Subject: [PATCH 07/10] Added ensure to interactor class. --- README.md | 21 ++++++ lib/interactor/hooks.rb | 79 +++++++++++++++++++-- spec/interactor/hooks_spec.rb | 128 +++++++++++++++++++++++++++++++++- 3 files changed, 220 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 8fd40c8..fb443be 100644 --- a/README.md +++ b/README.md @@ -198,6 +198,27 @@ around after 2 around after 1 ``` +#### Ensure Hooks + +Sometimes an interactor needs to perform an action even if the context fails like the +typical begin ensure block. + +```ruby +ensure_do do + context.finish_time = Time.now +end +``` + +A symbol argument can also be given, rather than a block. + +```ruby +before :set_finish_time + +def zero_emails_sent + context.finish_time = Time.now +end +``` + #### Interactor Concerns An interactor can define multiple before/after hooks, allowing common hooks to diff --git a/lib/interactor/hooks.rb b/lib/interactor/hooks.rb index d82e7a5..f5e214a 100644 --- a/lib/interactor/hooks.rb +++ b/lib/interactor/hooks.rb @@ -127,6 +127,43 @@ def after(*hooks, &block) hooks.each { |hook| after_hooks.unshift(hook) } end + # Public: Declare hooks to run after hooks in an ensure block. + # The ensure method may be called multiple times; subsequent calls prepend declared + # hooks to existing ensure hooks. + # + # hooks - Zero or more Symbol method names representing instance methods + # to be called after the hooks invocations. + # block - An optional block to be executed as a hook. If given, the block + # is executed before methods corresponding to any given Symbols. + # + # Examples + # + # class MyInteractor + # include Interactor + # + # ensure_do :set_finish_time + # + # ensure_do do + # puts "finished" + # end + # + # def call + # puts "called" + # end + # + # private + # + # def set_finish_time + # context.finish_time = Time.now + # end + # end + # + # Returns nothing. + def ensure_do(*hooks, &block) + hooks << block if block + hooks.each { |hook| ensure_hooks.unshift(hook) } + end + # Internal: An Array of declared hooks to run around Interactor # invocation. The hooks appear in the order in which they will be run. # @@ -165,7 +202,7 @@ def before_hooks @before_hooks ||= [] end - # Internal: An Array of declared hooks to run before Interactor + # Internal: An Array of declared hooks to run after Interactor # invocation. The hooks appear in the order in which they will be run. # # Examples @@ -183,6 +220,25 @@ def before_hooks def after_hooks @after_hooks ||= [] end + + # Internal: An Array of declared hooks to run after the hooks + # invocation. The hooks appear in the order in which they will be run. + # + # Examples + # + # class MyInteractor + # include Interactor + # + # ensure_do :set_finish_time, :say_goodbye + # end + # + # MyInteractor.ensure_hooks + # # => [:say_goodbye, :set_finish_time] + # + # Returns an Array of Symbols and Procs. + def ensure_hooks + @ensure_hooks ||= [] + end end private @@ -208,10 +264,14 @@ def after_hooks # # Returns nothing. def with_hooks - run_around_hooks do - run_before_hooks - yield - run_after_hooks + begin + run_around_hooks do + run_before_hooks + yield + run_after_hooks + end + ensure + run_ensure_hooks end end @@ -238,7 +298,14 @@ def run_after_hooks run_hooks(self.class.after_hooks) end - # Internal: Run a colection of hooks. The "run_hooks" method is the common + # Internal: Run ensure hooks. + # + # Returns nothing. + def run_ensure_hooks + run_hooks(self.class.ensure_hooks) + end + + # Internal: Run a collection of hooks. The "run_hooks" method is the common # interface by which collections of either before or after hooks are run. # # hooks - An Array of Symbol and Proc hooks. diff --git a/spec/interactor/hooks_spec.rb b/spec/interactor/hooks_spec.rb index 7e40f63..c96df11 100644 --- a/spec/interactor/hooks_spec.rb +++ b/spec/interactor/hooks_spec.rb @@ -7,6 +7,10 @@ def build_hooked(&block) hooked.class_eval do attr_reader :steps + def self.create + new + end + def self.process new.tap(&:process).steps end @@ -306,7 +310,95 @@ def add_after2 end end - context "with around, before and after hooks" do + context "with an ensure hook method" do + let(:hooked) { + build_hooked do + ensure_do :add_ensure + + private + + def add_ensure + steps << :ensure + end + end + } + + it "runs the after hook methods" do + expect(hooked.process).to eq([ + :process, + :ensure + ]) + end + end + + context "with an ensure hook block" do + let(:hooked) { + build_hooked do + ensure_do do + steps << :ensure + end + end + } + + it "runs the after hook blocks" do + expect(hooked.process).to eq([ + :process, + :ensure + ]) + end + end + + context "with an ensure hook method and block in one call" do + let(:hooked) { + build_hooked do + ensure_do :add_ensure1 do + steps << :ensure2 + end + + private + + def add_ensure1 + steps << :ensure1 + end + end + } + + it "runs the after hook method and block in order" do + expect(hooked.process).to eq([ + :process, + :ensure2, + :ensure1 + ]) + end + end + + context "with an ensure hook method and block in multiple calls" do + let(:hooked) { + build_hooked do + after do + steps << :ensure1 + end + + after :add_ensure2 + + private + + def add_ensure2 + steps << :ensure2 + end + end + } + + it "runs the after hook block and method in order" do + expect(hooked.process).to eq([ + :process, + :ensure2, + :ensure1 + ]) + end + end + + context "with around, before, after and ensure hooks" do let(:hooked) { build_hooked do around do |hooked| @@ -336,6 +428,14 @@ def add_after2 after do steps << :after2 end + + ensure_do do + steps << :ensure1 + end + + ensure_do do + steps << :ensure2 + end end } @@ -349,10 +449,34 @@ def add_after2 :after2, :after1, :around_after2, - :around_after1 + :around_after1, + :ensure2, + :ensure1 ]) end end + + context "ensure hook run even if an exception is raised" do + let(:hooked) { + build_hooked do + ensure_do :add_ensure + + private + + def add_ensure + steps << :ensure + end + end + } + + it "run the ensure hook" do + allow_any_instance_of(Interactor::Hooks).to receive(:run_around_hooks).and_raise(Failure) + object = hooked.create + + expect { object.process }.to raise_error(Failure) + expect(object.steps).to eq([:ensure]) + end + end end end end From d60171024f077835fdf3c6617a5cd5e24b2f1fdf Mon Sep 17 00:00:00 2001 From: Guillermo Guerrero Ibarra Date: Tue, 17 May 2022 17:56:45 +0200 Subject: [PATCH 08/10] Fixed example. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fb443be..a7663ae 100644 --- a/README.md +++ b/README.md @@ -212,9 +212,9 @@ end A symbol argument can also be given, rather than a block. ```ruby -before :set_finish_time +ensure_do :set_finish_time -def zero_emails_sent +def set_finish_time context.finish_time = Time.now end ``` From ad9de161869002ddee747fad7df7569c7228ef7f Mon Sep 17 00:00:00 2001 From: Guillermo Guerrero Ibarra Date: Wed, 18 May 2022 10:50:25 +0200 Subject: [PATCH 09/10] trigger pipelines on PRs --- .github/workflows/tests.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c3eb96d..5e1562b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,10 +1,6 @@ name: Run Tests -on: - push: - branches: - - "master" - - "v3" +on: [push, pull_request] jobs: spec: From 6ed99136bea4e379b579d514e8f2d72c8b5573f8 Mon Sep 17 00:00:00 2001 From: Guillermo Guerrero Ibarra Date: Wed, 18 May 2022 11:42:47 +0200 Subject: [PATCH 10/10] style compliance. --- lib/interactor/hooks.rb | 16 +++++++--------- lib/interactor/organizer.rb | 16 +++++++--------- spec/interactor/hooks_spec.rb | 2 +- spec/support/lint.rb | 2 +- 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/lib/interactor/hooks.rb b/lib/interactor/hooks.rb index f5e214a..9b6c7b0 100644 --- a/lib/interactor/hooks.rb +++ b/lib/interactor/hooks.rb @@ -221,7 +221,7 @@ def after_hooks @after_hooks ||= [] end - # Internal: An Array of declared hooks to run after the hooks + # Internal: An Array of declared hooks to run after the hooks # invocation. The hooks appear in the order in which they will be run. # # Examples @@ -264,15 +264,13 @@ def ensure_hooks # # Returns nothing. def with_hooks - begin - run_around_hooks do - run_before_hooks - yield - run_after_hooks - end - ensure - run_ensure_hooks + run_around_hooks do + run_before_hooks + yield + run_after_hooks end + ensure + run_ensure_hooks end # Internal: Run around hooks. diff --git a/lib/interactor/organizer.rb b/lib/interactor/organizer.rb index 9356423..5ee4589 100644 --- a/lib/interactor/organizer.rb +++ b/lib/interactor/organizer.rb @@ -125,15 +125,13 @@ def call end def call! - begin - self.class.organized.each do |interactor| - context._current_interactor_class = interactor - interactor.call!(context) - end - ensure - self.class.ensured.each do |interactor| - interactor.call(context) - end + self.class.organized.each do |interactor| + context._current_interactor_class = interactor + interactor.call!(context) + end + ensure + self.class.ensured.each do |interactor| + interactor.call(context) end end end diff --git a/spec/interactor/hooks_spec.rb b/spec/interactor/hooks_spec.rb index c96df11..ec4dbc5 100644 --- a/spec/interactor/hooks_spec.rb +++ b/spec/interactor/hooks_spec.rb @@ -313,7 +313,7 @@ def add_after2 context "with an ensure hook method" do let(:hooked) { build_hooked do - ensure_do :add_ensure + ensure_do :add_ensure private diff --git a/spec/support/lint.rb b/spec/support/lint.rb index bffcae6..fc37ae8 100644 --- a/spec/support/lint.rb +++ b/spec/support/lint.rb @@ -1,6 +1,6 @@ shared_examples :lint do let(:foo_params) do - { foo: 'bar' } + {foo: "bar"} end let(:interactor) { Class.new.send(:include, described_class) }