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, and let Ok and Err handle their initialization and 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 Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(value) ⇒ Any

Returns a new instance of Any.



13
14
15
# File 'lib/errgonomic/result.rb', line 13

def initialize(value)
  @value = value
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:



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/errgonomic/result.rb', line 60

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 Attribute Details

#valueObject (readonly)

Returns the value of attribute value.



11
12
13
# File 'lib/errgonomic/result.rb', line 11

def value
  @value
end

Instance Method Details

#<=>(other) ⇒ Object

Results order like Rust's: Ok sorts before any Err, and same variants order by their inner values. Follows Ruby's <=> convention of returning nil for incomparable operands, whether the other object is not a Result or the inner values do not themselves compare.

Examples:

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


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

def <=>(other)
  return nil unless other.is_a?(Errgonomic::Result::Any)
  return ok? ? -1 : 1 if self.class != other.class

  value <=> other.value
end

#==(other) ⇒ Object

Equality comparison for Result objects is based on value not reference.

Examples:

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

Parameters:



90
91
92
93
94
# File 'lib/errgonomic/result.rb', line 90

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

  value == other.value
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:



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

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)


227
228
229
230
231
232
233
234
235
236
# File 'lib/errgonomic/result.rb', line 227

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

#deconstructObject

Examples:

simple pattern match with variable capture of the value

result = Errgonomic::Result::Ok.new(1)
case result
in Errgonomic::Result::Ok, value
  "Measurement is #{value}"
in Errgonomic::Result::Err, err
  "Measurement is not available"
end # => "Measurement is 1"

more advanced pattern match against the kind of value

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


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

def deconstruct
  [self, 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)]

Returns:

  • (Boolean)


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

def eql?(other)
  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)


152
153
154
155
156
157
158
# File 'lib/errgonomic/result.rb', line 152

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

#expect!(msg) ⇒ Object

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

Examples:

Ok(1).expect!("should have worked") # => 1
Err(:d).expect!("should have worked") # => raise Errgonomic::ExpectError, "should have worked"

Parameters:

Raises:



179
180
181
182
183
# File 'lib/errgonomic/result.rb', line 179

def expect!(msg)
  raise Errgonomic::ExpectError, msg unless ok?

  @value
end

#hashObject

Examples:

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


113
114
115
# File 'lib/errgonomic/result.rb', line 113

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)


333
334
335
336
337
# File 'lib/errgonomic/result.rb', line 333

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)


344
345
346
347
348
# File 'lib/errgonomic/result.rb', line 344

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)


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

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:



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

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)


269
270
271
272
273
274
275
276
277
278
# File 'lib/errgonomic/result.rb', line 269

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.



374
375
376
# File 'lib/errgonomic/result.rb', line 374

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

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

Returns:

  • (Boolean)


76
77
78
# File 'lib/errgonomic/result.rb', line 76

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)


124
125
126
# File 'lib/errgonomic/result.rb', line 124

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


316
317
318
319
# File 'lib/errgonomic/result.rb', line 316

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.



323
324
325
326
# File 'lib/errgonomic/result.rb', line 323

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:



368
369
370
# File 'lib/errgonomic/result.rb', line 368

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

#to_sObject

Refuse to serialize an unwrapped Result as a String. Results must be correctly handled to access their inner value.

Examples:

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

Raises:



356
357
358
# File 'lib/errgonomic/result.rb', line 356

def to_s
  raise Errgonomic::SerializeError, 'cannot serialize an unwrapped Result'
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:



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

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.

Examples:

Ok(1).unwrap_err! # => raise Errgonomic::UnwrapError, 1
Err(:e).unwrap_err! # => :e

Raises:



190
191
192
193
194
# File 'lib/errgonomic/result.rb', line 190

def unwrap_err!
  raise Errgonomic::UnwrapError, 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:



287
288
289
290
291
# File 'lib/errgonomic/result.rb', line 287

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)


301
302
303
304
305
# File 'lib/errgonomic/result.rb', line 301

def unwrap_or_else(&block)
  return value if ok?

  block.call(value)
end