errgonomic 0.7.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 +117 -6
- 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,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'option'
|
|
4
|
+
require_relative 'optional_dig'
|
|
5
|
+
|
|
6
|
+
module Errgonomic
|
|
7
|
+
# A companion to Hash whose lookups return Options, composed around a plain
|
|
8
|
+
# Hash rather than subclassing it. Subclassing cannot keep Option semantics:
|
|
9
|
+
# since Ruby 3, most Hash methods return plain Hash instances, so wrapped
|
|
10
|
+
# behavior silently drops off in pipelines. Composition with a small, closed
|
|
11
|
+
# API keeps the semantics honest; reach the plain Hash back with to_h.
|
|
12
|
+
#
|
|
13
|
+
# Lookups follow key presence, not value truthiness, so a key holding nil is
|
|
14
|
+
# Some(nil). This matches Option presence semantics: the discriminant tells
|
|
15
|
+
# you whether the key was there, and the inner value is yours to judge.
|
|
16
|
+
class OptionalHash
|
|
17
|
+
include OptionalDig
|
|
18
|
+
|
|
19
|
+
def initialize(hash = {})
|
|
20
|
+
unless hash.is_a?(::Hash)
|
|
21
|
+
raise Errgonomic::TypeMismatchError,
|
|
22
|
+
"OptionalHash wraps a Hash, got #{hash.class}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
@hash = hash
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Retrieve the value for a key, wrapped in an Option. A present key with
|
|
29
|
+
# a nil value is Some(nil); only a missing key is None.
|
|
30
|
+
#
|
|
31
|
+
# @example
|
|
32
|
+
# h = { color: :blue, shade: nil }.into_optional
|
|
33
|
+
# h[:color] # => Some(:blue)
|
|
34
|
+
# h[:shade] # => Some(nil)
|
|
35
|
+
# h[:smell] # => None()
|
|
36
|
+
def [](key)
|
|
37
|
+
return None() unless @hash.key?(key)
|
|
38
|
+
|
|
39
|
+
Some(@hash[key])
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Write through to the underlying hash.
|
|
43
|
+
#
|
|
44
|
+
# @example
|
|
45
|
+
# h = {}.into_optional
|
|
46
|
+
# h[:color] = :blue
|
|
47
|
+
# h[:color] # => Some(:blue)
|
|
48
|
+
def []=(key, value)
|
|
49
|
+
@hash[key] = value
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Like Hash#dig, but every step checks presence, so the result
|
|
53
|
+
# distinguishes an absent path (None) from a present nil (Some(nil)),
|
|
54
|
+
# which Hash#dig conflates. Walks nested Hashes, Arrays, and
|
|
55
|
+
# OptionalHashes; digging into anything else raises, pedantically, where
|
|
56
|
+
# Hash#dig would raise TypeError.
|
|
57
|
+
#
|
|
58
|
+
# @example
|
|
59
|
+
# h = { person: { name: 'Ada', middle_name: nil } }.into_optional
|
|
60
|
+
# h.dig(:person, :name) # => Some("Ada")
|
|
61
|
+
# h.dig(:person, :middle_name) # => Some(nil)
|
|
62
|
+
# h.dig(:person, :nickname) # => None()
|
|
63
|
+
# h.dig(:company, :name) # => None()
|
|
64
|
+
#
|
|
65
|
+
# @example arrays participate, with bounds checked
|
|
66
|
+
# h = { people: [{ name: 'Ada' }] }.into_optional
|
|
67
|
+
# h.dig(:people, 0, :name) # => Some("Ada")
|
|
68
|
+
# h.dig(:people, 1, :name) # => None()
|
|
69
|
+
#
|
|
70
|
+
# @example a nested wrapper walks the rest of the path itself
|
|
71
|
+
# h = { person: { name: 'Ada' }.into_optional }.into_optional
|
|
72
|
+
# h.dig(:person, :name) # => Some("Ada")
|
|
73
|
+
# h.dig(:person, :nickname) # => None()
|
|
74
|
+
#
|
|
75
|
+
# @example digging into a non-collection is an error, not a None
|
|
76
|
+
# h = { name: 'Ada' }.into_optional
|
|
77
|
+
# h.dig(:name, :length) # => raise Errgonomic::TypeMismatchError, "cannot dig into String"
|
|
78
|
+
def dig(key, *rest)
|
|
79
|
+
optional_dig(@hash, [key, *rest])
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @example
|
|
83
|
+
# h = { shade: nil }.into_optional
|
|
84
|
+
# h.key?(:shade) # => true
|
|
85
|
+
# h.key?(:color) # => false
|
|
86
|
+
def key?(key)
|
|
87
|
+
@hash.key?(key)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @example
|
|
91
|
+
# {}.into_optional.empty? # => true
|
|
92
|
+
# { a: 1 }.into_optional.empty? # => false
|
|
93
|
+
def empty?
|
|
94
|
+
@hash.empty?
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# @example
|
|
98
|
+
# { a: 1 }.into_optional.size # => 1
|
|
99
|
+
def size
|
|
100
|
+
@hash.size
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# The escape hatch back to a plain Hash: a shallow copy, so hash-shaped
|
|
104
|
+
# code cannot mutate the wrapped state behind the Option semantics.
|
|
105
|
+
#
|
|
106
|
+
# @example
|
|
107
|
+
# { a: 1 }.into_optional.to_h # => { a: 1 }
|
|
108
|
+
def to_h
|
|
109
|
+
@hash.dup
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Equal to another OptionalHash wrapping an equal hash; never equal to a
|
|
113
|
+
# plain Hash, mirroring how Some(x) is never equal to x.
|
|
114
|
+
#
|
|
115
|
+
# @example
|
|
116
|
+
# { a: 1 }.into_optional == { a: 1 }.into_optional # => true
|
|
117
|
+
# { a: 1 }.into_optional == { a: 2 }.into_optional # => false
|
|
118
|
+
# { a: 1 }.into_optional == { a: 1 } # => false
|
|
119
|
+
def ==(other)
|
|
120
|
+
other.is_a?(OptionalHash) && inner == other.inner
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# @example
|
|
124
|
+
# { a: 1 }.into_optional.eql?({ a: 1 }.into_optional) # => true
|
|
125
|
+
# h = { a: 1 }.into_optional
|
|
126
|
+
# { h => :hit }[{ a: 1 }.into_optional] # => :hit
|
|
127
|
+
def eql?(other)
|
|
128
|
+
other.is_a?(OptionalHash) && inner.eql?(other.inner)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def hash
|
|
132
|
+
[self.class, inner].hash
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# @example
|
|
136
|
+
# {}.into_optional.inspect # => "OptionalHash({})"
|
|
137
|
+
def inspect
|
|
138
|
+
"OptionalHash(#{@hash.inspect})"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
protected
|
|
142
|
+
|
|
143
|
+
def inner
|
|
144
|
+
@hash
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
data/lib/errgonomic/presence.rb
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
# gem a dependency.
|
|
6
6
|
require_relative './core_ext/blank' unless Object.methods.include?(:blank?)
|
|
7
7
|
|
|
8
|
+
# Presence-based fallbacks for every object: keep the receiver when it is
|
|
9
|
+
# present, otherwise substitute, compute, or raise.
|
|
8
10
|
class Object
|
|
9
11
|
# Returns the receiver if it is present, otherwise raises a NotPresentError.
|
|
10
12
|
# This method is useful to enforce strong expectations, where it is preferable
|
|
@@ -18,7 +20,7 @@ class Object
|
|
|
18
20
|
self
|
|
19
21
|
end
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
alias present_or_raise present_or_raise!
|
|
22
24
|
|
|
23
25
|
# Returns the receiver if it is present, otherwise returns the given value. If
|
|
24
26
|
# constructing the default value is expensive, consider using
|
|
@@ -61,7 +63,7 @@ class Object
|
|
|
61
63
|
self
|
|
62
64
|
end
|
|
63
65
|
|
|
64
|
-
|
|
66
|
+
alias blank_or_raise blank_or_raise!
|
|
65
67
|
|
|
66
68
|
# Returns the receiver if it is blank, otherwise returns the given value.
|
|
67
69
|
#
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Errgonomic
|
|
2
4
|
module Rails
|
|
5
|
+
# Adds a `delegate_optional` class method in the spirit of Rails'
|
|
6
|
+
# `delegate`, returning an Option instead of nil or NoMethodError when
|
|
7
|
+
# the delegation target is absent.
|
|
3
8
|
module ActiveRecordDelegateOptional
|
|
4
9
|
extend ActiveSupport::Concern
|
|
5
10
|
|
|
@@ -14,6 +19,7 @@ module Errgonomic
|
|
|
14
19
|
#{to}.map { |obj| obj.send(:#{method_name}) }
|
|
15
20
|
end
|
|
16
21
|
RUBY
|
|
22
|
+
send(:private, prefixed_method_name) if private
|
|
17
23
|
end
|
|
18
24
|
end
|
|
19
25
|
end
|
|
@@ -4,6 +4,21 @@ module Errgonomic
|
|
|
4
4
|
module Rails
|
|
5
5
|
# Concern to make ActiveRecord optional attributes and associations return an Option.
|
|
6
6
|
#
|
|
7
|
+
# Four pragmatic compromises below satisfy ActiveRecord's assumptions
|
|
8
|
+
# about how accessors behave. They are deliberate exceptions to "Option
|
|
9
|
+
# behaves like Rust's Option", and the set is closed: a fifth would be a
|
|
10
|
+
# signal that ActiveRecord is pushing back somewhere unmapped, deserving
|
|
11
|
+
# a design discussion rather than a quiet patch.
|
|
12
|
+
#
|
|
13
|
+
# 1. None#nil? answers true, so AR internals and ordinary nil checks
|
|
14
|
+
# treat an absent value as absent. Equality does not follow suit:
|
|
15
|
+
# None() == nil stays false.
|
|
16
|
+
# 2. Some delegates persisted?, marked_for_destruction?, and touch_later
|
|
17
|
+
# to its record, so a Some can stand in for it during persistence.
|
|
18
|
+
# 3. Two quoting prepends unwrap Options at the SQL boundary, so an
|
|
19
|
+
# Option can be passed to where/quote.
|
|
20
|
+
# 4. SomeValidator provides a presence-style validation for Option
|
|
21
|
+
# attributes.
|
|
7
22
|
module ActiveRecordOptional
|
|
8
23
|
extend ActiveSupport::Concern
|
|
9
24
|
|
|
@@ -18,8 +33,19 @@ module Errgonomic
|
|
|
18
33
|
@errgonomic_optionals.each do |name|
|
|
19
34
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
20
35
|
def #{name}
|
|
21
|
-
|
|
22
|
-
|
|
36
|
+
reads = Thread.current[:errgonomic_optional_reads] ||= {}
|
|
37
|
+
key = [object_id, :#{name}]
|
|
38
|
+
if reads[key]
|
|
39
|
+
raise Errgonomic::RecursiveOptionalReadError,
|
|
40
|
+
"\#{self.class}##{name} re-entered itself; something beneath this reader reads it again"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
reads[key] = true
|
|
44
|
+
begin
|
|
45
|
+
val = super
|
|
46
|
+
ensure
|
|
47
|
+
reads.delete(key)
|
|
48
|
+
end
|
|
23
49
|
val.nil? ? Errgonomic::Option::None.new : Errgonomic::Option::Some.new(val)
|
|
24
50
|
end
|
|
25
51
|
RUBY
|
|
@@ -35,7 +61,8 @@ module Errgonomic
|
|
|
35
61
|
end
|
|
36
62
|
end
|
|
37
63
|
|
|
38
|
-
#
|
|
64
|
+
# Validates that an Option attribute is Some, analogous to a presence
|
|
65
|
+
# validation on a plain attribute.
|
|
39
66
|
class SomeValidator < ActiveModel::EachValidator
|
|
40
67
|
def validate_each(record, attribute, value)
|
|
41
68
|
record.errors.add(attribute, 'is invalid') unless value.some?
|
|
@@ -44,17 +71,16 @@ end
|
|
|
44
71
|
|
|
45
72
|
module Errgonomic
|
|
46
73
|
module Option
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
alias none? blank?
|
|
50
|
-
end
|
|
51
|
-
|
|
74
|
+
# Delegate ActiveRecord lifecycle checks to the wrapped record, so a Some
|
|
75
|
+
# can stand in for its record during persistence.
|
|
52
76
|
class Some
|
|
53
77
|
delegate :marked_for_destruction?, to: :value
|
|
54
78
|
delegate :persisted?, to: :value
|
|
55
79
|
delegate :touch_later, to: :value
|
|
56
80
|
end
|
|
57
81
|
|
|
82
|
+
# A None answers nil? like nil itself, so ActiveRecord internals that
|
|
83
|
+
# check for nil treat an absent value as absent.
|
|
58
84
|
class None
|
|
59
85
|
def nil?
|
|
60
86
|
true
|
|
@@ -63,6 +89,8 @@ module Errgonomic
|
|
|
63
89
|
end
|
|
64
90
|
end
|
|
65
91
|
|
|
92
|
+
# Teach ActiveRecord type casting to unwrap Options: a Some casts as its
|
|
93
|
+
# inner value, a None casts as nil.
|
|
66
94
|
module ActiveRecordOptionShim
|
|
67
95
|
def type_cast(value)
|
|
68
96
|
case value
|
|
@@ -78,12 +106,14 @@ end
|
|
|
78
106
|
|
|
79
107
|
ActiveRecord::ConnectionAdapters::Quoting.prepend(ActiveRecordOptionShim)
|
|
80
108
|
|
|
109
|
+
# Lift nil into None.
|
|
81
110
|
class NilClass
|
|
82
111
|
def to_option
|
|
83
112
|
None()
|
|
84
113
|
end
|
|
85
114
|
end
|
|
86
115
|
|
|
116
|
+
# Lift any other value into Some.
|
|
87
117
|
class Object
|
|
88
118
|
def to_option
|
|
89
119
|
Some(self)
|
|
@@ -92,6 +122,8 @@ end
|
|
|
92
122
|
|
|
93
123
|
module Errgonomic
|
|
94
124
|
module Rails
|
|
125
|
+
# Teach ActiveRecord SQL quoting to unwrap Options, quoting a None as
|
|
126
|
+
# SQL NULL.
|
|
95
127
|
module ActiveRecordQuoting
|
|
96
128
|
def quote(value)
|
|
97
129
|
return super(value) unless value.is_a?(Errgonomic::Option::Any)
|
data/lib/errgonomic/rails.rb
CHANGED
data/lib/errgonomic/result.rb
CHANGED
|
@@ -6,12 +6,77 @@ module Errgonomic
|
|
|
6
6
|
# much logic as possible here, and let Ok and Err handle their
|
|
7
7
|
# initialization and self identification.
|
|
8
8
|
class Any
|
|
9
|
+
include Comparable
|
|
10
|
+
|
|
9
11
|
attr_reader :value
|
|
10
12
|
|
|
11
13
|
def initialize(value)
|
|
12
14
|
@value = value
|
|
13
15
|
end
|
|
14
16
|
|
|
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.
|
|
21
|
+
#
|
|
22
|
+
# @example
|
|
23
|
+
# (Ok(1) <=> Ok(2)) # => -1
|
|
24
|
+
# (Ok(1) <=> Err(:a)) # => -1
|
|
25
|
+
# (Err(:a) <=> Ok(1)) # => 1
|
|
26
|
+
# (Err(:a) <=> Err(:b)) # => -1
|
|
27
|
+
# (Ok(1) <=> 1) # => nil
|
|
28
|
+
# [Err(:a), Ok(2), Ok(1)].sort # => [Ok(1), Ok(2), Err(:a)]
|
|
29
|
+
def <=>(other)
|
|
30
|
+
return nil unless other.is_a?(Errgonomic::Result::Any)
|
|
31
|
+
return ok? ? -1 : 1 if self.class != other.class
|
|
32
|
+
|
|
33
|
+
value <=> other.value
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Rust spellings we accept but do not advertise: they delegate to the
|
|
37
|
+
# Ruby-idiomatic predicate and nudge the caller there via stderr.
|
|
38
|
+
RUST_SPELLINGS = {
|
|
39
|
+
is_ok: :ok?,
|
|
40
|
+
is_err: :err?,
|
|
41
|
+
is_ok_and: :ok_and?,
|
|
42
|
+
is_err_and: :err_and?
|
|
43
|
+
}.freeze
|
|
44
|
+
|
|
45
|
+
# A Result deliberately forwards nothing to its inner value, so a miss
|
|
46
|
+
# here is almost always someone treating the container as its contents.
|
|
47
|
+
# Teach the route out instead of leaving a bare NoMethodError. Rust
|
|
48
|
+
# spellings of the predicates delegate, with a nudge on stderr.
|
|
49
|
+
#
|
|
50
|
+
# @example
|
|
51
|
+
# begin
|
|
52
|
+
# Ok(5) + 1
|
|
53
|
+
# rescue NoMethodError => e
|
|
54
|
+
# e.class
|
|
55
|
+
# end # => Errgonomic::UnwrappedAccessError
|
|
56
|
+
# Ok(5).respond_to?(:+) # => false
|
|
57
|
+
# Ok(1).is_ok_and(&:odd?) # => true
|
|
58
|
+
# Err(:a).is_err # => true
|
|
59
|
+
# Ok(1).respond_to?(:is_ok) # => true
|
|
60
|
+
def method_missing(name, *args, &block)
|
|
61
|
+
if (canonical = RUST_SPELLINGS[name])
|
|
62
|
+
warn "Errgonomic: `#{name}` is the Rust spelling; prefer `#{canonical}`. Delegating."
|
|
63
|
+
return public_send(canonical, *args, &block)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
raise Errgonomic::UnwrappedAccessError.new(<<~MSG, name)
|
|
67
|
+
undefined method `#{name}' for #{inspect}, a Result, which does not forward methods to its inner value.
|
|
68
|
+
Reach for a combinator instead:
|
|
69
|
+
map, map_err, and_then, or_else: transform the value or the error
|
|
70
|
+
unwrap_or, unwrap_or_else: supply a fallback
|
|
71
|
+
ok_and?, err_and?: test a predicate against the inner value
|
|
72
|
+
unwrap!, unwrap_err!, and expect! also exist, but are intended for tests rather than application code.
|
|
73
|
+
MSG
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def respond_to_missing?(name, include_private = false)
|
|
77
|
+
RUST_SPELLINGS.key?(name) || super
|
|
78
|
+
end
|
|
79
|
+
|
|
15
80
|
# Equality comparison for Result objects is based on value not reference.
|
|
16
81
|
#
|
|
17
82
|
# @param other [Object]
|
|
@@ -28,6 +93,27 @@ module Errgonomic
|
|
|
28
93
|
value == other.value
|
|
29
94
|
end
|
|
30
95
|
|
|
96
|
+
# Hash-based collections (Hash keys, Set, uniq, group_by) use eql? and
|
|
97
|
+
# hash, not ==. Follow the inner value's own eql? semantics, so Results
|
|
98
|
+
# behave as keys exactly like their inner values.
|
|
99
|
+
#
|
|
100
|
+
# @example
|
|
101
|
+
# Ok(5).eql?(Ok(5)) # => true
|
|
102
|
+
# Ok(1).eql?(Ok(1.0)) # => false
|
|
103
|
+
# Ok(1).eql?(Err(1)) # => false
|
|
104
|
+
# { Ok(5) => 1 }[Ok(5)] # => 1
|
|
105
|
+
# [Err(:a), Err(:a)].uniq # => [Err(:a)]
|
|
106
|
+
def eql?(other)
|
|
107
|
+
self.class == other.class && value.eql?(other.value)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @example
|
|
111
|
+
# Ok(5).hash == Ok(5).hash # => true
|
|
112
|
+
# Ok(5).hash == Err(5).hash # => false
|
|
113
|
+
def hash
|
|
114
|
+
[self.class, value].hash
|
|
115
|
+
end
|
|
116
|
+
|
|
31
117
|
# Indicate that this is some kind of result object. Contrast to
|
|
32
118
|
# Object#result? which is false for all other types.
|
|
33
119
|
#
|
|
@@ -283,6 +369,12 @@ module Errgonomic
|
|
|
283
369
|
raise Errgonomic::SerializeError, 'cannot serialize an unwrapped Result'
|
|
284
370
|
end
|
|
285
371
|
|
|
372
|
+
# pp uses its own object dump unless told otherwise; keep it consistent
|
|
373
|
+
# with inspect.
|
|
374
|
+
def pretty_print(pp)
|
|
375
|
+
pp.text(inspect)
|
|
376
|
+
end
|
|
377
|
+
|
|
286
378
|
# @example simple pattern match with variable capture of the value
|
|
287
379
|
# result = Errgonomic::Result::Ok.new(1)
|
|
288
380
|
# case result
|
|
@@ -305,7 +397,6 @@ module Errgonomic
|
|
|
305
397
|
def deconstruct
|
|
306
398
|
[self, value]
|
|
307
399
|
end
|
|
308
|
-
|
|
309
400
|
end
|
|
310
401
|
|
|
311
402
|
# The Ok variant.
|
|
@@ -327,8 +418,18 @@ module Errgonomic
|
|
|
327
418
|
def err?
|
|
328
419
|
false
|
|
329
420
|
end
|
|
421
|
+
|
|
422
|
+
# Render like Rust's Debug, delegating to the inner value's inspect.
|
|
423
|
+
#
|
|
424
|
+
# @example
|
|
425
|
+
# Ok(1).inspect # => "Ok(1)"
|
|
426
|
+
# Ok("x").inspect # => "Ok(\"x\")"
|
|
427
|
+
def inspect
|
|
428
|
+
"Ok(#{value.inspect})"
|
|
429
|
+
end
|
|
330
430
|
end
|
|
331
431
|
|
|
432
|
+
# The Err variant.
|
|
332
433
|
class Err < Any
|
|
333
434
|
class Arbitrary; end
|
|
334
435
|
|
|
@@ -358,15 +459,25 @@ module Errgonomic
|
|
|
358
459
|
def ok?
|
|
359
460
|
false
|
|
360
461
|
end
|
|
462
|
+
|
|
463
|
+
# Render like Rust's Debug; a value-less Err renders bare.
|
|
464
|
+
#
|
|
465
|
+
# @example
|
|
466
|
+
# Err(:nope).inspect # => "Err(:nope)"
|
|
467
|
+
# Err().inspect # => "Err()"
|
|
468
|
+
def inspect
|
|
469
|
+
return 'Err()' if value == Arbitrary
|
|
470
|
+
|
|
471
|
+
"Err(#{value.inspect})"
|
|
472
|
+
end
|
|
361
473
|
end
|
|
362
474
|
end
|
|
363
475
|
end
|
|
364
476
|
|
|
365
|
-
# Introduce
|
|
366
|
-
#
|
|
367
|
-
#
|
|
368
|
-
#
|
|
369
|
-
# "foo".assert_result! # => raise Errgonomic::ResultRequiredError
|
|
477
|
+
# Introduce result-ness helpers into the Object class. No doctests here:
|
|
478
|
+
# several files reopen Object, and YARD keeps only one docstring for it, so
|
|
479
|
+
# examples on the class itself can be silently dropped. Each method carries
|
|
480
|
+
# its own examples instead.
|
|
370
481
|
class Object
|
|
371
482
|
# Convenience method to indicate whether we are working with a result.
|
|
372
483
|
# TBD whether we implement some stubs for the rest of the Result API; I want
|
data/lib/errgonomic/type.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Runtime type assertions for every object: keep the receiver when it matches
|
|
4
|
+
# the expected type, otherwise substitute, compute, or raise.
|
|
3
5
|
class Object
|
|
4
6
|
# Returns the receiver if it matches the expected type, otherwise raises a TypeMismatchError.
|
|
5
7
|
# This is useful for enforcing type expectations in method arguments.
|
data/lib/errgonomic/version.rb
CHANGED
data/lib/errgonomic.rb
CHANGED
|
@@ -12,6 +12,13 @@ require_relative 'errgonomic/type'
|
|
|
12
12
|
require_relative 'errgonomic/option'
|
|
13
13
|
require_relative 'errgonomic/result'
|
|
14
14
|
|
|
15
|
+
# Option-returning lookups for the collections.
|
|
16
|
+
require_relative 'errgonomic/core_ext/hash'
|
|
17
|
+
require_relative 'errgonomic/core_ext/array'
|
|
18
|
+
|
|
19
|
+
# Lift booleans into Option and Result.
|
|
20
|
+
require_relative 'errgonomic/core_ext/bool'
|
|
21
|
+
|
|
15
22
|
# Rails fu
|
|
16
23
|
require_relative 'errgonomic/rails' if defined?(Rails::Railtie)
|
|
17
24
|
|
|
@@ -29,6 +36,8 @@ module Errgonomic
|
|
|
29
36
|
|
|
30
37
|
class TypeMismatchError < Error; end
|
|
31
38
|
|
|
39
|
+
# Raised when unwrap! is called on a None or an Err. Carries the Err's
|
|
40
|
+
# inner value so diagnostics can show what actually went wrong.
|
|
32
41
|
class UnwrapError < Error
|
|
33
42
|
attr_reader :value
|
|
34
43
|
|
|
@@ -44,8 +53,18 @@ module Errgonomic
|
|
|
44
53
|
|
|
45
54
|
class ResultRequiredError < Error; end
|
|
46
55
|
|
|
56
|
+
# Raised when a wrapped ActiveRecord attribute reader re-enters itself on
|
|
57
|
+
# the same record, catching runaway recursion at the first repeated frame
|
|
58
|
+
# instead of a SystemStackError thousands of frames later.
|
|
59
|
+
class RecursiveOptionalReadError < Error; end
|
|
60
|
+
|
|
47
61
|
class NotComparableError < StandardError; end
|
|
48
62
|
|
|
63
|
+
# Raised when a method missing from Option or Result is called, with a
|
|
64
|
+
# message that teaches the combinators. Subclasses NoMethodError so every
|
|
65
|
+
# rescue path and Ruby-internal probe that expects one keeps working.
|
|
66
|
+
class UnwrappedAccessError < ::NoMethodError; end
|
|
67
|
+
|
|
49
68
|
class SerializeError < TypeError; end
|
|
50
69
|
|
|
51
70
|
# A little bit of control over how pedantic we are in our runtime type checks.
|
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.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nick Zadrozny
|
|
@@ -66,16 +66,23 @@ files:
|
|
|
66
66
|
- ".yardopts"
|
|
67
67
|
- CHANGELOG.md
|
|
68
68
|
- CODE_OF_CONDUCT.md
|
|
69
|
+
- CONTRIBUTING.md
|
|
69
70
|
- LICENSE.txt
|
|
70
71
|
- README.md
|
|
71
72
|
- Rakefile
|
|
72
73
|
- doctest_helper.rb
|
|
73
74
|
- flake.lock
|
|
74
75
|
- flake.nix
|
|
75
|
-
-
|
|
76
|
+
- gem-groups.json
|
|
76
77
|
- lib/errgonomic.rb
|
|
78
|
+
- lib/errgonomic/core_ext/array.rb
|
|
77
79
|
- lib/errgonomic/core_ext/blank.rb
|
|
80
|
+
- lib/errgonomic/core_ext/bool.rb
|
|
81
|
+
- lib/errgonomic/core_ext/hash.rb
|
|
78
82
|
- lib/errgonomic/option.rb
|
|
83
|
+
- lib/errgonomic/optional_array.rb
|
|
84
|
+
- lib/errgonomic/optional_dig.rb
|
|
85
|
+
- lib/errgonomic/optional_hash.rb
|
|
79
86
|
- lib/errgonomic/presence.rb
|
|
80
87
|
- lib/errgonomic/rails.rb
|
|
81
88
|
- lib/errgonomic/rails/active_record_delegate_optional.rb
|
|
@@ -104,7 +111,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
104
111
|
- !ruby/object:Gem::Version
|
|
105
112
|
version: '0'
|
|
106
113
|
requirements: []
|
|
107
|
-
rubygems_version: 3.
|
|
114
|
+
rubygems_version: 3.7.2
|
|
108
115
|
specification_version: 4
|
|
109
116
|
summary: Opinionated, ergonomic error handling for Ruby, inspired by Rails and Rust.
|
|
110
117
|
test_files: []
|