diff options
Diffstat (limited to 'spec/lib/result_spec.rb')
-rw-r--r-- | spec/lib/result_spec.rb | 328 |
1 files changed, 328 insertions, 0 deletions
diff --git a/spec/lib/result_spec.rb b/spec/lib/result_spec.rb new file mode 100644 index 00000000000..2b88521fe14 --- /dev/null +++ b/spec/lib/result_spec.rb @@ -0,0 +1,328 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +# NOTE: +# This spec is intended to serve as documentation examples of idiomatic usage for the `Result` type. +# These examples can be executed as-is in a Rails console to see the results. +# +# To support this, we have intentionally used some `rubocop:disable` comments to allow for more +# explicit and readable examples. +# rubocop:disable RSpec/DescribedClass, Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration +RSpec.describe Result, feature_category: :remote_development do + describe 'usage of Result.ok and Result.err' do + context 'when checked with .ok? and .err?' do + it 'works with ok result' do + result = Result.ok(:success) + expect(result.ok?).to eq(true) + expect(result.err?).to eq(false) + expect(result.unwrap).to eq(:success) + end + + it 'works with error result' do + result = Result.err(:failure) + expect(result.err?).to eq(true) + expect(result.ok?).to eq(false) + expect(result.unwrap_err).to eq(:failure) + end + end + + context 'when checked with destructuring' do + it 'works with ok result' do + Result.ok(:success) => { ok: } # example of rightward assignment + expect(ok).to eq(:success) + + Result.ok(:success) => { ok: success_value } # rightward assignment destructuring to different var + expect(success_value).to eq(:success) + end + + it 'works with error result' do + Result.err(:failure) => { err: } + expect(err).to eq(:failure) + + Result.err(:failure) => { err: error_value } + expect(error_value).to eq(:failure) + end + end + + context 'when checked with pattern matching' do + def check_result_with_pattern_matching(result) + case result + in { ok: Symbol => ok_value } + { success: ok_value } + in { err: String => error_value } + { failure: error_value } + else + raise "Unmatched result type: #{result.unwrap.class.name}" + end + end + + it 'works with ok result' do + ok_result = Result.ok(:success_symbol) + expect(check_result_with_pattern_matching(ok_result)).to eq({ success: :success_symbol }) + end + + it 'works with error result' do + error_result = Result.err('failure string') + expect(check_result_with_pattern_matching(error_result)).to eq({ failure: 'failure string' }) + end + + it 'raises error with unmatched type in pattern match' do + unmatched_type_result = Result.ok([]) + expect do + check_result_with_pattern_matching(unmatched_type_result) + end.to raise_error(RuntimeError, 'Unmatched result type: Array') + end + + it 'raises error with invalid pattern matching key' do + result = Result.ok(:success) + expect do + case result + in { invalid_pattern_match_because_it_is_not_ok_or_err: :value } + :unreachable_from_case + else + :unreachable_from_else + end + end.to raise_error(ArgumentError, 'Use either :ok or :err for pattern matching') + end + end + end + + describe 'usage of #and_then' do + context 'when passed a proc' do + it 'returns last ok value in successful chain' do + initial_result = Result.ok(1) + final_result = + initial_result + .and_then(->(value) { Result.ok(value + 1) }) + .and_then(->(value) { Result.ok(value + 1) }) + + expect(final_result.ok?).to eq(true) + expect(final_result.unwrap).to eq(3) + end + + it 'short-circuits the rest of the chain on the first err value encountered' do + initial_result = Result.ok(1) + final_result = + initial_result + .and_then(->(value) { Result.err("invalid: #{value}") }) + .and_then(->(value) { Result.ok(value + 1) }) + + expect(final_result.err?).to eq(true) + expect(final_result.unwrap_err).to eq('invalid: 1') + end + end + + context 'when passed a module or class (singleton) method object' do + module MyModuleUsingResult + def self.double(value) + Result.ok(value * 2) + end + + def self.return_err(value) + Result.err("invalid: #{value}") + end + + class MyClassUsingResult + def self.triple(value) + Result.ok(value * 3) + end + end + end + + it 'returns last ok value in successful chain' do + initial_result = Result.ok(1) + final_result = + initial_result + .and_then(::MyModuleUsingResult.method(:double)) + .and_then(::MyModuleUsingResult::MyClassUsingResult.method(:triple)) + + expect(final_result.ok?).to eq(true) + expect(final_result.unwrap).to eq(6) + end + + it 'returns first err value in failed chain' do + initial_result = Result.ok(1) + final_result = + initial_result + .and_then(::MyModuleUsingResult.method(:double)) + .and_then(::MyModuleUsingResult::MyClassUsingResult.method(:triple)) + .and_then(::MyModuleUsingResult.method(:return_err)) + .and_then(::MyModuleUsingResult.method(:double)) + + expect(final_result.err?).to eq(true) + expect(final_result.unwrap_err).to eq('invalid: 6') + end + end + + describe 'type checking validation' do + describe 'enforcement of argument type' do + it 'raises TypeError if passed anything other than a lambda or singleton method object' do + ex = TypeError + msg = /expects a lambda or singleton method object/ + # noinspection RubyMismatchedArgumentType + expect { Result.ok(1).and_then('string') }.to raise_error(ex, msg) + expect { Result.ok(1).and_then(proc { Result.ok(1) }) }.to raise_error(ex, msg) + expect { Result.ok(1).and_then(1.method(:to_s)) }.to raise_error(ex, msg) + expect { Result.ok(1).and_then(Integer.method(:to_s)) }.to raise_error(ex, msg) + end + end + + describe 'enforcement of argument arity' do + it 'raises ArgumentError if passed lambda or singleton method object with an arity other than 1' do + expect do + Result.ok(1).and_then(->(a, b) { Result.ok(a + b) }) + end.to raise_error(ArgumentError, /expects .* with a single argument \(arity of 1\)/) + end + end + + describe 'enforcement that passed lambda or method returns a Result type' do + it 'raises ArgumentError if passed lambda or singleton method object which returns non-Result type' do + expect do + Result.ok(1).and_then(->(a) { a + 1 }) + end.to raise_error(TypeError, /expects .* which returns a 'Result' type/) + end + end + end + end + + describe 'usage of #map' do + context 'when passed a proc' do + it 'returns last ok value in successful chain' do + initial_result = Result.ok(1) + final_result = + initial_result + .map(->(value) { value + 1 }) + .map(->(value) { value + 1 }) + + expect(final_result.ok?).to eq(true) + expect(final_result.unwrap).to eq(3) + end + + it 'returns first err value in failed chain' do + initial_result = Result.ok(1) + final_result = + initial_result + .and_then(->(value) { Result.err("invalid: #{value}") }) + .map(->(value) { value + 1 }) + + expect(final_result.err?).to eq(true) + expect(final_result.unwrap_err).to eq('invalid: 1') + end + end + + context 'when passed a module or class (singleton) method object' do + module MyModuleNotUsingResult + def self.double(value) + value * 2 + end + + class MyClassNotUsingResult + def self.triple(value) + value * 3 + end + end + end + + it 'returns last ok value in successful chain' do + initial_result = Result.ok(1) + final_result = + initial_result + .map(::MyModuleNotUsingResult.method(:double)) + .map(::MyModuleNotUsingResult::MyClassNotUsingResult.method(:triple)) + + expect(final_result.ok?).to eq(true) + expect(final_result.unwrap).to eq(6) + end + + it 'returns first err value in failed chain' do + initial_result = Result.ok(1) + final_result = + initial_result + .map(::MyModuleNotUsingResult.method(:double)) + .and_then(->(value) { Result.err("invalid: #{value}") }) + .map(::MyModuleUsingResult.method(:double)) + + expect(final_result.err?).to eq(true) + expect(final_result.unwrap_err).to eq('invalid: 2') + end + end + + describe 'type checking validation' do + describe 'enforcement of argument type' do + it 'raises TypeError if passed anything other than a lambda or singleton method object' do + ex = TypeError + msg = /expects a lambda or singleton method object/ + # noinspection RubyMismatchedArgumentType + expect { Result.ok(1).map('string') }.to raise_error(ex, msg) + expect { Result.ok(1).map(proc { 1 }) }.to raise_error(ex, msg) + expect { Result.ok(1).map(1.method(:to_s)) }.to raise_error(ex, msg) + expect { Result.ok(1).map(Integer.method(:to_s)) }.to raise_error(ex, msg) + end + end + + describe 'enforcement of argument arity' do + it 'raises ArgumentError if passed lambda or singleton method object with an arity other than 1' do + expect do + Result.ok(1).map(->(a, b) { a + b }) + end.to raise_error(ArgumentError, /expects .* with a single argument \(arity of 1\)/) + end + end + + describe 'enforcement that passed lambda or method does not return a Result type' do + it 'raises TypeError if passed lambda or singleton method object which returns non-Result type' do + expect do + Result.ok(1).map(->(a) { Result.ok(a + 1) }) + end.to raise_error(TypeError, /expects .* which returns an unwrapped value, not a 'Result'/) + end + end + end + end + + describe '#unwrap' do + it 'returns wrapped value if ok' do + expect(Result.ok(1).unwrap).to eq(1) + end + + it 'raises error if err' do + expect { Result.err('error').unwrap }.to raise_error(RuntimeError, /called.*unwrap.*on an 'err' Result/i) + end + end + + describe '#unwrap_err' do + it 'returns wrapped value if err' do + expect(Result.err('error').unwrap_err).to eq('error') + end + + it 'raises error if ok' do + expect { Result.ok(1).unwrap_err }.to raise_error(RuntimeError, /called.*unwrap_err.*on an 'ok' Result/i) + end + end + + describe '#==' do + it 'implements equality' do + expect(Result.ok(1)).to eq(Result.ok(1)) + expect(Result.err('error')).to eq(Result.err('error')) + expect(Result.ok(1)).not_to eq(Result.ok(2)) + expect(Result.err('error')).not_to eq(Result.err('other error')) + expect(Result.ok(1)).not_to eq(Result.err(1)) + end + end + + describe 'validation' do + context 'for enforcing usage of only public interface' do + context 'when private constructor is called with invalid params' do + it 'raises ArgumentError if both ok_value and err_value are passed' do + expect { Result.new(ok_value: :ignored, err_value: :ignored) } + .to raise_error(ArgumentError, 'Do not directly use private constructor, use Result.ok or Result.err') + end + + it 'raises ArgumentError if neither ok_value nor err_value are passed' do + expect { Result.new } + .to raise_error(ArgumentError, 'Do not directly use private constructor, use Result.ok or Result.err') + end + end + end + end +end +# rubocop:enable RSpec/DescribedClass, Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration |