You need to sign in or sign up before continuing.
Select Git revision
-
Furkan Ayhan authored
Using .gitlab-ci.yml as the default CI Config is distributed across the codebase. In this MR, we are simplifying it for further improvements
Furkan Ayhan authoredUsing .gitlab-ci.yml as the default CI Config is distributed across the codebase. In this MR, we are simplifying it for further improvements
237
result_spec.rb 11.58 KiB
# frozen_string_literal: true
require '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