errgonomic 0.9.1 → 0.9.3
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/CHANGELOG.md +24 -0
- data/README.md +1 -1
- data/lib/errgonomic/core_ext/enumerable.rb +2 -2
- data/lib/errgonomic/option.rb +83 -8
- data/lib/errgonomic/result.rb +27 -8
- data/lib/errgonomic/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 58963c6e1c1cfcf52d8fe4b065dd986acda616f4e55001d7d2bc13cbeab1eb28
|
|
4
|
+
data.tar.gz: 9d6f93fdd27c94525f2f2a2dc53010eb4f99760dae7514eaf4f035f5c9947869
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2adf6eadd64c94629ba265ea9747d943314616f8c26943ad075ea5399bc86c4747e433946273989640b668e0841c337cdc6f3ecc873fed9006aa82eb439510d9
|
|
7
|
+
data.tar.gz: 475c6a42553b89700d4de4d0d7257bcbb71388c61d8b12da69a51ce4aa30a75f7074757a498bb20d5074539748b8e799bf8c78303ef3dc8ac33a74306d615aad
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.9.3] - 2026-09-10
|
|
4
|
+
|
|
5
|
+
This release removes the public `value` slot from `Some`, `Ok` and `Err`, and freezes every instance as it is constructed, so an Option or a Result is the value the README already said it was.
|
|
6
|
+
|
|
7
|
+
### Upgrading from 0.9.2
|
|
8
|
+
|
|
9
|
+
`value` and `value=` are gone from `Some`, `Ok` and `Err`, and every instance is frozen as it is constructed. A read of `.value` becomes `unwrap_or(fallback)`, `expect!(message)`, `map`, `and_then` or a pattern, each of which names the other branch; a write of `.value=` becomes a new `Some(v)` assigned where the old one lived. A call to either now raises `Errgonomic::UnwrappedAccessError`, which is a `NoMethodError`, naming the combinators.
|
|
10
|
+
|
|
11
|
+
### Changes
|
|
12
|
+
|
|
13
|
+
- [Behavior change] `Some`, `Ok` and `Err` no longer expose `value` or `value=`, and every Option and Result is frozen as it is constructed, whether by `Some`, `None`, `Ok`, `Err`, `new` or a combinator; `clone` keeps it frozen. A copy that skips construction, from `dup`, `Marshal.load`, a YAML load or ActiveSupport's `deep_dup`, is not frozen; with no writer, it changes only through `instance_variable_set`. The reader reached the inner value with no `None` branch, the writer mutated a wrapper through an alias and moved a Hash key out from under its own bucket, and the README already said an Option is a value rather than a slot. The reader is protected, for the sibling reads equality, ordering and `zip` need; a call from outside gets the combinator teaching `Errgonomic::UnwrappedAccessError` gives any other miss
|
|
14
|
+
|
|
15
|
+
## [0.9.2] - 2026-09-10
|
|
16
|
+
|
|
17
|
+
This release makes `Option#and`, `#xor`, `#zip` and `#zip_with` check their operand the way `or` already did.
|
|
18
|
+
|
|
19
|
+
### Upgrading from 0.9.1
|
|
20
|
+
|
|
21
|
+
`Option#and`, `#xor`, `#zip` and `#zip_with` raise `Errgonomic::ArgumentError` on a bare operand, on a `None` receiver as well as a `Some`. Code that passed a bare value to `and` and read it back has to wrap it.
|
|
22
|
+
|
|
23
|
+
### Changes
|
|
24
|
+
|
|
25
|
+
- [Behavior change] `Option#and`, `#xor`, `#zip` and `#zip_with` check their operand the way `or` already did, raising `Errgonomic::ArgumentError` (`other must be an Option, was Integer`) before the receiver's variant is consulted. 0.9.x let `Some(2).and(3)` hand back the bare `3`, let `None().and(3)` and `None().zip(2)` accept the operand silently, and let `Some(1).zip(2)` and `Some(:l).xor(:r)` fall into a bare `NoMethodError` on `some?` or `none?`
|
|
26
|
+
|
|
3
27
|
## [0.9.1] - 2026-09-10
|
|
4
28
|
|
|
5
29
|
This release reverts the 0.9.0 change that made `to_s` render an Option or a Result. The raise is back, with a message that says what to call instead.
|
data/README.md
CHANGED
|
@@ -144,7 +144,7 @@ Writers unwrap under that integration, which changes what a truthiness slip cost
|
|
|
144
144
|
|
|
145
145
|
The remaining present-side helpers are soft-deprecated on Options in favor of the combinators. They unwrap, where on any other object they return the receiver: `Some(v).present_or_raise!(msg)`, `present_or(default)` and `present_or_else { }` all yield `v`, and `None` raises, substitutes, or computes. Each prints a one-line stderr nudge naming the combinator to use instead (`expect!`, `unwrap_or`, `unwrap_or_else`), once per process per method rather than once per call, so a hot path does not flood the log. The blank side (`blank_or*`) raises `UnwrappedAccessError` outright: an Option's blankness is its discriminant, so test it with `none?`.
|
|
146
146
|
|
|
147
|
-
Four of Rust's methods are deliberately absent: `take`, `replace`, `insert` and `get_or_insert`. Every one of them writes through an `&mut Option`, and an Option here is a value rather than a slot: `Some(1)` is something you pass around and compare, not a cell whose contents you swap out from under another reference. Build the Option you want and assign it where the old one lived.
|
|
147
|
+
Four of Rust's methods are deliberately absent: `take`, `replace`, `insert` and `get_or_insert`. Every one of them writes through an `&mut Option`, and an Option here is a value rather than a slot: `Some(1)` is something you pass around and compare, not a cell whose contents you swap out from under another reference. Build the Option you want and assign it where the old one lived. The same rule is why a `Some`, an `Ok` and an `Err` have no `value` reader or writer, and why every instance is frozen as it is constructed. A copy that skips construction, from `dup`, `Marshal.load`, a YAML load or ActiveSupport's `deep_dup`, is not frozen; with no writer, it changes only through `instance_variable_set`. A reader would reach the inner value with no `None` branch, and a writer would move a Hash key out from under its own bucket. Reach in with `unwrap_or`, `expect!`, `map`, `and_then` or a pattern, each of which names what happens on the other branch.
|
|
148
148
|
|
|
149
149
|
Equality is between Options only: `Some(5) == Some(5)`, but `Some(5) == 5` and `None() == nil` are `false`. That is quiet, never an error, matching how every Ruby object compares across types. Rust rejects `Some(5) == 5` at compile time; Ruby cannot, so guard the idiom in review and tests: compare against a wrapped value (`opt == Some(5)`) or test the inner value (`opt.some_and? { |v| v == 5 }`). `Errgonomic.strict_equality = true` turns that guard into an error, which is what a test suite wants; see [Pedantic runtime checks](#pedantic-runtime-checks).
|
|
150
150
|
|
|
@@ -46,7 +46,7 @@ module Enumerable
|
|
|
46
46
|
end
|
|
47
47
|
return None() if member.none?
|
|
48
48
|
|
|
49
|
-
values <<
|
|
49
|
+
member.tap_some { |value| values << value }
|
|
50
50
|
end
|
|
51
51
|
Some(values)
|
|
52
52
|
end
|
|
@@ -87,7 +87,7 @@ module Enumerable
|
|
|
87
87
|
end
|
|
88
88
|
return member if member.err?
|
|
89
89
|
|
|
90
|
-
values <<
|
|
90
|
+
member.tap_ok { |value| values << value }
|
|
91
91
|
end
|
|
92
92
|
Ok(values)
|
|
93
93
|
end
|
data/lib/errgonomic/option.rb
CHANGED
|
@@ -178,7 +178,7 @@ module Errgonomic
|
|
|
178
178
|
# measurement = Errgonomic::Option::Some.new(1)
|
|
179
179
|
# case measurement
|
|
180
180
|
# in Errgonomic::Option::Some, value
|
|
181
|
-
# "Measurement is #{
|
|
181
|
+
# "Measurement is #{value}"
|
|
182
182
|
# in Errgonomic::Option::None
|
|
183
183
|
# "Measurement is not available"
|
|
184
184
|
# else
|
|
@@ -594,10 +594,10 @@ module Errgonomic
|
|
|
594
594
|
# @example
|
|
595
595
|
# None().or(Some(1)) # => Some(1)
|
|
596
596
|
# Some(2).or(Some(3)) # => Some(2)
|
|
597
|
-
# None().or(2) # => raise Errgonomic::ArgumentError
|
|
597
|
+
# None().or(2) # => raise Errgonomic::ArgumentError, "other must be an Option, was Integer"
|
|
598
|
+
# Some(1).or(2) # => raise Errgonomic::ArgumentError, "other must be an Option, was Integer"
|
|
598
599
|
def or(other)
|
|
599
|
-
|
|
600
|
-
|
|
600
|
+
option_operand!(other)
|
|
601
601
|
return self if some?
|
|
602
602
|
|
|
603
603
|
other
|
|
@@ -620,12 +620,17 @@ module Errgonomic
|
|
|
620
620
|
val
|
|
621
621
|
end
|
|
622
622
|
|
|
623
|
-
# If self is Some, return the provided other Option.
|
|
623
|
+
# If self is Some, return the provided other Option. The operand is
|
|
624
|
+
# checked on both variants, so a None-heavy path still learns that it
|
|
625
|
+
# was handed a bare value.
|
|
624
626
|
#
|
|
625
627
|
# @example
|
|
626
628
|
# None().and(Some(1)) # => None()
|
|
627
629
|
# Some(2).and(Some(3)) # => Some(3)
|
|
630
|
+
# Some(2).and(3) # => raise Errgonomic::ArgumentError, "other must be an Option, was Integer"
|
|
631
|
+
# None().and(3) # => raise Errgonomic::ArgumentError, "other must be an Option, was Integer"
|
|
628
632
|
def and(other)
|
|
633
|
+
option_operand!(other)
|
|
629
634
|
return self if none?
|
|
630
635
|
|
|
631
636
|
other
|
|
@@ -657,7 +662,10 @@ module Errgonomic
|
|
|
657
662
|
# None().zip(Some(1)) # => None()
|
|
658
663
|
# Some(1).zip(None()) # => None()
|
|
659
664
|
# Some(2).zip(Some(3)) # => Some([2, 3])
|
|
665
|
+
# Some(1).zip(2) # => raise Errgonomic::ArgumentError, "other must be an Option, was Integer"
|
|
666
|
+
# None().zip(2) # => raise Errgonomic::ArgumentError, "other must be an Option, was Integer"
|
|
660
667
|
def zip(other)
|
|
668
|
+
option_operand!(other)
|
|
661
669
|
return None() unless some? && other.some?
|
|
662
670
|
|
|
663
671
|
Some([value, other.value])
|
|
@@ -671,7 +679,10 @@ module Errgonomic
|
|
|
671
679
|
# None().zip_with(Some(1)) { |a, b| a + b } # => None()
|
|
672
680
|
# Some(1).zip_with(None()) { |a, b| a + b } # => None()
|
|
673
681
|
# Some(2).zip_with(Some(3)) { |a, b| a + b } # => Some(5)
|
|
682
|
+
# Some(1).zip_with(2) { |a, b| a + b } # => raise Errgonomic::ArgumentError, "other must be an Option, was Integer"
|
|
683
|
+
# None().zip_with(2) { |a, b| a + b } # => raise Errgonomic::ArgumentError, "other must be an Option, was Integer"
|
|
674
684
|
def zip_with(other, &block)
|
|
685
|
+
option_operand!(other)
|
|
675
686
|
return None() unless some? && other.some?
|
|
676
687
|
|
|
677
688
|
other = block.call(value, other.value)
|
|
@@ -767,8 +778,10 @@ module Errgonomic
|
|
|
767
778
|
# Some(:left).xor(Some(:right)) # => None()
|
|
768
779
|
# Some(:left).xor(None()) #=> Some(:left)
|
|
769
780
|
# None().xor(Some(:right)) #=> Some(:right)
|
|
770
|
-
#
|
|
781
|
+
# Some(:left).xor(:right) # => raise Errgonomic::ArgumentError, "other must be an Option, was Symbol"
|
|
782
|
+
# None().xor(:right) # => raise Errgonomic::ArgumentError, "other must be an Option, was Symbol"
|
|
771
783
|
def xor(other)
|
|
784
|
+
option_operand!(other)
|
|
772
785
|
return self if some? && other.none?
|
|
773
786
|
return other if other.some? && none?
|
|
774
787
|
|
|
@@ -777,6 +790,14 @@ module Errgonomic
|
|
|
777
790
|
|
|
778
791
|
private
|
|
779
792
|
|
|
793
|
+
# Checked before the discriminant is consulted, so a None-heavy path
|
|
794
|
+
# learns about a bare operand as soon as a Some-heavy one would.
|
|
795
|
+
def option_operand!(other)
|
|
796
|
+
return if other.is_a?(Errgonomic::Option::Any)
|
|
797
|
+
|
|
798
|
+
raise Errgonomic::ArgumentError, "other must be an Option, was #{other.class.name}"
|
|
799
|
+
end
|
|
800
|
+
|
|
780
801
|
def to_s_refusal
|
|
781
802
|
"#{bounded_inspect} refuses to_s; use inspect for a log line, or unwrap_or / expect! for the value"
|
|
782
803
|
end
|
|
@@ -830,11 +851,47 @@ module Errgonomic
|
|
|
830
851
|
|
|
831
852
|
# Represent a value
|
|
832
853
|
class Some < Any
|
|
833
|
-
|
|
834
|
-
|
|
854
|
+
# A Some is a value, not a slot: nothing outside reads the inner value
|
|
855
|
+
# without handling the None branch, and nothing swaps it out from under
|
|
856
|
+
# another reference or a Hash key.
|
|
857
|
+
#
|
|
858
|
+
# @example the inner value is reached through a combinator, never a reader
|
|
859
|
+
# begin
|
|
860
|
+
# Some(1).value
|
|
861
|
+
# rescue NoMethodError => e
|
|
862
|
+
# e.class
|
|
863
|
+
# end # => Errgonomic::UnwrappedAccessError
|
|
864
|
+
# Some(1).respond_to?(:value) # => false
|
|
865
|
+
#
|
|
866
|
+
# @example a Some cannot be mutated through an alias
|
|
867
|
+
# a = Some(1)
|
|
868
|
+
# b = a
|
|
869
|
+
# begin
|
|
870
|
+
# b.value = 99
|
|
871
|
+
# rescue NoMethodError => e
|
|
872
|
+
# e.class
|
|
873
|
+
# end # => Errgonomic::UnwrappedAccessError
|
|
874
|
+
# a # => Some(1)
|
|
875
|
+
# Some(1).frozen? # => true
|
|
876
|
+
# begin
|
|
877
|
+
# Some(1).instance_variable_set(:@value, 2)
|
|
878
|
+
# rescue FrozenError => e
|
|
879
|
+
# e.class
|
|
880
|
+
# end # => FrozenError
|
|
881
|
+
#
|
|
882
|
+
# @example a Some keeps its place as a Hash key
|
|
883
|
+
# k = Some(1)
|
|
884
|
+
# h = { k => :v }
|
|
885
|
+
# begin
|
|
886
|
+
# k.value = 2
|
|
887
|
+
# rescue NoMethodError
|
|
888
|
+
# nil
|
|
889
|
+
# end
|
|
890
|
+
# h[k] # => :v
|
|
835
891
|
def initialize(value)
|
|
836
892
|
super()
|
|
837
893
|
@value = value
|
|
894
|
+
freeze
|
|
838
895
|
end
|
|
839
896
|
|
|
840
897
|
def some?
|
|
@@ -856,10 +913,28 @@ module Errgonomic
|
|
|
856
913
|
def inspect
|
|
857
914
|
"Some(#{value.inspect})"
|
|
858
915
|
end
|
|
916
|
+
|
|
917
|
+
protected
|
|
918
|
+
|
|
919
|
+
# Sibling instances read each other's value for equality, ordering and
|
|
920
|
+
# zip; nothing else does.
|
|
921
|
+
attr_reader :value
|
|
859
922
|
end
|
|
860
923
|
|
|
861
924
|
# Represent the absence of a value.
|
|
862
925
|
class None < Any
|
|
926
|
+
# @example a None has no value to read, and says so the same way a Some does
|
|
927
|
+
# begin
|
|
928
|
+
# None().value
|
|
929
|
+
# rescue NoMethodError => e
|
|
930
|
+
# e.class
|
|
931
|
+
# end # => Errgonomic::UnwrappedAccessError
|
|
932
|
+
# None().frozen? # => true
|
|
933
|
+
def initialize
|
|
934
|
+
super
|
|
935
|
+
freeze
|
|
936
|
+
end
|
|
937
|
+
|
|
863
938
|
def some?
|
|
864
939
|
false
|
|
865
940
|
end
|
data/lib/errgonomic/result.rb
CHANGED
|
@@ -8,10 +8,27 @@ module Errgonomic
|
|
|
8
8
|
class Any
|
|
9
9
|
include Comparable
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
# A Result is a value, not a slot: the inner value is reached through a
|
|
12
|
+
# combinator that handles the other variant, and nothing swaps it out
|
|
13
|
+
# from under another reference.
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# begin
|
|
17
|
+
# Ok(1).value
|
|
18
|
+
# rescue NoMethodError => e
|
|
19
|
+
# e.class
|
|
20
|
+
# end # => Errgonomic::UnwrappedAccessError
|
|
21
|
+
# begin
|
|
22
|
+
# Err(:x).value = :y
|
|
23
|
+
# rescue NoMethodError => e
|
|
24
|
+
# e.class
|
|
25
|
+
# end # => Errgonomic::UnwrappedAccessError
|
|
26
|
+
# Ok(1).respond_to?(:value) # => false
|
|
27
|
+
# Ok(1).frozen? # => true
|
|
28
|
+
# Err().frozen? # => true
|
|
13
29
|
def initialize(value)
|
|
14
30
|
@value = value
|
|
31
|
+
freeze
|
|
15
32
|
end
|
|
16
33
|
|
|
17
34
|
# Results order like Rust's: Ok sorts before any Err, and same variants
|
|
@@ -477,6 +494,12 @@ module Errgonomic
|
|
|
477
494
|
[self, value]
|
|
478
495
|
end
|
|
479
496
|
|
|
497
|
+
protected
|
|
498
|
+
|
|
499
|
+
# Sibling instances read each other's value for equality and ordering;
|
|
500
|
+
# nothing else does.
|
|
501
|
+
attr_reader :value
|
|
502
|
+
|
|
480
503
|
private
|
|
481
504
|
|
|
482
505
|
def to_s_refusal
|
|
@@ -512,8 +535,6 @@ module Errgonomic
|
|
|
512
535
|
|
|
513
536
|
# The Ok variant.
|
|
514
537
|
class Ok < Any
|
|
515
|
-
attr_accessor :value
|
|
516
|
-
|
|
517
538
|
# Ok is always ok
|
|
518
539
|
#
|
|
519
540
|
# @example
|
|
@@ -544,13 +565,11 @@ module Errgonomic
|
|
|
544
565
|
class Err < Any
|
|
545
566
|
class Arbitrary; end
|
|
546
567
|
|
|
547
|
-
attr_accessor :value
|
|
548
|
-
|
|
549
568
|
# Err may be constructed without a value, if you want.
|
|
550
569
|
#
|
|
551
570
|
# @example
|
|
552
|
-
# Err(:y).
|
|
553
|
-
# Err().
|
|
571
|
+
# Err(:y).unwrap_err! # => :y
|
|
572
|
+
# Err().unwrap_err! # => Arbitrary
|
|
554
573
|
def initialize(value = Arbitrary)
|
|
555
574
|
super(value)
|
|
556
575
|
end
|
data/lib/errgonomic/version.rb
CHANGED