-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #14 from Kaligo/feature/result-monad-guard-clauses
Add guard clauses through new ResultMonad::GuardClause mixin
- Loading branch information
Showing
8 changed files
with
260 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
PATH | ||
remote: . | ||
specs: | ||
stimpack (0.6.0) | ||
stimpack (0.7.0) | ||
activesupport (~> 6.1) | ||
|
||
GEM | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
# frozen_string_literal: true | ||
|
||
module Stimpack | ||
module ResultMonad | ||
# This module adds a `#guard` method, which can be used inside a `#call` | ||
# to declare a step which, if it fails, breaks the flow of the method and | ||
# propagates the error result. | ||
# | ||
# Example: | ||
# | ||
# def call | ||
# guard :price_check | ||
# | ||
# ... | ||
# end | ||
# | ||
# In the above example, if the price check fails, the wrapping service | ||
# will halt and return an error result. This replaces use of `return`, and | ||
# has the benefit of invoking all related callbacks on *both* services if | ||
# the guard fails. | ||
# | ||
module GuardClause | ||
# This module prepends a wrapper `#call` method which "catches" errors | ||
# returned from the guarded service, and propagates the error result. | ||
# | ||
module GuardCatcher | ||
def call | ||
super | ||
rescue GuardFailed => e | ||
run_callback(:error) | ||
|
||
e.result | ||
end | ||
end | ||
|
||
# This error is used to break out of the current execution flow when a | ||
# guard fails. It carries the error result with it, and passes it to the | ||
# caller which can then work with it. | ||
# | ||
class GuardFailed < StandardError | ||
# rubocop:disable Lint/MissingSuper | ||
def initialize(result) | ||
@result = result | ||
end | ||
# rubocop:enable Lint/MissingSuper | ||
|
||
attr_reader :result | ||
end | ||
|
||
# The guard declaration takes either a label, a block, or both (in which | ||
# case the block takes precedence.) A label is interpreted as an instance | ||
# method of the service. | ||
# | ||
def guard(label = nil, &block) | ||
raise ArgumentError, "Guard needs either a label or a block." if label.nil? && !block_given? | ||
|
||
result = block_given? ? instance_eval(&block) : send(label) | ||
|
||
raise GuardFailed, result if result.failed? | ||
|
||
result | ||
end | ||
|
||
def self.included(klass) | ||
klass.prepend(GuardCatcher) | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
# frozen_string_literal: true | ||
|
||
module Stimpack | ||
VERSION = "0.6.1" | ||
VERSION = "0.7.0" | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
# frozen_string_literal: true | ||
|
||
require "stimpack/result_monad" | ||
|
||
RSpec.describe Stimpack::ResultMonad::GuardClause do | ||
subject(:service) { klass } | ||
|
||
let(:instance) { service.new } | ||
|
||
let(:klass) do | ||
Class.new do | ||
include Stimpack::ResultMonad | ||
|
||
result :foo | ||
|
||
def success_result(**options) | ||
success(**options) | ||
end | ||
|
||
def error_result(errors:) | ||
error(errors: errors) | ||
end | ||
|
||
def self.to_s | ||
"Foo" | ||
end | ||
|
||
def call | ||
guard :foo | ||
guard { bar } | ||
|
||
success(foo: "bar") | ||
end | ||
|
||
private | ||
|
||
def foo | ||
# Stubbed in test cases. | ||
end | ||
|
||
def bar | ||
# Stubbed in test cases. | ||
end | ||
end | ||
end | ||
|
||
let(:inner_service_error) do | ||
double( | ||
Stimpack::ResultMonad::Result, | ||
failed?: true, | ||
errors: ["Inner error"] | ||
) | ||
end | ||
|
||
let(:inner_service_success) do | ||
double( | ||
Stimpack::ResultMonad::Result, | ||
failed?: false, | ||
errors: [] | ||
) | ||
end | ||
|
||
describe "#guard" do | ||
context "when using a label" do | ||
it { expect { instance.guard(:foo) }.not_to raise_error(ArgumentError) } | ||
end | ||
|
||
context "when using a block" do | ||
it { expect { instance.guard { :foo } }.not_to raise_error(ArgumentError) } | ||
end | ||
|
||
context "when using both a label and a block" do | ||
it { expect { instance.guard(:foo) { :foo } }.not_to raise_error(ArgumentError) } | ||
end | ||
|
||
context "when using no arguments" do | ||
it { expect { instance.guard }.to raise_error(ArgumentError) } | ||
end | ||
end | ||
|
||
describe ".call" do | ||
context "when all guards pass" do | ||
before do | ||
allow(instance).to receive(:foo).and_return(inner_service_success) | ||
allow(instance).to receive(:bar).and_return(inner_service_success) | ||
end | ||
|
||
it { expect(instance.()).to be_successful } | ||
end | ||
|
||
context "when a guard fails" do | ||
context "when guard is invoked using a label" do | ||
before do | ||
allow(instance).to receive(:foo).and_return(inner_service_error) | ||
allow(instance).to receive(:bar).and_return(inner_service_success) | ||
end | ||
|
||
it { expect(instance.()).to be_failed } | ||
it { expect(instance.().errors).to eq(["Inner error"]) } | ||
end | ||
|
||
context "when guard is invoked using a block" do | ||
before do | ||
allow(instance).to receive(:foo).and_return(inner_service_success) | ||
allow(instance).to receive(:bar).and_return(inner_service_error) | ||
end | ||
|
||
it { expect(instance.()).to be_failed } | ||
it { expect(instance.().errors).to eq(["Inner error"]) } | ||
end | ||
end | ||
end | ||
|
||
describe ".before_error" do | ||
before do | ||
allow(instance).to receive(:inspect) | ||
allow(instance).to receive(:foo).and_return(inner_service_error) | ||
|
||
service.before_error { inspect } | ||
|
||
instance.() | ||
end | ||
|
||
it { expect(instance).to have_received(:inspect).once } | ||
end | ||
end |