From 82843f8c7283ee796406ba7ef7ac27145f458d3a Mon Sep 17 00:00:00 2001
From: Ted Johansson <drenmi@gmail.com>
Date: Fri, 12 Nov 2021 21:56:20 +0800
Subject: [PATCH] Make ResultMonad callbacks inherited

---
 CHANGELOG.md                       |  6 +++++
 README.md                          | 35 +++++++++++++++++++++++++--
 lib/stimpack/result_monad.rb       |  6 +++--
 lib/stimpack/version.rb            |  2 +-
 spec/stimpack/result_monad_spec.rb | 38 ++++++++++++++++++++++++------
 5 files changed, 75 insertions(+), 12 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 614b97e..70dcce6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Changelog
 
+## 0.9.0
+
+### New features
+
+- `ResultMonad` callbacks (`before_success`, `before_error`) are now inherited.
+
 ## 0.8.3
 
 ### New features
diff --git a/README.md b/README.md
index 4844e65..705210b 100644
--- a/README.md
+++ b/README.md
@@ -291,8 +291,8 @@ The `ResultMonad` mixin exposes two callbacks, `before_success` and
 `before_error`. These can be configured by passing a block to them in the
 class body.
 
-*Note: Callbacks are not inherited, and declaring multiple callbacks in the
-same class will overwrite the previous one.*
+*Note: Declaring an already declared callback in the same class will overwrite
+the previous one.*
 
 **Example:**
 
@@ -315,6 +315,37 @@ end
 *Note: The block is evaluated in the context of the instance, so you can call
 any instance methods from inside the block.*
 
+Callbacks are inherited, and all inherited callbacks will be invoked as they
+are traversed up the inheritance chain. In this case, all callbacks are
+evaluated in the context of the class where the `success` or `error` method
+was called.
+
+**Example:**
+
+```ruby
+class Foo
+  include Stimpack::ResultMonad
+
+  before_success do
+    puts "Parent"
+  end
+end
+
+class Bar < Foo
+  before_success do
+    puts "Child"
+  end
+
+  def call
+    success
+  end
+end
+
+Bar.()
+#=> "Child"
+#=> "Parent"
+```
+
 ### Guard clauses
 
 The `ResultMonad::GuardClause` mixin (included by default) allows for stepwise
diff --git a/lib/stimpack/result_monad.rb b/lib/stimpack/result_monad.rb
index f5a0325..5585c95 100644
--- a/lib/stimpack/result_monad.rb
+++ b/lib/stimpack/result_monad.rb
@@ -174,9 +174,11 @@ def incompatible_result_error(actual_attributes)
     end
 
     def run_callback(name)
-      callback = self.class.callbacks["#{self.class}.#{name}"]
+      self.class.ancestors.each do |ancestor|
+        callback = self.class.callbacks["#{ancestor}.#{name}"]
 
-      instance_exec(&callback) if callback.respond_to?(:call)
+        instance_exec(&callback) if callback.respond_to?(:call)
+      end
     end
   end
 end
diff --git a/lib/stimpack/version.rb b/lib/stimpack/version.rb
index b5241f8..3301ca1 100644
--- a/lib/stimpack/version.rb
+++ b/lib/stimpack/version.rb
@@ -1,5 +1,5 @@
 # frozen_string_literal: true
 
 module Stimpack
-  VERSION = "0.8.3"
+  VERSION = "0.9.0"
 end
diff --git a/spec/stimpack/result_monad_spec.rb b/spec/stimpack/result_monad_spec.rb
index e2d3843..cf988ad 100644
--- a/spec/stimpack/result_monad_spec.rb
+++ b/spec/stimpack/result_monad_spec.rb
@@ -5,10 +5,14 @@
 RSpec.describe Stimpack::ResultMonad do
   subject(:service) { klass }
 
-  let(:klass) do
+  let(:super_klass) do
     Class.new do
       include Stimpack::ResultMonad
+    end
+  end
 
+  let(:klass) do
+    Class.new(super_klass) do
       def success_result(**options)
         success(**options)
       end
@@ -17,6 +21,8 @@ def error_result(errors:)
         error(errors: errors)
       end
 
+      def accumulator; end
+
       def self.to_s
         "Foo"
       end
@@ -87,32 +93,50 @@ def self.to_s
 
   describe ".before_success" do
     let(:instance) { service.new }
+    let(:accumulator) { spy }
 
     before do
-      allow(instance).to receive(:inspect)
+      allow(instance).to receive(:accumulator).and_return(accumulator)
+
+      allow(accumulator).to receive(:callback)
+      allow(accumulator).to receive(:parent_callback)
 
       service.blank_result
-      service.before_success { inspect }
+      service.before_success { accumulator.callback }
+
+      super_klass.before_success { accumulator.parent_callback }
 
       instance.success_result
     end
 
-    it { expect(instance).to have_received(:inspect).once }
+    it "runs the callbacks up the class hierarchy" do
+      expect(accumulator).to have_received(:callback).ordered
+      expect(accumulator).to have_received(:parent_callback).ordered
+    end
   end
 
   describe ".before_error" do
     let(:instance) { service.new }
+    let(:accumulator) { spy }
 
     before do
-      allow(instance).to receive(:inspect)
+      allow(instance).to receive(:accumulator).and_return(accumulator)
+
+      allow(accumulator).to receive(:callback)
+      allow(accumulator).to receive(:parent_callback)
 
       service.blank_result
-      service.before_error { inspect }
+      service.before_error { accumulator.callback }
+
+      super_klass.before_error { accumulator.parent_callback }
 
       instance.error_result(errors: ["foo"])
     end
 
-    it { expect(instance).to have_received(:inspect).once }
+    it "runs the callbacks up the class hierarchy" do
+      expect(accumulator).to have_received(:callback).ordered
+      expect(accumulator).to have_received(:parent_callback).ordered
+    end
   end
 
   describe "#success" do