From bc0fbb605e31a3dea3c3588ec6af15a614ae931d Mon Sep 17 00:00:00 2001 From: Kasper Meyer Date: Mon, 19 Oct 2020 20:17:02 +0200 Subject: [PATCH 1/3] Ensure interactors inherit hooks defined in ancestors --- README.md | 56 +++++++++++++ lib/interactor/hooks.rb | 151 ++++++++++++++++++++++++++++++++-- spec/interactor/hooks_spec.rb | 104 +++++++++++++++++++++++ 3 files changed, 304 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 6de00f2..30e5a70 100644 --- a/README.md +++ b/README.md @@ -470,6 +470,62 @@ end interactor should have a single purpose, there should be no need to clean up after any failed interactor. +## Interactor Inheritance + +Interactors can inherit from other interactors. Subclasses will inherit hooks declared in ancestors: + +```ruby +class ParentInteractor + around do |interactor| + puts "around before ancestor" + interactor.call + puts "around after ancestor" + end + + before do + puts "before ancestor" + end + + after do + puts "after ancestor" + end +end + +class ChildInteractor < ParentInteractor + around do |interactor| + puts "around before child" + interactor.call + puts "around after child" + end + + before do + puts "before child" + end + + after do + puts "after child" + end + + def call + puts "called" + end +end +``` + +Calling the child interactor will output: + +``` +around before child +around before parent +before child +before parent +called +after parent +after child +around after parent +around after child +``` + ## Testing Interactors When written correctly, an interactor is easy to test because it only *does* one diff --git a/lib/interactor/hooks.rb b/lib/interactor/hooks.rb index d82e7a5..3c2369a 100644 --- a/lib/interactor/hooks.rb +++ b/lib/interactor/hooks.rb @@ -50,7 +50,7 @@ module ClassMethods # Returns nothing. def around(*hooks, &block) hooks << block if block - hooks.each { |hook| around_hooks.push(hook) } + hooks.each { |hook| internal_around_hooks.push(hook) } end # Public: Declare hooks to run before Interactor invocation. The before @@ -87,7 +87,7 @@ def around(*hooks, &block) # Returns nothing. def before(*hooks, &block) hooks << block if block - hooks.each { |hook| before_hooks.push(hook) } + hooks.each { |hook| internal_before_hooks.push(hook) } end # Public: Declare hooks to run after Interactor invocation. The after @@ -124,11 +124,12 @@ def before(*hooks, &block) # Returns nothing. def after(*hooks, &block) hooks << block if block - hooks.each { |hook| after_hooks.unshift(hook) } + hooks.each { |hook| internal_after_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. + # Includes hooks declared in ancestors. # # Examples # @@ -143,11 +144,12 @@ def after(*hooks, &block) # # Returns an Array of Symbols and Procs. def around_hooks - @around_hooks ||= [] + internal_around_hooks + ancestor_around_hooks end # Internal: An Array of declared hooks to run before Interactor # invocation. The hooks appear in the order in which they will be run. + # Includes hooks declared in ancestors. # # Examples # @@ -162,11 +164,12 @@ def around_hooks # # Returns an Array of Symbols and Procs. def before_hooks - @before_hooks ||= [] + internal_before_hooks + ancestor_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. + # Includes hooks declared in ancestors. # # Examples # @@ -181,7 +184,141 @@ def before_hooks # # Returns an Array of Symbols and Procs. def after_hooks - @after_hooks ||= [] + ancestor_after_hooks + internal_after_hooks + end + + # Internal: An Array of declared hooks to run around Interactor + # invocation. The hooks appear in the order in which they will be run. + # + # Examples + # + # class MyInteractor + # include Interactor + # + # around :time_execution, :use_transaction + # end + # + # MyInteractor.internal_around_hooks + # # => [:time_execution, :use_transaction] + # + # Returns an Array of Symbols and Procs. + def internal_around_hooks + @internal_around_hooks ||= [] + end + + # Internal: An Array of declared hooks to run before Interactor + # invocation. The hooks appear in the order in which they will be run. + # + # Examples + # + # class MyInteractor + # include Interactor + # + # before :set_start_time, :say_hello + # end + # + # MyInteractor.internal_before_hooks + # # => [:set_start_time, :say_hello] + # + # Returns an Array of Symbols and Procs. + def internal_before_hooks + @internal_before_hooks ||= [] + end + + # Internal: An Array of declared hooks to run after Interactor + # invocation. The hooks appear in the order in which they will be run. + # + # Examples + # + # class MyInteractor + # include Interactor + # + # after :set_finish_time, :say_goodbye + # end + # + # MyInteractor.internal_after_hooks + # # => [:say_goodbye, :set_finish_time] + # + # Returns an Array of Symbols and Procs. + def internal_after_hooks + @internal_after_hooks ||= [] + end + + # Internal: An Array of around hooks declared in ancestors. + # The hooks appear in the order in which they will be run. + # + # Examples + # + # class MyParentInteractor + # include Interactor + # + # around :time_execution, :use_transaction + # end + # + # class MyChildInteractor < MyParentInteractor + # end + # + # MyChildInteractor.ancestor_around_hooks + # # => [:time_execution, :use_transaction] + # + # Returns an Array of Symbols and Procs. + def ancestor_around_hooks + ancestor_hooks(:around_hooks) + end + + # Internal: An Array of before hooks declared in ancestors. + # The hooks appear in the order in which they will be run. + # + # Examples + # + # class MyParentInteractor + # include Interactor + # + # before :set_finish_time, :say_goodbye + # end + # + # class MyChildInteractor < MyParentInteractor + # end + # + # MyChildInteractor.ancestor_before_hooks + # # => [:set_finish_time, :say_goodbye] + # + # Returns an Array of Symbols and Procs. + def ancestor_before_hooks + ancestor_hooks(:before_hooks) + end + + # Internal: An Array of after hooks declared in ancestors. + # The hooks appear in the order in which they will be run. + # + # Examples + # + # class MyParentInteractor + # include Interactor + # + # after :set_finish_time, :say_goodbye + # end + # + # class MyChildInteractor < MyParentInteractor + # end + # + # MyChildInteractor.ancestor_after_hooks + # # => [:say_goodbye, :set_finish_time] + # + # Returns an Array of Symbols and Procs. + def ancestor_after_hooks + ancestor_hooks(:after_hooks) + end + + private + + # Internal: Fetches hooks declared in the ancestor. + # + # name - A Symbol corresponding to the hook method in the ancestor. + # + # Returns an Array of Symbols and Procs. + def ancestor_hooks(name) + superclass && superclass.respond_to?(name) ? superclass.send(name) : [] end end diff --git a/spec/interactor/hooks_spec.rb b/spec/interactor/hooks_spec.rb index e0a188b..afc9ce6 100644 --- a/spec/interactor/hooks_spec.rb +++ b/spec/interactor/hooks_spec.rb @@ -353,6 +353,110 @@ def add_after2 ]) end end + + context "with inheritance" do + context "with multiple ancestors" do + let(:ancestor_top) { + build_hooked do + around do |interactor| + steps << :around_before_ancestor_top + interactor.call + steps << :around_after_ancestor_top + end + + before do + steps << :before_ancestor_top + end + + after do + steps << :after_ancestor_top + end + end + } + + let(:ancestor) { + Class.new(ancestor_top) do + around do |interactor| + steps << :around_before_ancestor + interactor.call + steps << :around_after_ancestor + end + + before do + steps << :before_ancestor + end + + after do + steps << :after_ancestor + end + end + } + + let(:hooked) { + Class.new(ancestor) do + around do |interactor| + steps << :around_before + interactor.call + steps << :around_after + end + + before do + steps << :before + end + + after do + steps << :after + end + end + } + + it "runs hooks defined in ancestors" do + expect(hooked.process).to eq([ + :around_before, + :around_before_ancestor, + :around_before_ancestor_top, + :before, + :before_ancestor, + :before_ancestor_top, + :process, + :after_ancestor_top, + :after_ancestor, + :after, + :around_after_ancestor_top, + :around_after_ancestor, + :around_after + ]) + end + end + + describe "with hooks added to ancestors at runtime" do + let(:ancestor) { + build_hooked do + before do + steps << :before_at_parse + end + end + } + + let(:hooked) { + Class.new(ancestor) + } + + before do + ancestor.before do + steps << :before_at_runtime + end + end + + it "runs hooks defined in ancestors" do + expect(hooked.process).to eq([ + :before_at_parse, + :before_at_runtime, + :process + ]) + end + end + end end end end From c9ba0d3a9bea76136eb69504ab0e94426fa280e0 Mon Sep 17 00:00:00 2001 From: Kasper Meyer Date: Sat, 31 Oct 2020 16:55:07 +0100 Subject: [PATCH 2/3] Fix Standard violations --- lib/interactor/hooks.rb | 2 +- spec/integration_spec.rb | 54 +++++++++++++++++------------------ spec/interactor/hooks_spec.rb | 26 ++++++++--------- spec/spec_helper.rb | 2 +- 4 files changed, 42 insertions(+), 42 deletions(-) diff --git a/lib/interactor/hooks.rb b/lib/interactor/hooks.rb index 3c2369a..45747e7 100644 --- a/lib/interactor/hooks.rb +++ b/lib/interactor/hooks.rb @@ -318,7 +318,7 @@ def ancestor_after_hooks # # Returns an Array of Symbols and Procs. def ancestor_hooks(name) - superclass && superclass.respond_to?(name) ? superclass.send(name) : [] + superclass&.respond_to?(name) ? superclass.send(name) : [] end end diff --git a/spec/integration_spec.rb b/spec/integration_spec.rb index ca083f3..be6217f 100644 --- a/spec/integration_spec.rb +++ b/spec/integration_spec.rb @@ -308,7 +308,7 @@ def rollback :around_before4c, :before4c, :call4c, :after4c, :around_after4c, :after4, :around_after4, :around_before5, :before5, :call5, :after5, :around_after5, - :after, :around_after, + :after, :around_after ]) end end @@ -406,7 +406,7 @@ def rollback }.to change { context.steps }.from([]).to([ - :around_before, + :around_before ]) end end @@ -440,7 +440,7 @@ def rollback }.to change { context.steps }.from([]).to([ - :around_before, + :around_before ]) end @@ -497,7 +497,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end end @@ -551,7 +551,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end @@ -609,7 +609,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end end @@ -664,7 +664,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end @@ -717,7 +717,7 @@ def rollback :after2, :around_after2, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end end @@ -765,7 +765,7 @@ def rollback :after2, :around_after2, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end @@ -819,7 +819,7 @@ def rollback :around_before3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end end @@ -870,7 +870,7 @@ def rollback :around_before3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end @@ -924,7 +924,7 @@ def rollback :around_before3, :before3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end end @@ -975,7 +975,7 @@ def rollback :around_before3, :before3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end @@ -1030,7 +1030,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end end @@ -1082,7 +1082,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end @@ -1137,7 +1137,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end end @@ -1189,7 +1189,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end @@ -1247,7 +1247,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end end @@ -1300,7 +1300,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end @@ -1359,7 +1359,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end end @@ -1415,7 +1415,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end @@ -1474,7 +1474,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end end @@ -1530,7 +1530,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end @@ -1590,7 +1590,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end end @@ -1647,7 +1647,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end @@ -1707,7 +1707,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end end @@ -1764,7 +1764,7 @@ def rollback :rollback3, :rollback2c, :rollback2b, - :rollback2a, + :rollback2a ]) end diff --git a/spec/interactor/hooks_spec.rb b/spec/interactor/hooks_spec.rb index afc9ce6..0ce0782 100644 --- a/spec/interactor/hooks_spec.rb +++ b/spec/interactor/hooks_spec.rb @@ -43,7 +43,7 @@ def add_around_before_and_around_after(hooked) expect(hooked.process).to eq([ :around_before, :process, - :around_after, + :around_after ]) end end @@ -63,7 +63,7 @@ def add_around_before_and_around_after(hooked) expect(hooked.process).to eq([ :around_before, :process, - :around_after, + :around_after ]) end end @@ -93,7 +93,7 @@ def add_around_before1_and_around_after1(hooked) :around_before2, :process, :around_after2, - :around_after1, + :around_after1 ]) end end @@ -125,7 +125,7 @@ def add_around_before2_and_around_after2(hooked) :around_before2, :process, :around_after2, - :around_after1, + :around_after1 ]) end end @@ -146,7 +146,7 @@ def add_before it "runs the before hook method" do expect(hooked.process).to eq([ :before, - :process, + :process ]) end end @@ -163,7 +163,7 @@ def add_before it "runs the before hook block" do expect(hooked.process).to eq([ :before, - :process, + :process ]) end end @@ -187,7 +187,7 @@ def add_before1 expect(hooked.process).to eq([ :before1, :before2, - :process, + :process ]) end end @@ -213,7 +213,7 @@ def add_before2 expect(hooked.process).to eq([ :before1, :before2, - :process, + :process ]) end end @@ -234,7 +234,7 @@ def add_after it "runs the after hook method" do expect(hooked.process).to eq([ :process, - :after, + :after ]) end end @@ -251,7 +251,7 @@ def add_after it "runs the after hook block" do expect(hooked.process).to eq([ :process, - :after, + :after ]) end end @@ -275,7 +275,7 @@ def add_after1 expect(hooked.process).to eq([ :process, :after2, - :after1, + :after1 ]) end end @@ -301,7 +301,7 @@ def add_after2 expect(hooked.process).to eq([ :process, :after2, - :after1, + :after1 ]) end end @@ -349,7 +349,7 @@ def add_after2 :after2, :after1, :around_after2, - :around_after1, + :around_after1 ]) end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ead3a1c..8a6db02 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -5,4 +5,4 @@ require "interactor" -Dir[File.expand_path("../support/*.rb", __FILE__)].each { |f| require f } +Dir[File.expand_path("../support/*.rb", __FILE__)].sort.each { |f| require f } From 21bffd91474002864f51dffeaec7c1e4f8ba2143 Mon Sep 17 00:00:00 2001 From: Kasper Meyer Date: Sat, 31 Oct 2020 17:39:18 +0100 Subject: [PATCH 3/3] Slim down method footprint --- lib/interactor/hooks.rb | 80 ++++------------------------------------- 1 file changed, 7 insertions(+), 73 deletions(-) diff --git a/lib/interactor/hooks.rb b/lib/interactor/hooks.rb index 45747e7..353d531 100644 --- a/lib/interactor/hooks.rb +++ b/lib/interactor/hooks.rb @@ -144,7 +144,7 @@ def after(*hooks, &block) # # Returns an Array of Symbols and Procs. def around_hooks - internal_around_hooks + ancestor_around_hooks + internal_around_hooks + ancestor_hooks(:around_hooks) end # Internal: An Array of declared hooks to run before Interactor @@ -164,7 +164,7 @@ def around_hooks # # Returns an Array of Symbols and Procs. def before_hooks - internal_before_hooks + ancestor_before_hooks + internal_before_hooks + ancestor_hooks(:before_hooks) end # Internal: An Array of declared hooks to run after Interactor @@ -184,9 +184,11 @@ def before_hooks # # Returns an Array of Symbols and Procs. def after_hooks - ancestor_after_hooks + internal_after_hooks + ancestor_hooks(:after_hooks) + internal_after_hooks end + private + # Internal: An Array of declared hooks to run around Interactor # invocation. The hooks appear in the order in which they will be run. # @@ -244,81 +246,13 @@ def internal_after_hooks @internal_after_hooks ||= [] end - # Internal: An Array of around hooks declared in ancestors. - # The hooks appear in the order in which they will be run. - # - # Examples - # - # class MyParentInteractor - # include Interactor - # - # around :time_execution, :use_transaction - # end - # - # class MyChildInteractor < MyParentInteractor - # end - # - # MyChildInteractor.ancestor_around_hooks - # # => [:time_execution, :use_transaction] - # - # Returns an Array of Symbols and Procs. - def ancestor_around_hooks - ancestor_hooks(:around_hooks) - end - - # Internal: An Array of before hooks declared in ancestors. - # The hooks appear in the order in which they will be run. - # - # Examples - # - # class MyParentInteractor - # include Interactor - # - # before :set_finish_time, :say_goodbye - # end - # - # class MyChildInteractor < MyParentInteractor - # end - # - # MyChildInteractor.ancestor_before_hooks - # # => [:set_finish_time, :say_goodbye] - # - # Returns an Array of Symbols and Procs. - def ancestor_before_hooks - ancestor_hooks(:before_hooks) - end - - # Internal: An Array of after hooks declared in ancestors. - # The hooks appear in the order in which they will be run. - # - # Examples - # - # class MyParentInteractor - # include Interactor - # - # after :set_finish_time, :say_goodbye - # end - # - # class MyChildInteractor < MyParentInteractor - # end - # - # MyChildInteractor.ancestor_after_hooks - # # => [:say_goodbye, :set_finish_time] - # - # Returns an Array of Symbols and Procs. - def ancestor_after_hooks - ancestor_hooks(:after_hooks) - end - - private - # Internal: Fetches hooks declared in the ancestor. # # name - A Symbol corresponding to the hook method in the ancestor. # # Returns an Array of Symbols and Procs. - def ancestor_hooks(name) - superclass&.respond_to?(name) ? superclass.send(name) : [] + def ancestor_hooks(hook) + superclass&.respond_to?(hook) ? superclass.send(hook) : [] end end