errgonomic 0.8.3 → 0.9.1

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,26 +225,32 @@ 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
184
242
 
185
243
  # Return the inner value of an Err, else raise an exception when Ok.
244
+ # The message is the Ok's value as inspect renders it, bounded, so an
245
+ # Ok holding an Option or a Result still has a message to print.
186
246
  #
187
247
  # @example
188
- # Ok(1).unwrap_err! # => raise Errgonomic::UnwrapError, 1
248
+ # Ok(1).unwrap_err! # => raise Errgonomic::UnwrapError, "1"
249
+ # Ok(Some(1)).unwrap_err! # => raise Errgonomic::UnwrapError, "Some(1)"
250
+ # Ok("a" * 100).unwrap_err! # => raise Errgonomic::UnwrapError, "\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa..."
189
251
  # Err(:e).unwrap_err! # => :e
190
252
  def unwrap_err!
191
- raise Errgonomic::UnwrapError, value unless err?
253
+ raise Errgonomic::UnwrapError.new(bounded_inspect(value), value) unless err?
192
254
 
193
255
  @value
194
256
  end
@@ -347,14 +409,22 @@ module Errgonomic
347
409
  Err(block.call(value))
348
410
  end
349
411
 
350
- # Refuse to serialize an unwrapped Result as a String. Results must be
351
- # correctly handled to access their inner value.
412
+ # Refuse to render as a String. Rust gives Result a Debug and no
413
+ # Display: a wrapper that reaches a string went unhandled, and a string
414
+ # is where it turns into data, a hostname, a hash key or a page. The
415
+ # refusal names the value and says how to log it or take it.
352
416
  #
353
417
  # @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"
418
+ # 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"
419
+ # 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"
420
+ # "outcome: #{Ok(1)}" # => raise Errgonomic::SerializeError, "Ok(1) refuses to_s; use inspect for a log line, or unwrap_or / expect! for the value"
421
+ # [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"
422
+ # 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"
423
+ # String(Ok(1)) # => raise Errgonomic::SerializeError, "Ok(1) refuses to_s; use inspect for a log line, or unwrap_or / expect! for the value"
424
+ # 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"
425
+ # Err(:nope).inspect # => "Err(:nope)"
356
426
  def to_s
357
- raise Errgonomic::SerializeError, 'cannot serialize an unwrapped Result'
427
+ raise Errgonomic::SerializeError, to_s_refusal
358
428
  end
359
429
 
360
430
  # Refuse to serialize an unwrapped Result as JSON. Not only should we
@@ -406,6 +476,38 @@ module Errgonomic
406
476
  def deconstruct
407
477
  [self, value]
408
478
  end
479
+
480
+ private
481
+
482
+ def to_s_refusal
483
+ "#{bounded_inspect} refuses to_s; use inspect for a log line, or unwrap_or / expect! for the value"
484
+ end
485
+
486
+ # Name the value the caller failed to handle, bounded: an inspect of a
487
+ # record or a long payload would bury the message carrying it.
488
+ def bounded_inspect(object = self)
489
+ rendered = object.inspect
490
+ rendered.length > 60 ? "#{rendered[0, 57]}..." : rendered
491
+ end
492
+
493
+ def strict_equality!(other, operator)
494
+ return unless Errgonomic.strict_equality?
495
+ return if other.is_a?(Errgonomic::Result::Any)
496
+
497
+ raise Errgonomic::TypeMismatchError,
498
+ "#{self.class} #{operator} #{other.class}, which strict equality refuses.\n" \
499
+ "#{strict_equality_remedy(other)}"
500
+ end
501
+
502
+ def strict_equality_remedy(other)
503
+ return <<~MSG.chomp if other.is_a?(Errgonomic::Option::Any)
504
+ A Result and an Option are different containers, and neither is the other.
505
+ Unwrap the one you meant (res.unwrap_or(nil) == opt.unwrap_or(nil)).
506
+ MSG
507
+
508
+ "Compare Results (res == Ok(#{other.inspect})), test the inner value " \
509
+ "(res.ok_and? { |v| v == #{other.inspect} }), or unwrap_or a fallback first."
510
+ end
409
511
  end
410
512
 
411
513
  # The Ok variant.
@@ -474,8 +576,9 @@ module Errgonomic
474
576
  # @example
475
577
  # Err(:nope).inspect # => "Err(:nope)"
476
578
  # Err().inspect # => "Err()"
579
+ # Errgonomic.with_strict_equality { Err(Some(1)).inspect } # => "Err(Some(1))"
477
580
  def inspect
478
- return 'Err()' if value == Arbitrary
581
+ return 'Err()' if value.equal?(Arbitrary)
479
582
 
480
583
  "Err(#{value.inspect})"
481
584
  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.1'
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,13 +1,13 @@
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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Zadrozny
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
10
+ date: 1980-01-01 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: concurrent-ruby
@@ -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