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..353d531 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_hooks(: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_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. + # Includes hooks declared in ancestors. # # Examples # @@ -181,7 +184,75 @@ def before_hooks # # Returns an Array of Symbols and Procs. def after_hooks - @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. + # + # 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: 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(hook) + superclass&.respond_to?(hook) ? superclass.send(hook) : [] 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 e0a188b..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,10 +349,114 @@ def add_after2 :after2, :after1, :around_after2, - :around_after1, + :around_after1 ]) 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 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 }