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.
- checksums.yaml +4 -4
- data/.rubocop.yml +18 -3
- data/CHANGELOG.md +132 -2
- data/CONTRIBUTING.md +3 -2
- data/README.md +164 -18
- data/Rakefile +11 -1
- data/doctest_helper.rb +153 -0
- data/gem-groups.json +1 -1
- data/lib/errgonomic/core_ext/enumerable.rb +94 -0
- data/lib/errgonomic/option.rb +229 -50
- data/lib/errgonomic/rails/active_record_delegate_optional.rb +170 -10
- data/lib/errgonomic/rails/active_record_optional.rb +500 -60
- data/lib/errgonomic/result.rb +98 -14
- data/lib/errgonomic/version.rb +1 -1
- data/lib/errgonomic.rb +30 -0
- metadata +2 -1
data/lib/errgonomic/result.rb
CHANGED
|
@@ -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.
|
|
19
|
-
#
|
|
20
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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
|
-
#
|
|
351
|
-
#
|
|
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(
|
|
355
|
-
# Err(
|
|
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
|
-
|
|
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
|
|
562
|
+
return 'Err()' if value.equal?(Arbitrary)
|
|
479
563
|
|
|
480
564
|
"Err(#{value.inspect})"
|
|
481
565
|
end
|
data/lib/errgonomic/version.rb
CHANGED
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.
|
|
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
|