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.
- checksums.yaml +4 -4
- data/.rubocop.yml +18 -3
- data/CHANGELOG.md +146 -2
- data/CONTRIBUTING.md +3 -2
- data/README.md +168 -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 +240 -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 +119 -16
- data/lib/errgonomic/version.rb +1 -1
- data/lib/errgonomic.rb +30 -0
- metadata +3 -2
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,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
|
-
|
|
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
|
|
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
|
|
351
|
-
#
|
|
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(
|
|
355
|
-
# Err(
|
|
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,
|
|
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
|
|
581
|
+
return 'Err()' if value.equal?(Arbitrary)
|
|
479
582
|
|
|
480
583
|
"Err(#{value.inspect})"
|
|
481
584
|
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,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: errgonomic
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
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-
|
|
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
|