Last active
May 19, 2023 21:32
-
-
Save westonganger/aee11a627c266a1281462cf08f2ea3c5 to your computer and use it in GitHub Desktop.
Ruby Result objects (Success / Failure)
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
class Result | |
def initialize(h={}, **attrs) | |
if !h.is_a?(Hash) | |
raise ArgumentError.new("Non-hash argument provided") | |
end | |
@attrs = h.presence || attrs | |
@attrs.each do |k,v| | |
define_singleton_method(k) do | |
return v | |
end | |
end | |
self | |
end | |
def success? | |
raise NoMethodError.new("Method not defined on subclass") | |
end | |
def to_h | |
Marshal.restore(Marshal.dump(@attrs)) | |
end | |
alias_method :to_hash, :to_h | |
end | |
class Success < SafeResult | |
def success? | |
true | |
end | |
end | |
class Failure < SafeResult | |
def success? | |
false | |
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
require "spec_helper" | |
RSpec.describe Result do | |
it "can only call defined attributes/methods" do | |
result = Result.new(foo: "foo-val", bar: "bar-val") | |
expect(result.foo).to eq("foo-val") | |
expect(result.bar).to eq("bar-val") | |
expect { result.baz }.to raise_error(NoMethodError) | |
end | |
it "raises error is success? is not defined" do | |
result = Result.new | |
expect { result.success? }.to raise_error(NoMethodError) | |
end | |
it "defines methods only to the singleton" do | |
result_a = Result.new(foo: "foo-val") | |
result_b = Result.new(bar: "bar-val") | |
expect(result_a.respond_to?(:foo)).to eq(true) | |
expect(result_a.respond_to?(:bar)).to eq(false) | |
expect(result_b.respond_to?(:foo)).to eq(false) | |
expect(result_b.respond_to?(:bar)).to eq(true) | |
end | |
it "assigns values only to the singleton" do | |
result_a = Result.new(foo: "foo1") | |
result_b = Result.new(foo: "foo2") | |
expect(result_a.foo).to eq("foo1") | |
expect(result_b.foo).to eq("foo2") | |
end | |
it "allow attributes with matching string/symbol names and only returns value for the last defined name" do | |
result = Result.new(:foo => "sym-foo", "foo" => "str-foo") | |
expect(result.foo).to eq("str-foo") | |
result = Result.new("foo" => "str-foo", :foo => "sym-foo") | |
expect(result.foo).to eq("sym-foo") | |
end | |
context "initialize" do | |
it "allows named arguments" do | |
result = Result.new(:foo => "sym-foo", "foo" => "str-foo") | |
expect(result.foo).to eq("str-foo") | |
end | |
it "allows hash as positional param" do | |
result = Result.new({:foo => "sym-foo", "foo" => "str-foo"}) | |
expect(result.foo).to eq("str-foo") | |
end | |
it "errors on non-hash as positional param" do | |
expect { Result.new("str", foo: :bar) }.to raise_error(ArgumentError) | |
expect { Result.new([], foo: :bar) }.to raise_error(ArgumentError) | |
end | |
end | |
context "to_h" do | |
it "with no attributes" do | |
result = described_class.new | |
expect(result.to_h).to eq({}) | |
end | |
it "with attributes" do | |
result = described_class.new(foo: "foo-val", bar: "bar-val") | |
expect(result.to_h).to eq({:foo => "foo-val", :bar => "bar-val"}) | |
end | |
it "returns both string and symbol keys" do | |
result = described_class.new({"foo" => "foo-val", :bar => "bar-val"}) | |
expect(result.to_h).to eq({"foo" => "foo-val", :bar => "bar-val"}) | |
end | |
it "cannot modify the input hash via the output hash" do | |
input_h = {foo: "foo", bar: "bar"} | |
result = described_class.new(input_h) | |
h = result.to_h | |
h.slice!(:bar) | |
expect(input_h).to eq({:foo => "foo", :bar => "bar"}) | |
end | |
it "cannot modify the result object via the output hash" do | |
result = described_class.new(foo: "foo", bar: "bar") | |
h = result.to_h | |
h.slice!(:bar) | |
expect(result.foo).to eq("foo") | |
expect(result.bar).to eq("bar") | |
result = described_class.new(foo: "foo", errors: {:bar => "bar", :baz => "baz"}) | |
h = result.to_h | |
h[:errors].slice!(:baz) | |
expect(result.errors).to eq({:bar => "bar", :baz => "baz"}) | |
end | |
end | |
end | |
RSpec.describe Success do | |
it "inherits from Result" do | |
expect(described_class < Result).to eq(true) | |
end | |
context "success?" do | |
it "returns true" do | |
result = described_class.new | |
expect(result.success?).to eq(true) | |
end | |
end | |
end | |
RSpec.describe Failure do | |
it "inherits from Result" do | |
expect(described_class < Result).to eq(true) | |
end | |
context "success?" do | |
it "returns false" do | |
result = described_class.new | |
expect(result.success?).to eq(false) | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment