errgonomic 0.7.0 → 0.8.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.
@@ -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
@@ -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
- alias_method :present_or_raise, :present_or_raise!
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
- alias_method :blank_or_raise, :blank_or_raise!
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,9 +1,25 @@
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
 
6
11
  class_methods do
12
+ # Names attributes that ActiveRecordOptional must leave alone. It has
13
+ # to be callable before the include, which is what computes the
14
+ # wrapped set, so it lives here rather than in the concern itself.
15
+ def errgonomic_optional_except(*names)
16
+ @errgonomic_optional_exceptions = errgonomic_optional_exceptions + names.map(&:to_s)
17
+ end
18
+
19
+ def errgonomic_optional_exceptions
20
+ @errgonomic_optional_exceptions ||= []
21
+ end
22
+
7
23
  def delegate_optional(*methods, to: nil, prefix: nil, private: nil)
8
24
  return if to.nil?
9
25
 
@@ -14,6 +30,7 @@ module Errgonomic
14
30
  #{to}.map { |obj| obj.send(:#{method_name}) }
15
31
  end
16
32
  RUBY
33
+ send(:private, prefixed_method_name) if private
17
34
  end
18
35
  end
19
36
  end
@@ -4,6 +4,28 @@ module Errgonomic
4
4
  module Rails
5
5
  # Concern to make ActiveRecord optional attributes and associations return an Option.
6
6
  #
7
+ # Five 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 sixth 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. Quoting and predicate-building prepends unwrap Options at the SQL
19
+ # boundary, so an Option can be passed to where/quote.
20
+ # 4. SomeValidator provides a presence-style validation for Option
21
+ # attributes.
22
+ # 5. Attributes declared with encrypts are never wrapped: ActiveRecord
23
+ # Encryption registers a length validator outside Model.validators
24
+ # that reads the raw value and cannot survive an Option.
25
+ #
26
+ # errgonomic_optional_except is not on the list: it is configuration, an
27
+ # escape hatch for whatever conflict shows up next, not a semantic
28
+ # exception.
7
29
  module ActiveRecordOptional
8
30
  extend ActiveSupport::Concern
9
31
 
@@ -12,14 +34,27 @@ module Errgonomic
12
34
  optional_associations = reflect_on_all_associations(:belongs_to)
13
35
  .select { |r| r.options[:optional] }
14
36
  .map(&:name)
37
+ excluded = Array(encrypted_attributes).map(&:to_s) + Array(try(:errgonomic_optional_exceptions))
15
38
  optional_attributes = column_names
16
39
  .select { |n| column_for_attribute(n).null }
40
+ .reject { |n| excluded.include?(n) }
17
41
  @errgonomic_optionals = (optional_attributes + optional_associations)
18
42
  @errgonomic_optionals.each do |name|
19
43
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
20
44
  def #{name}
21
- raise "stack too deep" if caller.length > 1024
22
- val = super
45
+ reads = Thread.current[:errgonomic_optional_reads] ||= {}
46
+ key = [object_id, :#{name}]
47
+ if reads[key]
48
+ raise Errgonomic::RecursiveOptionalReadError,
49
+ "\#{self.class}##{name} re-entered itself; something beneath this reader reads it again"
50
+ end
51
+
52
+ reads[key] = true
53
+ begin
54
+ val = super
55
+ ensure
56
+ reads.delete(key)
57
+ end
23
58
  val.nil? ? Errgonomic::Option::None.new : Errgonomic::Option::Some.new(val)
24
59
  end
25
60
  RUBY
@@ -30,12 +65,30 @@ module Errgonomic
30
65
  def errgonomic_optionals
31
66
  @errgonomic_optionals
32
67
  end
68
+
69
+ # Encryption surrounds an attribute with machinery that reads the raw
70
+ # value, including a length validator that calls to_s on it, so a
71
+ # wrapped encrypted attribute cannot be saved. Declaring encrypts
72
+ # after the include is the ordinary spelling, so catch it here too and
73
+ # give the attribute its plain reader back.
74
+ def encrypts(*names, **options)
75
+ super.tap { errgonomic_unwrap_optionals(*names) }
76
+ end
77
+
78
+ def errgonomic_unwrap_optionals(*names)
79
+ names.map(&:to_s).each do |name|
80
+ next unless @errgonomic_optionals&.delete(name)
81
+
82
+ remove_method(name)
83
+ end
84
+ end
33
85
  end
34
86
  end
35
87
  end
36
88
  end
37
89
 
38
- # do we need this since we alias present below?
90
+ # Validates that an Option attribute is Some, analogous to a presence
91
+ # validation on a plain attribute.
39
92
  class SomeValidator < ActiveModel::EachValidator
40
93
  def validate_each(record, attribute, value)
41
94
  record.errors.add(attribute, 'is invalid') unless value.some?
@@ -44,17 +97,16 @@ end
44
97
 
45
98
  module Errgonomic
46
99
  module Option
47
- class Any
48
- alias some? present?
49
- alias none? blank?
50
- end
51
-
100
+ # Delegate ActiveRecord lifecycle checks to the wrapped record, so a Some
101
+ # can stand in for its record during persistence.
52
102
  class Some
53
103
  delegate :marked_for_destruction?, to: :value
54
104
  delegate :persisted?, to: :value
55
105
  delegate :touch_later, to: :value
56
106
  end
57
107
 
108
+ # A None answers nil? like nil itself, so ActiveRecord internals that
109
+ # check for nil treat an absent value as absent.
58
110
  class None
59
111
  def nil?
60
112
  true
@@ -63,6 +115,8 @@ module Errgonomic
63
115
  end
64
116
  end
65
117
 
118
+ # Teach ActiveRecord type casting to unwrap Options: a Some casts as its
119
+ # inner value, a None casts as nil.
66
120
  module ActiveRecordOptionShim
67
121
  def type_cast(value)
68
122
  case value
@@ -78,12 +132,14 @@ end
78
132
 
79
133
  ActiveRecord::ConnectionAdapters::Quoting.prepend(ActiveRecordOptionShim)
80
134
 
135
+ # Lift nil into None.
81
136
  class NilClass
82
137
  def to_option
83
138
  None()
84
139
  end
85
140
  end
86
141
 
142
+ # Lift any other value into Some.
87
143
  class Object
88
144
  def to_option
89
145
  Some(self)
@@ -92,6 +148,8 @@ end
92
148
 
93
149
  module Errgonomic
94
150
  module Rails
151
+ # Teach ActiveRecord SQL quoting to unwrap Options, quoting a None as
152
+ # SQL NULL.
95
153
  module ActiveRecordQuoting
96
154
  def quote(value)
97
155
  return super(value) unless value.is_a?(Errgonomic::Option::Any)
@@ -104,3 +162,33 @@ module Errgonomic
104
162
  end
105
163
 
106
164
  ActiveRecord::ConnectionAdapters::Quoting.prepend(Errgonomic::Rails::ActiveRecordQuoting)
165
+
166
+ module Errgonomic
167
+ module Rails
168
+ # A hash condition never reaches the quoting layer as its raw value: the
169
+ # predicate builder hands it to a bind attribute, which serializes it
170
+ # through the column type and casts an unrecognized object to nil. Unwrap
171
+ # one step earlier, where every hash condition passes, so a Some binds as
172
+ # its inner value and a None as nil, which Arel renders as IS NULL.
173
+ module ActiveRecordPredicateBuilder
174
+ def build(attribute, value, *args)
175
+ super(attribute, Errgonomic::Rails.unwrap_options(value), *args)
176
+ end
177
+ end
178
+
179
+ # Unwrap Options in a query condition, reaching one level into an array
180
+ # so a list of Options binds like a list of values.
181
+ def self.unwrap_options(value)
182
+ case value
183
+ when Errgonomic::Option::Any
184
+ value.unwrap_or(nil)
185
+ when Array
186
+ value.any? { |v| v.is_a?(Errgonomic::Option::Any) } ? value.map { |v| unwrap_options(v) } : value
187
+ else
188
+ value
189
+ end
190
+ end
191
+ end
192
+ end
193
+
194
+ ActiveRecord::PredicateBuilder.prepend(Errgonomic::Rails::ActiveRecordPredicateBuilder)
@@ -1,4 +1,6 @@
1
- require_relative 'option'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../errgonomic'
2
4
  require_relative 'rails/active_record_optional'
3
5
  require_relative 'rails/active_record_delegate_optional'
4
6
 
@@ -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 certain helper methods into the Object class.
366
- #
367
- # @example
368
- # "foo".result? # => false
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
@@ -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.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Errgonomic
4
- VERSION = '0.7.0'
4
+ VERSION = '0.8.1'
5
5
  end
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,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: errgonomic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Zadrozny
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 1980-01-01 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: concurrent-ruby
@@ -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
- - gemset.nix
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.6.6
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: []