Class: Errgonomic::Result::Any

Inherits:
Object
  • Object
show all
Includes:
Comparable
Defined in:
lib/errgonomic/result.rb

Overview

The base class for Result's Ok and Err class variants. We implement as much logic as possible here, including construction, and let Ok and Err handle only their self identification.

Direct Known Subclasses

Err, Ok

Constant Summary collapse

RUST_SPELLINGS =

Rust spellings we accept but do not advertise: they delegate to the Ruby-idiomatic predicate and nudge the caller there via stderr.

{
  is_ok: :ok?,
  is_err: :err?,
  is_ok_and: :ok_and?,
  is_err_and: :err_and?
}.freeze

Instance Method Summary collapse

Constructor Details

#initialize(value) ⇒ Any

A Result is a value, not a slot: the inner value is reached through a combinator that handles the other variant, and nothing swaps it out from under another reference.

Examples:

begin
  Ok(1).value
rescue NoMethodError => e
  e.class
end # => Errgonomic::UnwrappedAccessError
begin
  Err(:x).value = :y
rescue NoMethodError => e
  e.class
end # => Errgonomic::UnwrappedAccessError
Ok(1).respond_to?(:value) # => false
Ok(1).frozen? # => true
Err(:x).frozen? # => true


31
32
33
34
# File 'lib/errgonomic/result.rb', line 31

def initialize(value)
  @value = value
  freeze
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(name, *args, &block) ⇒ Object

A Result deliberately forwards nothing to its inner value, so a miss here is almost always someone treating the container as its contents. Teach the route out instead of leaving a bare NoMethodError. Rust spellings of the predicates delegate, with a nudge on stderr.

Examples:

begin
  Ok(5) + 1
rescue NoMethodError => e
  e.class
end # => Errgonomic::UnwrappedAccessError
Ok(5).respond_to?(:+) # => false
Ok(1).is_ok_and(&:odd?) # => true
Err(:a).is_err # => true
Ok(1).respond_to?(:is_ok) # => true

Raises:



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/errgonomic/result.rb', line 90

def method_missing(name, *args, &block)
  if (canonical = RUST_SPELLINGS[name])
    warn "Errgonomic: `#{name}` is the Rust spelling; prefer `#{canonical}`. Delegating."
    return public_send(canonical, *args, &block)
  end

  raise Errgonomic::UnwrappedAccessError.new(<<~MSG, name)
    undefined method `#{name}' for #{inspect}, a Result, which does not forward methods to its inner value.
    Reach for a combinator instead:
      map, map_err, and_then, or_else: transform the value or the error
      unwrap_or, unwrap_or_else: supply a fallback
      ok_and?, err_and?: test a predicate against the inner value
    unwrap!, unwrap_err!, and expect! also exist, but are intended for tests rather than application code.
  MSG
end

Instance Method Details

#!=(other) ⇒ Object

Ruby derives != from ==, so a strict-equality message would name the operator the caller did not write.



174
175
176
177
# File 'lib/errgonomic/result.rb', line 174

def !=(other)
  strict_equality!(other, '!=')
  super
end

#<=>(other) ⇒ Object

Results order like Rust's: Ok sorts before any Err, and same variants order by their inner values. Two Results whose inner values do not themselves compare follow Ruby's convention and answer nil. A non-Result operand raises instead: Comparable turns a nil here into an ArgumentError that names the Result as the operand at fault, where what went wrong is that a wrapper was ordered against a bare value.

Examples:

(Ok(1) <=> Ok(2)) # => -1
(Ok(1) <=> Err(:a)) # => -1
(Err(:a) <=> Ok(1)) # => 1
(Err(:a) <=> Err(:b)) # => -1
[Err(:a), Ok(2), Ok(1)].sort # => [Ok(1), Ok(2), Err(:a)]

a bare value is not ordered against a Result

Ok(1) <= 2 # => raise Errgonomic::TypeMismatchError, "cannot compare Ok(1) with Integer; test the inner value (ok_and? { |v| v <= other }) or reach for it (map, unwrap_or)"
Ok(1).ok_and? { |v| v <= 2 } # => true
Ok(1).map { |v| v <= 2 } # => Ok(true)


54
55
56
57
58
59
60
61
62
63
64
# File 'lib/errgonomic/result.rb', line 54

def <=>(other)
  unless other.is_a?(Errgonomic::Result::Any)
    raise Errgonomic::TypeMismatchError,
          "cannot compare #{inspect} with #{other.class}; test the inner value " \
          '(ok_and? { |v| v <= other }) or reach for it (map, unwrap_or)'
  end

  return ok? ? -1 : 1 if self.class != other.class

  value <=> other.value
end

#==(other) ⇒ Object

A Result equals another Result of the same variant with an equal inner value. Comparing it with anything that is not a Result raises Errgonomic::TypeMismatchError, on the terms Option#== states: the raise reaches ==, !=, eql? and ===, hashing stays quiet, and a bare value on the left answers for itself.

Examples:

Ok(1) == Ok(1) # => true
Ok(1) == Err(1) # => false
Ok(1).object_id == Ok(1).object_id # => false

a cross-type comparison is an error, never a quiet false

Ok(1) == 1 # => raise Errgonomic::TypeMismatchError, "Errgonomic::Result::Ok == Integer, which strict equality refuses.\nCompare Results (res == Ok(1)), test the inner value (res.ok_and? { |v| v == 1 }), or unwrap_or a fallback first."
Ok(1) != 1 # => raise Errgonomic::TypeMismatchError, "Errgonomic::Result::Ok != Integer, which strict equality refuses.\nCompare Results (res == Ok(1)), test the inner value (res.ok_and? { |v| v == 1 }), or unwrap_or a fallback first."
Ok(1) === 1 # => raise Errgonomic::TypeMismatchError, "Errgonomic::Result::Ok === Integer, which strict equality refuses.\nCompare Results (res == Ok(1)), test the inner value (res.ok_and? { |v| v == 1 }), or unwrap_or a fallback first."
Err(:x) == nil # => raise Errgonomic::TypeMismatchError, "Errgonomic::Result::Err == NilClass, which strict equality refuses.\nCompare Results (res == Ok(nil)), test the inner value (res.ok_and? { |v| v == nil }), or unwrap_or a fallback first."
Ok(1) === Ok(1) # => true
begin
  [Ok(1)].include?(1)
rescue Errgonomic::TypeMismatchError => e
  e.class
end # => Errgonomic::TypeMismatchError
{ Ok(1) => :v }[1] # => nil
nil == Err(:x) # => false

an Option is another container, not another Result

Ok(1) == Some(1) # => raise Errgonomic::TypeMismatchError, "Errgonomic::Result::Ok == Errgonomic::Option::Some, which strict equality refuses.\nA Result and an Option are different containers, and neither is the other.\nUnwrap the one you meant (res.unwrap_or(nil) == opt.unwrap_or(nil))."

Parameters:



139
140
141
142
143
144
# File 'lib/errgonomic/result.rb', line 139

def ==(other)
  strict_equality!(other, '==')
  return false if self.class != other.class

  value == other.value
end

#===(other) ⇒ Object

Object#=== is ==, so a case value when Ok(1) and a pinned pattern reach the same check, named for the operator that was written.



148
149
150
151
# File 'lib/errgonomic/result.rb', line 148

def ===(other)
  strict_equality!(other, '===')
  self == other
end

#and(other) ⇒ Object

Given another result, return it if the inner result is Ok, else return the inner Err. Raise an exception if the other value is not a Result.

Examples:

Ok(1).and(Ok(2)) # => Ok(2)
Ok(1).and(Err(:f)) # => Err(:f)
Err(:g).and(Ok(1)) # => Err(:g)
Err(:h).and(Err(:i)) # => Err(:h)
Ok(1).and(2) # => raise Errgonomic::ArgumentError, "other must be a Result"

Parameters:

Raises:



282
283
284
285
286
287
# File 'lib/errgonomic/result.rb', line 282

def and(other)
  raise Errgonomic::ArgumentError, 'other must be a Result' unless other.is_a?(Errgonomic::Result::Any)
  return self if err?

  other
end

#and_then(&block) ⇒ Object

Given a block, evaluate it and return its result if the inner result is Ok, else return the inner Err. This is lazy evaluated, and we pedantically check the type of the block's return value at runtime. This is annoying, sorry, but better than an "undefined method" error. Hopefully it gives your test suite a chance to detect incorrect usage.

Examples:

Ok(1).and_then { |x| Ok(x + 1) } # => Ok(2)
Ok(1).and_then { |_| Err(:error) } # => Err(:error)
Err(:error).and_then { |x| Ok(x + 1) } # => Err(:error)
Err(:error).and_then { |x| Err(:error2) } # => Err(:error)

Parameters:

  • block (Proc)


302
303
304
305
306
307
308
309
310
311
# File 'lib/errgonomic/result.rb', line 302

def and_then(&block)
  return self if err?

  res = block.call(value)
  if !res.is_a?(Errgonomic::Result::Any) && !Errgonomic.give_me_ambiguous_downstream_errors?
    raise Errgonomic::ArgumentError, 'and_then block must return a Result'
  end

  res
end

#as_json(*_args) ⇒ Object

ActiveSupport's Hash#as_json and Array#as_json recurse through their members with as_json rather than to_json, so a Result nested in a payload reaches Object#as_json and serializes as its instance variables. Refuse there too, and the guard holds wherever a Result travels.



460
461
462
# File 'lib/errgonomic/result.rb', line 460

def as_json(*_args)
  raise Errgonomic::SerializeError, 'cannot serialize an unwrapped Result'
end

#deconstructObject

The Rust shape: each variant deconstructs to its one payload, so in Ok(v) binds the value and in Err(e) binds the error.

Examples:

Ok(1).deconstruct # => [1]
Err(:x).deconstruct # => [:x]
Ok(1).respond_to?(:deconstruct_keys) # => false

every Err carries an error, so in Err() matches none of them

case Err(:x)
in Err() then :empty
in Err(e) then e
end # => :x
case Err(:x)
in Err then :any_err
end # => :any_err
case Err(:x)
in Err(_) then :any_err
end # => :any_err

an Err nested in an Ok matches through the Ok's payload

case Ok(Err(:boom))
in Ok(Err(e)) then e
end # => :boom
case Ok(Err(:boom))
in Ok(Err()) then :empty
in Ok(Err(_)) then :err_inside
end # => :err_inside
case Ok(Err(:boom))
in Ok(Ok(value)) then value
in Ok(Err) then :err_inside
end # => :err_inside

a two-branch case/in with no else is exhaustive

case Ok(1)
in Ok(value)
  "Measurement is #{value}"
in Err(err)
  "Measurement is not available"
end # => "Measurement is 1"

the wrong type falls through to Ruby's own exhaustiveness check

begin
  case :done
  in Ok(value) then value
  in Err(err) then err
  end
rescue NoMatchingPatternError => e
  [e.class, e.message]
end # => [NoMatchingPatternError, "done"]

an Option that falls through carries a message that refuses to print

begin
  case Some(1)
  in Ok(value) then value
  in Err(err) then err
  end
rescue NoMatchingPatternError => e
  [e.class, (e.message rescue $!.class)]
end # => [NoMatchingPatternError, Errgonomic::SerializeError]

a pattern reaches the kind of value inside the variant

result = Err(StandardError.new("nope"))
case result
in Ok(value)
  "Measurement is #{value}"
in Err(String => msg)
  "Measurement failed with a message: #{msg}"
in Err(Exception => e)
  "Measurement produced an exception -- #{e.class}: #{e}"
end # => "Measurement produced an exception -- StandardError: nope"


541
542
543
# File 'lib/errgonomic/result.rb', line 541

def deconstruct
  [value]
end

#eql?(other) ⇒ Boolean

Hash-based collections (Hash keys, Set, uniq, group_by) use eql? and hash, not ==. Follow the inner value's own eql? semantics, so Results behave as keys exactly like their inner values.

Examples:

Ok(5).eql?(Ok(5)) # => true
Ok(1).eql?(Ok(1.0)) # => false
Ok(1).eql?(Err(1)) # => false
{ Ok(5) => 1 }[Ok(5)] # => 1
[Err(:a), Err(:a)].uniq # => [Err(:a)]

a cross-type eql? raises as == does, and hash is untouched

Ok(5).eql?(5) # => raise Errgonomic::TypeMismatchError, "Errgonomic::Result::Ok eql? Integer, which strict equality refuses.\nCompare Results (res == Ok(5)), test the inner value (res.ok_and? { |v| v == 5 }), or unwrap_or a fallback first."
Ok(5).hash == Ok(5).hash # => true

Returns:

  • (Boolean)


167
168
169
170
# File 'lib/errgonomic/result.rb', line 167

def eql?(other)
  strict_equality!(other, 'eql?')
  self.class == other.class && value.eql?(other.value)
end

#err_and?(&block) ⇒ Boolean

Return true if the inner value is an Err and the result of the block is truthy.

Examples:

Ok(1).err_and?(&:odd?) # => false
Ok(1).err_and?(&:even?) # => false
Err(:a).err_and? { |_| true } # => true
Err(:b).err_and? { |_| false } # => false

Returns:

  • (Boolean)


221
222
223
224
225
226
227
# File 'lib/errgonomic/result.rb', line 221

def err_and?(&block)
  if err?
    !!block.call(value)
  else
    false
  end
end

#expect!(msg = nil, &block) ⇒ Object

Return the inner value of an Ok, else raise an exception with the given message when Err. A block is called only on the Err branch, so a message that interpolates costs nothing on the path that succeeds.

Examples:

Ok(1).expect!("should have worked") # => 1
Err(:d).expect!("should have worked") # => raise Errgonomic::ExpectError, "should have worked"
Err(:d).expect! { "no rate for #{7}" } # => raise Errgonomic::ExpectError, "no rate for 7"

Parameters:

  • msg (String) (defaults to: nil)

Raises:



250
251
252
253
254
# File 'lib/errgonomic/result.rb', line 250

def expect!(msg = nil, &block)
  raise Errgonomic::ExpectError, block ? block.call : msg unless ok?

  @value
end

#hashObject

Examples:

Ok(5).hash == Ok(5).hash # => true
Ok(5).hash == Err(5).hash # => false


182
183
184
# File 'lib/errgonomic/result.rb', line 182

def hash
  [self.class, value].hash
end

#map(&block) ⇒ Object

Map the Ok(a) to an Ok(b), preserving the Err

Examples:

Err(:broken).map { |_val| :nominal } # => Err(:broken)
Ok(:plausible).map { |_val| :success } # => Ok(:success)


408
409
410
411
412
# File 'lib/errgonomic/result.rb', line 408

def map(&block)
  return self if err?

  Ok(block.call(value))
end

#map_err(&block) ⇒ Object

Map the Err(e) to an Err(f), preserving the Ok

Examples:

Ok(:Alice).map_err { |_e| :Bob } # => Ok(:Alice)
Err(:bob).map_err { |e| e.capitalize } # => Err(:Bob)


419
420
421
422
423
# File 'lib/errgonomic/result.rb', line 419

def map_err(&block)
  return self if ok?

  Err(block.call(value))
end

#ok_and?(&block) ⇒ Boolean

Return true if the inner value is an Ok and the result of the block is truthy.

Examples:

Ok(1).ok_and?(&:odd?) # => true
Ok(1).ok_and?(&:even?) # => false
Err(:a).ok_and? { |_| true } # => false
Err(:b).ok_and? { |_| false } # => false

Parameters:

  • block (Proc)

    The block to evaluate if the inner value is an Ok.

Returns:

  • (Boolean)


207
208
209
210
211
# File 'lib/errgonomic/result.rb', line 207

def ok_and?(&block)
  return false if err?

  !!block.call(value)
end

#or(other) ⇒ Object

Return other if self is Err, else return the original Option. Raises a pedantic runtime exception if other is not a Result.

Examples:

Err(:j).or(Ok(1)) # => Ok(1)
Err(:k).or(Err(:l)) # => Err(:l)
Err(:m).or("oops") # => raise Errgonomic::ArgumentError, "other must be a Result; you might want unwrap_or"

Parameters:



322
323
324
325
326
327
328
329
330
# File 'lib/errgonomic/result.rb', line 322

def or(other)
  unless other.is_a?(Errgonomic::Result::Any)
    raise Errgonomic::ArgumentError,
          'other must be a Result; you might want unwrap_or'
  end
  return other if err?

  self
end

#or_else(&block) ⇒ Object

Return self if it is Ok, else lazy evaluate the block and return its result. Raises a pedantic runtime check that the block returns a Result. Sorry about that, hopefully it helps your tests. Better than ambiguous downstream "undefined method" errors, probably.

Examples:

Ok(1).or_else { |e| Ok(2) } # => Ok(1)
Err(:o).or_else { |e| Ok(1) } # => Ok(1)
Err(:q).or_else { |e| Err(:r) } # => Err(:r)
Err(:s).or_else { |e| "oops" } # => raise Errgonomic::ArgumentError, "or_else block must return a Result"

Parameters:

  • block (Proc)


344
345
346
347
348
349
350
351
352
353
# File 'lib/errgonomic/result.rb', line 344

def or_else(&block)
  return self if ok?

  res = block.call(value)
  if !res.is_a?(Errgonomic::Result::Any) && !Errgonomic.give_me_ambiguous_downstream_errors?
    raise Errgonomic::ArgumentError, 'or_else block must return a Result'
  end

  res
end

#pretty_print(pp) ⇒ Object

pp uses its own object dump unless told otherwise; keep it consistent with inspect.



466
467
468
# File 'lib/errgonomic/result.rb', line 466

def pretty_print(pp)
  pp.text(inspect)
end

#respond_to_missing?(name, include_private = false) ⇒ Boolean

Returns:

  • (Boolean)


106
107
108
# File 'lib/errgonomic/result.rb', line 106

def respond_to_missing?(name, include_private = false)
  RUST_SPELLINGS.key?(name) || super
end

#result?Boolean

Indicate that this is some kind of result object. Contrast to Object#result? which is false for all other types.

Examples:

Ok("a").result? # => true
Err("a").result? # => true
"a".result? # => false

Returns:

  • (Boolean)


193
194
195
# File 'lib/errgonomic/result.rb', line 193

def result?
  true
end

#tap_err(&block) ⇒ Object

Calls the function with the inner error value, if Err, but returns the original Result.

Examples:

tapped = false
Ok(1).tap_err { |err| tapped = err } # => Ok(1)
tapped # => false
Err(:nope).tap_err { |err| tapped = err } # => Err(:nope)
tapped # => :nope


391
392
393
394
# File 'lib/errgonomic/result.rb', line 391

def tap_err(&block)
  block.call(value) if err?
  self
end

#tap_ok(&block) ⇒ Object

Calls the function with the inner ok value, if Ok, while returning the original Result.



398
399
400
401
# File 'lib/errgonomic/result.rb', line 398

def tap_ok(&block)
  block.call(value) if ok?
  self
end

#to_json(*_args) ⇒ Object

Refuse to serialize an unwrapped Result as JSON. Not only should we require that Results be correctly handled to access their inner value, but without this we will get undefined structures from default Object#to_json implementations.

Examples:

Ok("").to_json # => raise Errgonomic::SerializeError, "cannot serialize an unwrapped Result"
Err("").to_json # => raise Errgonomic::SerializeError, "cannot serialize an unwrapped Result"

Raises:



451
452
453
# File 'lib/errgonomic/result.rb', line 451

def to_json(*_args)
  raise Errgonomic::SerializeError, 'cannot serialize an unwrapped Result'
end

#to_sObject

Refuse to render as a String. Rust gives Result a Debug and no Display: a wrapper that reaches a string went unhandled, and a string is where it turns into data, a hostname, a hash key or a page. The refusal names the value and says how to log it or take it.

Examples:

Ok(1).to_s # => raise Errgonomic::SerializeError, "Ok(1) refuses to_s; use inspect for a log line, or unwrap_or / expect! for the value"
Err(:nope).to_s # => raise Errgonomic::SerializeError, "Err(:nope) refuses to_s; use inspect for a log line, or unwrap_or / expect! for the value"
"outcome: #{Ok(1)}" # => raise Errgonomic::SerializeError, "Ok(1) refuses to_s; use inspect for a log line, or unwrap_or / expect! for the value"
[Ok(1), Err(:x)].join(",") # => raise Errgonomic::SerializeError, "Ok(1) refuses to_s; use inspect for a log line, or unwrap_or / expect! for the value"
format("%s", Err(:x)) # => raise Errgonomic::SerializeError, "Err(:x) refuses to_s; use inspect for a log line, or unwrap_or / expect! for the value"
String(Ok(1)) # => raise Errgonomic::SerializeError, "Ok(1) refuses to_s; use inspect for a log line, or unwrap_or / expect! for the value"
Err("a" * 100).to_s # => raise Errgonomic::SerializeError, "Err(\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa... refuses to_s; use inspect for a log line, or unwrap_or / expect! for the value"
Err(:nope).inspect # => "Err(:nope)"

Raises:



439
440
441
# File 'lib/errgonomic/result.rb', line 439

def to_s
  raise Errgonomic::SerializeError, to_s_refusal
end

#unwrap!Object

Return the inner value of an Ok, else raise an exception when Err.

Examples:

Ok(1).unwrap! # => 1
Err(:c).unwrap! # => raise Errgonomic::UnwrapError.new("value is an Err", :c)

Raises:



234
235
236
237
238
# File 'lib/errgonomic/result.rb', line 234

def unwrap!
  raise Errgonomic::UnwrapError.new('value is an Err', @value) unless ok?

  @value
end

#unwrap_err!Object

Return the inner value of an Err, else raise an exception when Ok. The message is the Ok's value as inspect renders it, bounded, so an Ok holding an Option or a Result still has a message to print.

Examples:

Ok(1).unwrap_err! # => raise Errgonomic::UnwrapError, "1"
Ok(Some(1)).unwrap_err! # => raise Errgonomic::UnwrapError, "Some(1)"
Ok("a" * 100).unwrap_err! # => raise Errgonomic::UnwrapError, "\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa..."
Err(:e).unwrap_err! # => :e

Raises:



265
266
267
268
269
# File 'lib/errgonomic/result.rb', line 265

def unwrap_err!
  raise Errgonomic::UnwrapError.new(bounded_inspect(value), value) unless err?

  @value
end

#unwrap_or(other) ⇒ Object

Return the inner value if self is Ok, else return the provided default.

Examples:

Ok(1).unwrap_or(2) # => 1
Err(:t).unwrap_or(:u) # => :u

Parameters:



362
363
364
365
366
# File 'lib/errgonomic/result.rb', line 362

def unwrap_or(other)
  return value if ok?

  other
end

#unwrap_or_else(&block) ⇒ Object

Return the inner value if self is Ok, else lazy evaluate the block and return its result.

Examples:

Ok(1).unwrap_or_else { 2 } # => 1
Err("foo").unwrap_or_else { |s| s.length } # => 3

Parameters:

  • block (Proc)


376
377
378
379
380
# File 'lib/errgonomic/result.rb', line 376

def unwrap_or_else(&block)
  return value if ok?

  block.call(value)
end