errgonomic 0.6.0 → 0.8.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/.envrc +1 -1
- data/.rubocop.yml +28 -1
- data/CONTRIBUTING.md +67 -0
- data/README.md +180 -18
- data/Rakefile +14 -1
- data/flake.lock +26 -4
- data/flake.nix +84 -19
- data/gem-groups.json +1 -0
- data/lib/errgonomic/core_ext/array.rb +38 -0
- data/lib/errgonomic/core_ext/bool.rb +68 -0
- data/lib/errgonomic/core_ext/hash.rb +36 -0
- data/lib/errgonomic/option.rb +204 -30
- data/lib/errgonomic/optional_array.rb +146 -0
- data/lib/errgonomic/optional_dig.rb +33 -0
- data/lib/errgonomic/optional_hash.rb +147 -0
- data/lib/errgonomic/presence.rb +4 -2
- data/lib/errgonomic/rails/active_record_delegate_optional.rb +6 -0
- data/lib/errgonomic/rails/active_record_optional.rb +40 -8
- data/lib/errgonomic/rails.rb +3 -1
- data/lib/errgonomic/result.rb +151 -5
- data/lib/errgonomic/type.rb +2 -0
- data/lib/errgonomic/version.rb +1 -1
- data/lib/errgonomic.rb +19 -0
- metadata +10 -3
- data/gemset.nix +0 -1084
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../option'
|
|
4
|
+
require_relative '../result'
|
|
5
|
+
|
|
6
|
+
# Lift booleans into Option and Result, following Rust's bool: then_some,
|
|
7
|
+
# and ok_or/ok_or_else from nightly. Rust splits the lazy form into `then`,
|
|
8
|
+
# but that name is core Ruby (Kernel#then), so then_some takes either a
|
|
9
|
+
# value or a block. Rust's ok_or returns Result<(), E>; Ruby has no unit
|
|
10
|
+
# type, so Ok carries true.
|
|
11
|
+
class TrueClass
|
|
12
|
+
# @example
|
|
13
|
+
# true.then_some(:hello) # => Some(:hello)
|
|
14
|
+
# true.then_some { :hello } # => Some(:hello)
|
|
15
|
+
# true.then_some(nil) # => Some(nil)
|
|
16
|
+
# true.then_some # => raise Errgonomic::ArgumentError, "then_some takes either a value or a block"
|
|
17
|
+
def then_some(*args, &block)
|
|
18
|
+
if args.length == 1 && !block
|
|
19
|
+
Some(args[0])
|
|
20
|
+
elsif args.empty? && block
|
|
21
|
+
Some(block.call)
|
|
22
|
+
else
|
|
23
|
+
raise Errgonomic::ArgumentError, 'then_some takes either a value or a block'
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @example
|
|
28
|
+
# true.ok_or(:ohno) # => Ok(true)
|
|
29
|
+
def ok_or(_err)
|
|
30
|
+
Ok(true)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @example
|
|
34
|
+
# true.ok_or_else { :ohno } # => Ok(true)
|
|
35
|
+
def ok_or_else
|
|
36
|
+
Ok(true)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The false halves of the conversions above.
|
|
41
|
+
class FalseClass
|
|
42
|
+
# The block is never called; arguments are validated all the same, so a
|
|
43
|
+
# misuse fails regardless of which way the flag happens to point.
|
|
44
|
+
#
|
|
45
|
+
# @example
|
|
46
|
+
# false.then_some(:hello) # => None()
|
|
47
|
+
# false.then_some { :hello } # => None()
|
|
48
|
+
# false.then_some # => raise Errgonomic::ArgumentError, "then_some takes either a value or a block"
|
|
49
|
+
def then_some(*args, &block)
|
|
50
|
+
unless (args.length == 1 && !block) || (args.empty? && block)
|
|
51
|
+
raise Errgonomic::ArgumentError, 'then_some takes either a value or a block'
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
None()
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @example
|
|
58
|
+
# false.ok_or(:ohno) # => Err(:ohno)
|
|
59
|
+
def ok_or(err)
|
|
60
|
+
Err(err)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @example
|
|
64
|
+
# false.ok_or_else { :ohno } # => Err(:ohno)
|
|
65
|
+
def ok_or_else(&block)
|
|
66
|
+
Err(block.call)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../option'
|
|
4
|
+
require_relative '../optional_hash'
|
|
5
|
+
|
|
6
|
+
# Two additive lookups; no existing Hash behavior changes. This is as deep as
|
|
7
|
+
# the gem reaches into Hash: the wider Ruby ecosystem leans on Hash semantics
|
|
8
|
+
# too heavily to patch them, so richer behavior lives in
|
|
9
|
+
# Errgonomic::OptionalHash instead.
|
|
10
|
+
class Hash
|
|
11
|
+
# Retrieve the value for a key, wrapped in an Option, following key
|
|
12
|
+
# presence as Rust's HashMap#get does: a present key with a nil value is
|
|
13
|
+
# Some(nil); only a missing key is None.
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# h = { color: :blue, shade: nil }
|
|
17
|
+
# h.fetch_option(:color) # => Some(:blue)
|
|
18
|
+
# h.fetch_option(:shade) # => Some(nil)
|
|
19
|
+
# h.fetch_option(:smell) # => None()
|
|
20
|
+
def fetch_option(key)
|
|
21
|
+
return None() unless key?(key)
|
|
22
|
+
|
|
23
|
+
Some(self[key])
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Wrap this hash in an Errgonomic::OptionalHash view. The wrapper reads and
|
|
27
|
+
# writes this same hash; use its to_h for a detached copy.
|
|
28
|
+
#
|
|
29
|
+
# @example
|
|
30
|
+
# h = { color: :blue }.into_optional
|
|
31
|
+
# h[:color] # => Some(:blue)
|
|
32
|
+
# h[:smell] # => None()
|
|
33
|
+
def into_optional
|
|
34
|
+
Errgonomic::OptionalHash.new(self)
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/errgonomic/option.rb
CHANGED
|
@@ -5,20 +5,65 @@ module Errgonomic
|
|
|
5
5
|
# The base class for all options. Some and None are subclasses.
|
|
6
6
|
#
|
|
7
7
|
class Any
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
include Comparable
|
|
9
|
+
|
|
10
|
+
# Rust spellings we accept but do not advertise: they delegate to the
|
|
11
|
+
# Ruby-idiomatic predicate and nudge the caller there via stderr.
|
|
12
|
+
RUST_SPELLINGS = {
|
|
13
|
+
is_some: :some?,
|
|
14
|
+
is_none: :none?,
|
|
15
|
+
is_some_and: :some_and?,
|
|
16
|
+
is_none_or: :none_or?
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
# An Option deliberately forwards nothing to its inner value, so a miss
|
|
20
|
+
# here is almost always someone treating the container as its contents.
|
|
21
|
+
# Teach the route out instead of leaving a bare NoMethodError. Rust
|
|
22
|
+
# spellings of the predicates delegate, with a nudge on stderr.
|
|
17
23
|
#
|
|
18
|
-
#
|
|
24
|
+
# @example
|
|
25
|
+
# begin
|
|
26
|
+
# Some(5) + 1
|
|
27
|
+
# rescue NoMethodError => e
|
|
28
|
+
# e.class
|
|
29
|
+
# end # => Errgonomic::UnwrappedAccessError
|
|
30
|
+
# Some(5).respond_to?(:+) # => false
|
|
31
|
+
# Some(1).is_some_and { |x| x > 0 } # => true
|
|
32
|
+
# None().is_none # => true
|
|
33
|
+
# Some(5).respond_to?(:is_some) # => true
|
|
34
|
+
def method_missing(name, *args, &block)
|
|
35
|
+
if (canonical = RUST_SPELLINGS[name])
|
|
36
|
+
warn "Errgonomic: `#{name}` is the Rust spelling; prefer `#{canonical}`. Delegating."
|
|
37
|
+
return public_send(canonical, *args, &block)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
raise Errgonomic::UnwrappedAccessError.new(<<~MSG, name)
|
|
41
|
+
undefined method `#{name}' for #{inspect}, an Option, which does not forward methods to its inner value.
|
|
42
|
+
Reach for a combinator instead:
|
|
43
|
+
map, and_then, filter: transform the value if present
|
|
44
|
+
unwrap_or, unwrap_or_else: supply a fallback
|
|
45
|
+
ok_or, ok_or_else: convert to a Result
|
|
46
|
+
some_and?, none_or?: test a predicate against the inner value
|
|
47
|
+
unwrap! and expect! also exist, but are intended for tests rather than application code.
|
|
48
|
+
MSG
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def respond_to_missing?(name, include_private = false)
|
|
52
|
+
RUST_SPELLINGS.key?(name) || super
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# An Option equals another Option of the same class with an equal inner
|
|
56
|
+
# value. Anything else, including nil and the raw inner value, is not
|
|
57
|
+
# equal: quietly false, never an error. Rust rejects Some(5) == 5 at
|
|
58
|
+
# compile time; Ruby cannot, and raising here would break the many
|
|
59
|
+
# places Ruby compares heterogeneous operands (Array#include?,
|
|
60
|
+
# assertion diffs, dirty tracking). Compare Options (opt == Some(5)) or
|
|
61
|
+
# test the inner value (opt.some_and? { |v| v == 5 }) instead.
|
|
19
62
|
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
63
|
+
# None() == nil is likewise false: None is a value that represents
|
|
64
|
+
# absence, not an absence Ruby can see. (The Rails integration
|
|
65
|
+
# separately makes None#nil? answer true, as an ActiveRecord
|
|
66
|
+
# compromise; equality does not follow it.)
|
|
22
67
|
#
|
|
23
68
|
# @example
|
|
24
69
|
# Some(1) == Some(1) # => true
|
|
@@ -34,6 +79,34 @@ module Errgonomic
|
|
|
34
79
|
value == other.value
|
|
35
80
|
end
|
|
36
81
|
|
|
82
|
+
# Hash-based collections (Hash keys, Set, uniq, group_by) use eql? and
|
|
83
|
+
# hash, not ==. Follow the inner value's own eql? semantics, so Options
|
|
84
|
+
# behave as keys exactly like their inner values: Some(1) and Some(1.0)
|
|
85
|
+
# are distinct keys, just as 1 and 1.0 are.
|
|
86
|
+
#
|
|
87
|
+
# @example
|
|
88
|
+
# Some(5).eql?(Some(5)) # => true
|
|
89
|
+
# Some(1).eql?(Some(1.0)) # => false
|
|
90
|
+
# None().eql?(None()) # => true
|
|
91
|
+
# { Some(5) => 1 }[Some(5)] # => 1
|
|
92
|
+
# [Some(1), Some(1), None(), None()].uniq # => [Some(1), None()]
|
|
93
|
+
def eql?(other)
|
|
94
|
+
return false if self.class != other.class
|
|
95
|
+
return true if none?
|
|
96
|
+
|
|
97
|
+
value.eql?(other.value)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# @example
|
|
101
|
+
# Some(5).hash == Some(5).hash # => true
|
|
102
|
+
# None().hash == None().hash # => true
|
|
103
|
+
# Some(5).hash == None().hash # => false
|
|
104
|
+
def hash
|
|
105
|
+
return self.class.hash if none?
|
|
106
|
+
|
|
107
|
+
[self.class, value].hash
|
|
108
|
+
end
|
|
109
|
+
|
|
37
110
|
# @example
|
|
38
111
|
# measurement = Errgonomic::Option::Some.new(1)
|
|
39
112
|
# case measurement
|
|
@@ -50,6 +123,28 @@ module Errgonomic
|
|
|
50
123
|
[Errgonomic::Option::None]
|
|
51
124
|
end
|
|
52
125
|
|
|
126
|
+
# Options order like Rust's: None sorts before any Some, and Somes
|
|
127
|
+
# order by their inner values. Follows Ruby's <=> convention of
|
|
128
|
+
# returning nil for incomparable operands, whether the other object is
|
|
129
|
+
# not an Option or the inner values do not themselves compare.
|
|
130
|
+
#
|
|
131
|
+
# @example
|
|
132
|
+
# (Some(5) <=> Some(6)) # => -1
|
|
133
|
+
# (None() <=> Some(5)) # => -1
|
|
134
|
+
# (Some(5) <=> None()) # => 1
|
|
135
|
+
# (None() <=> None()) # => 0
|
|
136
|
+
# (Some(1) <=> Some("x")) # => nil
|
|
137
|
+
# (Some(1) <=> 1) # => nil
|
|
138
|
+
# [Some(2), None(), Some(1)].sort # => [None(), Some(1), Some(2)]
|
|
139
|
+
# [Some(2), Some(1)].min # => Some(1)
|
|
140
|
+
def <=>(other)
|
|
141
|
+
return nil unless other.is_a?(Errgonomic::Option::Any)
|
|
142
|
+
return none? ? 0 : 1 if other.none?
|
|
143
|
+
return -1 if none?
|
|
144
|
+
|
|
145
|
+
value <=> other.value
|
|
146
|
+
end
|
|
147
|
+
|
|
53
148
|
# return true if the contained value is Some and the block returns truthy
|
|
54
149
|
#
|
|
55
150
|
# @example
|
|
@@ -78,6 +173,27 @@ module Errgonomic
|
|
|
78
173
|
|
|
79
174
|
alias none_or? none_or
|
|
80
175
|
|
|
176
|
+
# Presence follows the discriminant, not the inner value: Some is
|
|
177
|
+
# present, None is blank. So Some(false) and Some(nil) are present,
|
|
178
|
+
# unlike their unwrapped values.
|
|
179
|
+
#
|
|
180
|
+
# @example
|
|
181
|
+
# Some(1).present? # => true
|
|
182
|
+
# Some(false).present? # => true
|
|
183
|
+
# Some("").present? # => true
|
|
184
|
+
# None().present? # => false
|
|
185
|
+
def present?
|
|
186
|
+
some?
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# @example
|
|
190
|
+
# None().blank? # => true
|
|
191
|
+
# Some(1).blank? # => false
|
|
192
|
+
# Some(nil).blank? # => false
|
|
193
|
+
def blank?
|
|
194
|
+
none?
|
|
195
|
+
end
|
|
196
|
+
|
|
81
197
|
# return an Array with the contained value, if any
|
|
82
198
|
# @example
|
|
83
199
|
# Some(1).to_a # => [1]
|
|
@@ -119,16 +235,6 @@ module Errgonomic
|
|
|
119
235
|
value
|
|
120
236
|
end
|
|
121
237
|
|
|
122
|
-
# # returns the inner value if present, else returns the default value
|
|
123
|
-
# # @example
|
|
124
|
-
# # Some(1).unwrap_or(2) # => 1
|
|
125
|
-
# # None().unwrap_or(2) # => 2
|
|
126
|
-
# def unwrap_or_default
|
|
127
|
-
# self.class.respond_to?(:default) or raise
|
|
128
|
-
# return self.class.default if none?
|
|
129
|
-
# value
|
|
130
|
-
# end
|
|
131
|
-
|
|
132
238
|
# returns the inner value if present, else returns the result of the
|
|
133
239
|
# provided block
|
|
134
240
|
# @example
|
|
@@ -342,14 +448,62 @@ module Errgonomic
|
|
|
342
448
|
raise Errgonomic::SerializeError, 'cannot serialize an unwrapped Option'
|
|
343
449
|
end
|
|
344
450
|
|
|
345
|
-
#
|
|
346
|
-
#
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
#
|
|
352
|
-
#
|
|
451
|
+
# pp uses its own object dump unless told otherwise; keep it consistent
|
|
452
|
+
# with inspect.
|
|
453
|
+
def pretty_print(pp)
|
|
454
|
+
pp.text(inspect)
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# Return self if the predicate is truthy for the inner value, else None.
|
|
458
|
+
# None passes through.
|
|
459
|
+
#
|
|
460
|
+
# @example
|
|
461
|
+
# Some(1).filter(&:odd?) # => Some(1)
|
|
462
|
+
# Some(2).filter(&:odd?) # => None()
|
|
463
|
+
# None().filter(&:odd?) # => None()
|
|
464
|
+
def filter(&block)
|
|
465
|
+
return self if none?
|
|
466
|
+
|
|
467
|
+
block.call(value) ? self : None()
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
# Remove one level of Option nesting. Pedantically raises when the inner
|
|
471
|
+
# value is not itself an Option, which in Rust would not have compiled.
|
|
472
|
+
#
|
|
473
|
+
# @example
|
|
474
|
+
# Some(Some(1)).flatten # => Some(1)
|
|
475
|
+
# Some(None()).flatten # => None()
|
|
476
|
+
# None().flatten # => None()
|
|
477
|
+
# Some(Some(Some(1))).flatten # => Some(Some(1))
|
|
478
|
+
# Some(1).flatten # => raise Errgonomic::TypeMismatchError, "cannot flatten Integer; it is not an Option"
|
|
479
|
+
def flatten
|
|
480
|
+
return self if none?
|
|
481
|
+
|
|
482
|
+
unless value.is_a?(Errgonomic::Option::Any)
|
|
483
|
+
raise Errgonomic::TypeMismatchError,
|
|
484
|
+
"cannot flatten #{value.class}; it is not an Option"
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
value
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
# Return Some when either self or other are Some, otherwise return None
|
|
491
|
+
# when both are None or both are Some.
|
|
492
|
+
#
|
|
493
|
+
# @example
|
|
494
|
+
# Some(:left).xor(Some(:right)) # => None()
|
|
495
|
+
# Some(:left).xor(None()) #=> Some(:left)
|
|
496
|
+
# None().xor(Some(:right)) #=> Some(:right)
|
|
497
|
+
#
|
|
498
|
+
def xor(other)
|
|
499
|
+
return self if some? && other.none?
|
|
500
|
+
return other if other.some? && none?
|
|
501
|
+
|
|
502
|
+
None()
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
# Rust's mutating combinators (insert, get_or_insert, take, replace)
|
|
506
|
+
# are deliberately omitted: an Option here is a value, not a slot.
|
|
353
507
|
end
|
|
354
508
|
|
|
355
509
|
# Represent a value
|
|
@@ -357,6 +511,7 @@ module Errgonomic
|
|
|
357
511
|
attr_accessor :value
|
|
358
512
|
|
|
359
513
|
def initialize(value)
|
|
514
|
+
super()
|
|
360
515
|
@value = value
|
|
361
516
|
end
|
|
362
517
|
|
|
@@ -367,8 +522,21 @@ module Errgonomic
|
|
|
367
522
|
def none?
|
|
368
523
|
false
|
|
369
524
|
end
|
|
525
|
+
|
|
526
|
+
# Render like Rust's Debug, delegating to the inner value's inspect so
|
|
527
|
+
# nesting stays unambiguous.
|
|
528
|
+
#
|
|
529
|
+
# @example
|
|
530
|
+
# Some(5).inspect # => "Some(5)"
|
|
531
|
+
# Some("x").inspect # => "Some(\"x\")"
|
|
532
|
+
# Some(nil).inspect # => "Some(nil)"
|
|
533
|
+
# Some(Some(1)).inspect # => "Some(Some(1))"
|
|
534
|
+
def inspect
|
|
535
|
+
"Some(#{value.inspect})"
|
|
536
|
+
end
|
|
370
537
|
end
|
|
371
538
|
|
|
539
|
+
# Represent the absence of a value.
|
|
372
540
|
class None < Any
|
|
373
541
|
def some?
|
|
374
542
|
false
|
|
@@ -377,6 +545,12 @@ module Errgonomic
|
|
|
377
545
|
def none?
|
|
378
546
|
true
|
|
379
547
|
end
|
|
548
|
+
|
|
549
|
+
# @example
|
|
550
|
+
# None().inspect # => "None"
|
|
551
|
+
def inspect
|
|
552
|
+
'None'
|
|
553
|
+
end
|
|
380
554
|
end
|
|
381
555
|
end
|
|
382
556
|
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'option'
|
|
4
|
+
require_relative 'optional_dig'
|
|
5
|
+
|
|
6
|
+
module Errgonomic
|
|
7
|
+
# A companion to Array whose lookups return Options, composed around a
|
|
8
|
+
# plain Array for the same reasons OptionalHash composes around a Hash.
|
|
9
|
+
# Lookups follow element presence: an element holding nil is Some(nil),
|
|
10
|
+
# and only an out-of-bounds index is None, as with Rust's slice get.
|
|
11
|
+
class OptionalArray
|
|
12
|
+
include OptionalDig
|
|
13
|
+
|
|
14
|
+
def initialize(array = [])
|
|
15
|
+
unless array.is_a?(::Array)
|
|
16
|
+
raise Errgonomic::TypeMismatchError,
|
|
17
|
+
"OptionalArray wraps an Array, got #{array.class}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
@array = array
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Retrieve the element at an integer index, wrapped in an Option.
|
|
24
|
+
# Negative indexes count from the end, as usual. A non-integer index
|
|
25
|
+
# raises, pedantically: the silent nil of Array#[] with a bad argument is
|
|
26
|
+
# the ambiguity this class exists to remove.
|
|
27
|
+
#
|
|
28
|
+
# @example
|
|
29
|
+
# a = [:a, nil].into_optional
|
|
30
|
+
# a[0] # => Some(:a)
|
|
31
|
+
# a[1] # => Some(nil)
|
|
32
|
+
# a[2] # => None()
|
|
33
|
+
# a[-1] # => Some(nil)
|
|
34
|
+
# a[:nope] # => raise Errgonomic::TypeMismatchError, "index must be an Integer, got Symbol"
|
|
35
|
+
def [](index)
|
|
36
|
+
unless index.is_a?(::Integer)
|
|
37
|
+
raise Errgonomic::TypeMismatchError,
|
|
38
|
+
"index must be an Integer, got #{index.class}"
|
|
39
|
+
end
|
|
40
|
+
return None() unless (-@array.length...@array.length).cover?(index)
|
|
41
|
+
|
|
42
|
+
Some(@array[index])
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Write through to the underlying array.
|
|
46
|
+
#
|
|
47
|
+
# @example
|
|
48
|
+
# a = [].into_optional
|
|
49
|
+
# a[0] = :a
|
|
50
|
+
# a[0] # => Some(:a)
|
|
51
|
+
def []=(index, value)
|
|
52
|
+
@array[index] = value
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Like Array#dig, but every step checks presence, so an absent path
|
|
56
|
+
# (None) stays distinct from a present nil (Some(nil)).
|
|
57
|
+
#
|
|
58
|
+
# @example
|
|
59
|
+
# a = [{ name: 'Ada' }].into_optional
|
|
60
|
+
# a.dig(0, :name) # => Some("Ada")
|
|
61
|
+
# a.dig(0, :nickname) # => None()
|
|
62
|
+
# a.dig(1, :name) # => None()
|
|
63
|
+
def dig(index, *rest)
|
|
64
|
+
optional_dig(@array, [index, *rest])
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The first element as an Option, as with Rust's slice first.
|
|
68
|
+
#
|
|
69
|
+
# @example
|
|
70
|
+
# [1, 2].into_optional.first # => Some(1)
|
|
71
|
+
# [nil].into_optional.first # => Some(nil)
|
|
72
|
+
# [].into_optional.first # => None()
|
|
73
|
+
def first
|
|
74
|
+
return None() if @array.empty?
|
|
75
|
+
|
|
76
|
+
Some(@array.first)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The last element as an Option, as with Rust's slice last.
|
|
80
|
+
#
|
|
81
|
+
# @example
|
|
82
|
+
# [1, 2].into_optional.last # => Some(2)
|
|
83
|
+
# [].into_optional.last # => None()
|
|
84
|
+
def last
|
|
85
|
+
return None() if @array.empty?
|
|
86
|
+
|
|
87
|
+
Some(@array.last)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @example
|
|
91
|
+
# [].into_optional.empty? # => true
|
|
92
|
+
# [1].into_optional.empty? # => false
|
|
93
|
+
def empty?
|
|
94
|
+
@array.empty?
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# @example
|
|
98
|
+
# [1, 2].into_optional.size # => 2
|
|
99
|
+
def size
|
|
100
|
+
@array.size
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# The escape hatch back to a plain Array: a shallow copy, so array-shaped
|
|
104
|
+
# code cannot mutate the wrapped state behind the Option semantics.
|
|
105
|
+
#
|
|
106
|
+
# @example
|
|
107
|
+
# [1].into_optional.to_a # => [1]
|
|
108
|
+
def to_a
|
|
109
|
+
@array.dup
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Equal to another OptionalArray wrapping an equal array; never equal to
|
|
113
|
+
# a plain Array, mirroring how Some(x) is never equal to x.
|
|
114
|
+
#
|
|
115
|
+
# @example
|
|
116
|
+
# [1].into_optional == [1].into_optional # => true
|
|
117
|
+
# [1].into_optional == [2].into_optional # => false
|
|
118
|
+
# [1].into_optional == [1] # => false
|
|
119
|
+
def ==(other)
|
|
120
|
+
other.is_a?(OptionalArray) && inner == other.inner
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# @example
|
|
124
|
+
# [1].into_optional.eql?([1].into_optional) # => true
|
|
125
|
+
# { [1].into_optional => :hit }[[1].into_optional] # => :hit
|
|
126
|
+
def eql?(other)
|
|
127
|
+
other.is_a?(OptionalArray) && inner.eql?(other.inner)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def hash
|
|
131
|
+
[self.class, inner].hash
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# @example
|
|
135
|
+
# [1].into_optional.inspect # => "OptionalArray([1])"
|
|
136
|
+
def inspect
|
|
137
|
+
"OptionalArray(#{@array.inspect})"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
protected
|
|
141
|
+
|
|
142
|
+
def inner
|
|
143
|
+
@array
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'option'
|
|
4
|
+
|
|
5
|
+
module Errgonomic
|
|
6
|
+
# The presence-checking walk behind OptionalHash#dig and OptionalArray#dig:
|
|
7
|
+
# every step checks key or bounds presence, so an absent path (None) stays
|
|
8
|
+
# distinct from a present nil (Some(nil)), which the core dig methods
|
|
9
|
+
# conflate. A nested wrapper handles the rest of the walk itself, by
|
|
10
|
+
# recursion. Digging into a non-collection raises, pedantically, where
|
|
11
|
+
# core dig would raise TypeError.
|
|
12
|
+
module OptionalDig
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def optional_dig(start, keys)
|
|
16
|
+
current = start
|
|
17
|
+
keys.each_with_index do |key, idx|
|
|
18
|
+
case current
|
|
19
|
+
when OptionalHash, OptionalArray
|
|
20
|
+
return current.dig(key, *keys[(idx + 1)..])
|
|
21
|
+
when ::Hash
|
|
22
|
+
return None() unless current.key?(key)
|
|
23
|
+
when ::Array
|
|
24
|
+
return None() unless key.is_a?(Integer) && (-current.length...current.length).cover?(key)
|
|
25
|
+
else
|
|
26
|
+
raise Errgonomic::TypeMismatchError, "cannot dig into #{current.class}"
|
|
27
|
+
end
|
|
28
|
+
current = current[key]
|
|
29
|
+
end
|
|
30
|
+
Some(current)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|