errgonomic 0.8.3 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,19 +15,30 @@ module Errgonomic
15
15
  end
16
16
 
17
17
  # Results order like Rust's: Ok sorts before any Err, and same variants
18
- # order by their inner values. Follows Ruby's <=> convention of
19
- # returning nil for incomparable operands, whether the other object is
20
- # not a Result or the inner values do not themselves compare.
18
+ # order by their inner values. Two Results whose inner values do not
19
+ # themselves compare follow Ruby's convention and answer nil. A
20
+ # non-Result operand raises instead: Comparable turns a nil here into an
21
+ # ArgumentError that names the Result as the operand at fault, where
22
+ # what went wrong is that a wrapper was ordered against a bare value.
21
23
  #
22
24
  # @example
23
25
  # (Ok(1) <=> Ok(2)) # => -1
24
26
  # (Ok(1) <=> Err(:a)) # => -1
25
27
  # (Err(:a) <=> Ok(1)) # => 1
26
28
  # (Err(:a) <=> Err(:b)) # => -1
27
- # (Ok(1) <=> 1) # => nil
28
29
  # [Err(:a), Ok(2), Ok(1)].sort # => [Ok(1), Ok(2), Err(:a)]
30
+ #
31
+ # @example a bare value is not ordered against a Result
32
+ # 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)"
33
+ # Ok(1).ok_and? { |v| v <= 2 } # => true
34
+ # Ok(1).map { |v| v <= 2 } # => Ok(true)
29
35
  def <=>(other)
30
- return nil unless other.is_a?(Errgonomic::Result::Any)
36
+ unless other.is_a?(Errgonomic::Result::Any)
37
+ raise Errgonomic::TypeMismatchError,
38
+ "cannot compare #{inspect} with #{other.class}; test the inner value " \
39
+ '(ok_and? { |v| v <= other }) or reach for it (map, unwrap_or)'
40
+ end
41
+
31
42
  return ok? ? -1 : 1 if self.class != other.class
32
43
 
33
44
  value <=> other.value
@@ -87,7 +98,34 @@ module Errgonomic
87
98
  # Ok(1).object_id == Ok(1).object_id # => false
88
99
  # Ok(1) == 1 # => false
89
100
  # Err() == nil # => false
101
+ #
102
+ # @example strict equality makes a cross-type comparison an error
103
+ # Errgonomic.with_strict_equality do
104
+ # begin
105
+ # Ok(1) == 1
106
+ # rescue Errgonomic::TypeMismatchError => e
107
+ # e.class
108
+ # end
109
+ # end # => Errgonomic::TypeMismatchError
110
+ # Errgonomic.with_strict_equality { Ok(1) == Ok(1) } # => true
111
+ # Errgonomic.with_strict_equality do
112
+ # begin
113
+ # Ok(1) != 1
114
+ # rescue Errgonomic::TypeMismatchError => e
115
+ # e.message.include?("!=")
116
+ # end
117
+ # end # => true
118
+ #
119
+ # @example an Option is another container, not another Result
120
+ # Errgonomic.with_strict_equality do
121
+ # begin
122
+ # Ok(1) == Some(1)
123
+ # rescue Errgonomic::TypeMismatchError => e
124
+ # e.message.include?("different containers")
125
+ # end
126
+ # end # => true
90
127
  def ==(other)
128
+ strict_equality!(other, '==')
91
129
  return false if self.class != other.class
92
130
 
93
131
  value == other.value
@@ -103,7 +141,25 @@ module Errgonomic
103
141
  # Ok(1).eql?(Err(1)) # => false
104
142
  # { Ok(5) => 1 }[Ok(5)] # => 1
105
143
  # [Err(:a), Err(:a)].uniq # => [Err(:a)]
144
+ #
145
+ # @example strict equality reaches eql?, and leaves hash alone
146
+ # Errgonomic.with_strict_equality do
147
+ # begin
148
+ # Ok(5).eql?(5)
149
+ # rescue Errgonomic::TypeMismatchError => e
150
+ # e.class
151
+ # end
152
+ # end # => Errgonomic::TypeMismatchError
153
+ # Errgonomic.with_strict_equality { Ok(5).hash == Ok(5).hash } # => true
154
+ # Ruby derives != from ==, so a strict-equality message would name the
155
+ # operator the caller did not write.
156
+ def !=(other)
157
+ strict_equality!(other, '!=')
158
+ super
159
+ end
160
+
106
161
  def eql?(other)
162
+ strict_equality!(other, 'eql?')
107
163
  self.class == other.class && value.eql?(other.value)
108
164
  end
109
165
 
@@ -169,15 +225,17 @@ module Errgonomic
169
225
  end
170
226
 
171
227
  # Return the inner value of an Ok, else raise an exception with the given
172
- # message when Err.
228
+ # message when Err. A block is called only on the Err branch, so a
229
+ # message that interpolates costs nothing on the path that succeeds.
173
230
  #
174
231
  # @param msg [String]
175
232
  #
176
233
  # @example
177
234
  # Ok(1).expect!("should have worked") # => 1
178
235
  # Err(:d).expect!("should have worked") # => raise Errgonomic::ExpectError, "should have worked"
179
- def expect!(msg)
180
- raise Errgonomic::ExpectError, msg unless ok?
236
+ # Err(:d).expect! { "no rate for #{7}" } # => raise Errgonomic::ExpectError, "no rate for 7"
237
+ def expect!(msg = nil, &block)
238
+ raise Errgonomic::ExpectError, block ? block.call : msg unless ok?
181
239
 
182
240
  @value
183
241
  end
@@ -347,14 +405,18 @@ module Errgonomic
347
405
  Err(block.call(value))
348
406
  end
349
407
 
350
- # Refuse to serialize an unwrapped Result as a String. Results must be
351
- # correctly handled to access their inner value.
408
+ # Render as inspect does. Rust gives Result a Debug and no Display, so
409
+ # refusing was faithful, but a to_s that raises replaces the real
410
+ # exception while a rescue builds its log line, and the rendered form
411
+ # says plainly that a wrapper arrived where a value was meant.
352
412
  #
353
413
  # @example
354
- # Ok("").to_s # => raise Errgonomic::SerializeError, "cannot serialize an unwrapped Result"
355
- # Err("").to_s # => raise Errgonomic::SerializeError, "cannot serialize an unwrapped Result"
414
+ # Ok(1).to_s # => "Ok(1)"
415
+ # Err(:nope).to_s # => "Err(:nope)"
416
+ # Err().to_s # => "Err()"
417
+ # "outcome: #{Ok(1)}" # => "outcome: Ok(1)"
356
418
  def to_s
357
- raise Errgonomic::SerializeError, 'cannot serialize an unwrapped Result'
419
+ inspect
358
420
  end
359
421
 
360
422
  # Refuse to serialize an unwrapped Result as JSON. Not only should we
@@ -406,6 +468,27 @@ module Errgonomic
406
468
  def deconstruct
407
469
  [self, value]
408
470
  end
471
+
472
+ private
473
+
474
+ def strict_equality!(other, operator)
475
+ return unless Errgonomic.strict_equality?
476
+ return if other.is_a?(Errgonomic::Result::Any)
477
+
478
+ raise Errgonomic::TypeMismatchError,
479
+ "#{self.class} #{operator} #{other.class}, which strict equality refuses.\n" \
480
+ "#{strict_equality_remedy(other)}"
481
+ end
482
+
483
+ def strict_equality_remedy(other)
484
+ return <<~MSG.chomp if other.is_a?(Errgonomic::Option::Any)
485
+ A Result and an Option are different containers, and neither is the other.
486
+ Unwrap the one you meant (res.unwrap_or(nil) == opt.unwrap_or(nil)).
487
+ MSG
488
+
489
+ "Compare Results (res == Ok(#{other.inspect})), test the inner value " \
490
+ "(res.ok_and? { |v| v == #{other.inspect} }), or unwrap_or a fallback first."
491
+ end
409
492
  end
410
493
 
411
494
  # The Ok variant.
@@ -474,8 +557,9 @@ module Errgonomic
474
557
  # @example
475
558
  # Err(:nope).inspect # => "Err(:nope)"
476
559
  # Err().inspect # => "Err()"
560
+ # Errgonomic.with_strict_equality { Err(Some(1)).inspect } # => "Err(Some(1))"
477
561
  def inspect
478
- return 'Err()' if value == Arbitrary
562
+ return 'Err()' if value.equal?(Arbitrary)
479
563
 
480
564
  "Err(#{value.inspect})"
481
565
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Errgonomic
4
- VERSION = '0.8.3'
4
+ VERSION = '0.9.0'
5
5
  end
data/lib/errgonomic.rb CHANGED
@@ -19,6 +19,9 @@ require_relative 'errgonomic/core_ext/array'
19
19
  # Lift booleans into Option and Result.
20
20
  require_relative 'errgonomic/core_ext/bool'
21
21
 
22
+ # All-or-nothing collection over an Enumerable of Options or Results.
23
+ require_relative 'errgonomic/core_ext/enumerable'
24
+
22
25
  # Rails fu
23
26
  require_relative 'errgonomic/rails' if defined?(Rails::Railtie)
24
27
 
@@ -83,6 +86,33 @@ module Errgonomic
83
86
  @give_me_ambiguous_downstream_errors = original_value
84
87
  end
85
88
 
89
+ # Cross-type equality is quiet by default, as it is for every Ruby object.
90
+ # Strict equality turns it into an error instead, for a test suite or CI:
91
+ # `Some(5) == 5` is the comparison Rust rejects at compile time, and it is
92
+ # silently false here otherwise.
93
+ #
94
+ # @example
95
+ # Errgonomic.strict_equality? # => false
96
+ # Errgonomic.with_strict_equality { Errgonomic.strict_equality? } # => true
97
+ # Errgonomic.strict_equality? # => false
98
+ def self.strict_equality?
99
+ !!@strict_equality
100
+ end
101
+
102
+ class << self
103
+ # Turn cross-type equality into an error for the rest of the process.
104
+ attr_writer :strict_equality
105
+ end
106
+
107
+ # Turn cross-type equality into an error for the duration of the block.
108
+ def self.with_strict_equality
109
+ original_value = @strict_equality
110
+ @strict_equality = true
111
+ yield
112
+ ensure
113
+ @strict_equality = original_value
114
+ end
115
+
86
116
  # Lenient inner value comparison means the inner value of a Some or Ok can be
87
117
  # compared to some other non-Result or non-Option value.
88
118
  def self.lenient_inner_value_comparison?
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: errgonomic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.3
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Zadrozny
@@ -78,6 +78,7 @@ files:
78
78
  - lib/errgonomic/core_ext/array.rb
79
79
  - lib/errgonomic/core_ext/blank.rb
80
80
  - lib/errgonomic/core_ext/bool.rb
81
+ - lib/errgonomic/core_ext/enumerable.rb
81
82
  - lib/errgonomic/core_ext/hash.rb
82
83
  - lib/errgonomic/option.rb
83
84
  - lib/errgonomic/optional_array.rb