-
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.
- Loading branch information
Showing
6 changed files
with
302 additions
and
19 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
Gemfile.lock | ||
/.bundle/ | ||
/.yardoc | ||
/_yardoc/ | ||
|
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,5 +1,102 @@ | ||
require "safety_dance/version" | ||
|
||
module SafetyDance | ||
# Your code goes here... | ||
# Generic 'Result' object for declarative result success/failure/cascade handling. | ||
# | ||
# Usage: | ||
# | ||
# def some_action_that_succeeds(msg); msg; end | ||
# def some_action_that_fails(msg); raise msg; end | ||
# | ||
# SafetyDance.new { some_action_that_succeeds(:success) } #=> SafetyDance | ||
# | ||
# SafetyDance.new { some_action_that_succeeds(:success) } | ||
# .value {|error| "action failed with error #{error}" } #=> :success | ||
# | ||
# SafetyDance.new { some_action_that_fails("fail")} | ||
# .value {|error| "action failed with error #{error}" } #=> "action failed with error 'RuntimeError fail'" | ||
# | ||
# SafetyDance.new { some_action_that_succeeds(:success) } | ||
# .then { some_action_that_succeeds(:another_success } | ||
# .value {|error| "I am handling #{error}" } # => :another_success | ||
# | ||
# SafetyDance.new { some_action_that_fails("fail1") } | ||
# .then { some_action_that_fails("fail2") } | ||
# .then { some_action_that_succeeds(:another_success } | ||
# .then { some_action_that_fails("fail3") } | ||
# .value {|error| "I am handling #{error}" } # I am handling 'RuntimeError fail1'" | ||
# | ||
# Result object pattern is from https://johnnunemaker.com/resilience-in-ruby/ | ||
# e.g. https://github.com/github/github-ds/blob/fbda5389711edfb4c10b6c6bad19311dfcb1bac1/lib/github/result.rb | ||
class SafetyDance | ||
def initialize | ||
@value = yield | ||
@error = nil | ||
rescue => e | ||
@error = e | ||
end | ||
|
||
def ok? | ||
@error.nil? | ||
end | ||
|
||
def to_s | ||
if ok? | ||
"#<SafetyDance:0x%x value: %s>" % [object_id, @value.inspect] | ||
else | ||
"#<SafetyDance:0x%x error: %s>" % [object_id, @error.inspect] | ||
end | ||
end | ||
|
||
alias_method :inspect, :to_s | ||
|
||
def error | ||
@error | ||
end | ||
|
||
def value | ||
unless block_given? | ||
fail ArgumentError, "must provide a block to SafetyDance#value to be invoked in case of error" | ||
end | ||
if ok? | ||
@value | ||
else | ||
yield @error | ||
end | ||
end | ||
|
||
def value! | ||
if ok? | ||
@value | ||
else | ||
raise @error | ||
end | ||
end | ||
|
||
def then | ||
return self if !ok? | ||
SafetyDance.new { yield(@value) } | ||
end | ||
|
||
def then_tap | ||
self.then do |value| | ||
yield value | ||
value | ||
end | ||
end | ||
|
||
def rescue | ||
return self if ok? | ||
result = SafetyDance.new { yield(@error) } | ||
if result.ok? && result.value! == @error | ||
self | ||
else | ||
result | ||
end | ||
end | ||
|
||
def self.error(e) | ||
result = allocate | ||
result.instance_variable_set(:@error, e) | ||
result | ||
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,3 +1,3 @@ | ||
module SafetyDance | ||
VERSION = "0.1.0" | ||
class SafetyDance | ||
VERSION = "1.0.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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,15 +9,15 @@ Gem::Specification.new do |spec| | |
spec.authors = ["Benjamin Fleischer"] | ||
spec.email = ["[email protected]"] | ||
|
||
spec.summary = %q{TODO: Write a short summary, because RubyGems requires one.} | ||
spec.description = %q{TODO: Write a longer description or delete this line.} | ||
spec.homepage = "TODO: Put your gem's website or public repo URL here." | ||
spec.summary = %q{Response Objects pattern for resilient Ruby} | ||
spec.description = %q{SafetyDance.new { dance! }.then { |result| leave_friends_behind(!result) }.rescue { |error| not_friends_of_mine(error) }.value!} | ||
spec.homepage = "https://github.com/bf4/safety_dance" | ||
spec.license = "MIT" | ||
|
||
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' | ||
# to allow pushing to a single host or delete this section to allow pushing to any host. | ||
if spec.respond_to?(:metadata) | ||
spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'" | ||
spec.metadata["allowed_push_host"] = "https://rubygems.org" | ||
else | ||
raise "RubyGems 2.0 or newer is required to protect against " \ | ||
"public gem pushes." | ||
|
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,9 +1,117 @@ | ||
RSpec.describe SafetyDance do | ||
it "has a version number" do | ||
expect(SafetyDance::VERSION).not_to be nil | ||
|
||
def some_action_that_succeeds(msg); msg; end | ||
def some_action_that_fails(msg); raise msg; end | ||
|
||
it "returns a result object" do | ||
result = SafetyDance.new { some_action_that_succeeds(:success) } | ||
expect(result).to be_a(SafetyDance) | ||
end | ||
|
||
specify "inspecting a result object shows its value" do | ||
result = SafetyDance.new { some_action_that_succeeds(:success) } | ||
hex_id = format("%x", result.object_id) | ||
expect(result.to_s).to eq("#<SafetyDance:0x#{hex_id} value: :success>") | ||
expect(result.inspect).to eq("#<SafetyDance:0x#{hex_id} value: :success>") | ||
end | ||
|
||
specify "inspecting a result object shows its error" do | ||
result = SafetyDance.new { some_action_that_fails("omg") } | ||
hex_id = format("%x", result.object_id) | ||
expect(result.to_s).to eq("#<SafetyDance:0x#{hex_id} error: #<RuntimeError: omg>>") | ||
expect(result.inspect).to eq("#<SafetyDance:0x#{hex_id} error: #<RuntimeError: omg>>") | ||
end | ||
|
||
specify "a successful result is ok?" do | ||
result = SafetyDance.new { some_action_that_succeeds(:success) } | ||
expect(result).to be_ok | ||
end | ||
|
||
specify "a failed result is not ok?" do | ||
result = SafetyDance.new { some_action_that_fails("failed") } | ||
expect(result).not_to be_ok | ||
end | ||
|
||
specify "SafetyDance#value returns the value of the successful operation" do | ||
result = SafetyDance.new { some_action_that_succeeds(:success) } | ||
.value {|error| "action failed with error #{error}" } | ||
expect(result).to eq(:success) | ||
end | ||
|
||
specify "SafetyDance#value yields the error to the block on failed operation" do | ||
result = SafetyDance.new { some_action_that_fails("fail") } | ||
.value {|error| "action failed with error #{error}" } | ||
expect(result).to eq("action failed with error fail") | ||
end | ||
|
||
specify "SafetyDance#value! returns the value of the successful operation" do | ||
result = SafetyDance.new { some_action_that_succeeds(:success) } | ||
.value! | ||
expect(result).to eq(:success) | ||
end | ||
|
||
specify "SafetyDance#value! raises the error" do | ||
expect { | ||
SafetyDance.new { some_action_that_fails("fail") }.value! | ||
}.to raise_error(RuntimeError) {|e| | ||
expect(e.message).to eq("fail") | ||
} | ||
end | ||
|
||
specify "SafetyDance#then is executed when the previous result is ok" do | ||
result = SafetyDance.new { some_action_that_succeeds(:success) } | ||
.then { some_action_that_succeeds(:another_success) } | ||
.value {|error| "I am handling #{error}" } | ||
expect(result).to eq(:another_success) | ||
end | ||
|
||
specify "SafetyDance#then returns the first failed result at the end of a result chain" do | ||
result = SafetyDance.new { some_action_that_fails("first failure") } | ||
.then { some_action_that_fails("never reached failure") } | ||
.then { some_action_that_succeeds(:never_reached_success) } | ||
.then { some_action_that_fails("another never reached failure") } | ||
.value {|error| "I am handling #{error}" } | ||
expect(result).to eq("I am handling first failure") | ||
end | ||
|
||
specify "SafetyDance#rescue allows recovery from an error" do | ||
result = SafetyDance.new { some_action_that_fails("fail") } | ||
.rescue {|error| some_action_that_succeeds("Recovered from '#{error}'") } | ||
expect(result).to be_ok | ||
expect(result.value!).to eq("Recovered from 'fail'") | ||
end | ||
|
||
specify "SafetyDance#rescue is executed when the previous result is an error" do | ||
result = SafetyDance.new { some_action_that_succeeds("success") } | ||
.rescue {|error| some_action_that_succeeds("Not executed after success: '#{error}'") } | ||
.then { some_action_that_fails("failure") } | ||
.rescue {|error| some_action_that_succeeds("Recovered from '#{error}'") } | ||
expect(result).to be_ok | ||
expect(result.value!).to eq("Recovered from 'failure'") | ||
end | ||
|
||
specify "SafetyDance#rescue can itself fail and be rescued" do | ||
result = SafetyDance.new { some_action_that_fails("failure") } | ||
.rescue {|error| some_action_that_fails("Recover of '#{error}' failed") } | ||
.then { some_action_that_succeeds("unreached success") } | ||
.then { some_action_that_fails("unreached failure") } | ||
.rescue {|error| some_action_that_succeeds("Recovered from '#{error}'") } | ||
expect(result).to be_ok | ||
expect(result.value!).to eq("Recovered from 'Recover of 'failure' failed'") | ||
end | ||
|
||
it "does something useful" do | ||
expect(false).to eq(true) | ||
specify "SafetyDance#rescue can no-op by returning the yielded error" do | ||
result = SafetyDance.new { some_action_that_fails("failure is passed through") } | ||
.rescue {|error| | ||
case error | ||
when RuntimeError then error | ||
else some_action_that_succeeds("not reached") | ||
end | ||
} | ||
.then { some_action_that_succeeds("unreached success") } | ||
.then { some_action_that_fails("unreached failure") } | ||
expect(result).not_to be_ok | ||
hex_id = format("%x", result.object_id) | ||
expect(result.to_s).to eq("#<SafetyDance:0x#{hex_id} error: #<RuntimeError: failure is passed through>>") | ||
end | ||
end |