errgonomic 0.10.0 → 0.10.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/CHANGELOG.md +15 -0
- data/CONTRIBUTING.md +1 -2
- data/README.md +16 -14
- data/Rakefile +1 -9
- data/lib/errgonomic/option.rb +83 -60
- data/lib/errgonomic/rails/active_record_optional.rb +4 -3
- data/lib/errgonomic/result.rb +35 -42
- data/lib/errgonomic/version.rb +1 -1
- data/lib/errgonomic.rb +0 -37
- 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: 543390fc5f85546ffde4785cfc41951400800094506078271cfe32535e9d784a
|
|
4
|
+
data.tar.gz: 8d5cdf061dbf7026eaab5ba8cab628368c600943699dbad04235c9d1d9bd3f5a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ad7cbe42d7f1d65549323c026a3f4486d4e64a9b5274023fc3eebb0a576ceb09c030151d176512220e13ef8d6e1bc3c2bff78162783ecb286bbb6805ac16c8c9
|
|
7
|
+
data.tar.gz: 4588909f84b800a1c497882d030fd88f23606146fe64b7ae02ecc8b7f4406ba2c5dd749d0c709c7004717de80a9a69e73d6247b2ce11013446fa25db66271c3d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.10.1] - 2026-09-10
|
|
4
|
+
|
|
5
|
+
This release makes cross-type equality raise unconditionally and removes the switch that used to turn it on.
|
|
6
|
+
|
|
7
|
+
### Upgrading from 0.10.0
|
|
8
|
+
|
|
9
|
+
`Errgonomic.strict_equality=`, `Errgonomic.strict_equality?` and `Errgonomic.with_strict_equality` are removed, along with `rake test:strict`, because cross-type equality always raises now. A `Some(x) == x`, `!= x`, `eql?(x)` or `=== x` anywhere in an application raises `Errgonomic::TypeMismatchError` where 0.10.0 answered `false` unless strict equality was switched on, and so does a collection operation that compares pairwise and asks the wrapper (`[Some(x)].include?(x)`, `Array#index`, `Array#-`, `Array#==`, `Hash#==`, `case x when Some(x)`) and a Minitest `assert_equal` whose expected value is a wrapper. Which operations raise, by which side holds the wrapper, is listed in the README's Strict equality section. Compare wrappers (`opt == Some(x)`), test the inner value (`opt.some_and? { |v| v == x }`), or `unwrap_or` a fallback first. Delete any `Errgonomic.strict_equality = true` in a test helper; it has nothing left to set.
|
|
10
|
+
|
|
11
|
+
### Changes
|
|
12
|
+
|
|
13
|
+
- [Behavior change] `==`, `!=`, `eql?` and `===` between an Option or a Result and a value that is not one always raise `Errgonomic::TypeMismatchError`, with the message 0.9.0 gave under the flag. `Some(1) == 1` answering `false` is the silent wrong branch that mirrors a wrapper written into a string, and 0.9.0 had the safe behavior opt-in. `===` is defined so `case 5 when Some(5)` and a pinned pattern raise under the operator that was written. `eql?` is not exempted: Ruby's hashing compares hash values first and asks `eql?` only of a candidate whose hash matches, so `Hash#[]`, `Set` and `uniq` stay quiet while `Array#include?`, `Array#-`, `Array#==` and `case/when` compare pairwise and raise when the wrapper is the operand asked. Strict equality never answers wrong, it only sometimes fails to catch, and `nil == Some(1)` is answered by `NilClass` and cannot be intercepted. The README states the whole surface
|
|
14
|
+
- `Errgonomic.strict_equality=`, `Errgonomic.strict_equality?` and `Errgonomic.with_strict_equality` are removed, which also removes a process-global flag that leaked across threads and whose block form's `ensure` could turn the mode off for a thread that had set it
|
|
15
|
+
- `Errgonomic.lenient_inner_value_comparison?` and `Errgonomic.give_me_lenient_inner_value_comparison=` are removed; nothing read them
|
|
16
|
+
- [Dev, Test] `rake test:strict` is removed, with its support file and CI step: the Rails integration suite runs under strict equality as `rake test`
|
|
17
|
+
|
|
3
18
|
## [0.10.0] - 2026-09-10
|
|
4
19
|
|
|
5
20
|
This release gives `deconstruct` the Rust shape and names the four variants at top level, so a pattern reads as `in Some(v)`.
|
data/CONTRIBUTING.md
CHANGED
|
@@ -30,7 +30,6 @@ The inner rungs run constantly; the outer rungs are slower and run when preparin
|
|
|
30
30
|
bundle exec rubocop # formatted & lint-clean
|
|
31
31
|
bundle exec yard doctest # doctests pass — most behavior is specified here
|
|
32
32
|
bundle exec rake test # unit tests pass (incl. the Rails integration test)
|
|
33
|
-
bundle exec rake test:strict # the same suite with cross-type equality raising
|
|
34
33
|
```
|
|
35
34
|
|
|
36
35
|
A change that fails any of these is not ready. Keep formatting-only changes in their own commit so they do not obscure a behavioral diff. `bundle exec rake` runs the full suite (test + yard:doctest) in one shot.
|
|
@@ -43,7 +42,7 @@ nix build .#errgonomic # the gem builds as a derivation; rake runs in
|
|
|
43
42
|
nix flake check --all-systems # builds every check on the local system, evaluates all four
|
|
44
43
|
```
|
|
45
44
|
|
|
46
|
-
**After push (CI gate):** a push is done when CI is green, not when `git push` succeeds. Check whatever CI this repo runs (`gh run list`, `gh run view --log-failed`); checks take minutes, so it is fine to schedule the check as a followup and keep working — but the change is not landed until they pass. A CI failure is a regression: diagnose it from the logs, reproduce it locally where you can, and capture it as a test so it cannot recur silently. CI earns you the coverage you cannot run locally — a target your machine isn't, a matrix leg, a slower suite — for free. Here, CI (`.github/workflows/main.yml`) runs `yard doctest
|
|
45
|
+
**After push (CI gate):** a push is done when CI is green, not when `git push` succeeds. Check whatever CI this repo runs (`gh run list`, `gh run view --log-failed`); checks take minutes, so it is fine to schedule the check as a followup and keep working — but the change is not landed until they pass. A CI failure is a regression: diagnose it from the logs, reproduce it locally where you can, and capture it as a test so it cannot recur silently. CI earns you the coverage you cannot run locally — a target your machine isn't, a matrix leg, a slower suite — for free. Here, CI (`.github/workflows/main.yml`) runs `yard doctest` and `rake test` on the latest Ruby 3.4.x on ubuntu-latest, matching the Ruby pinned by the flake.
|
|
47
46
|
|
|
48
47
|
Tests *are* the requirements: a behavior is defined by the test that asserts it. Prefer doctests where an example clarifies a function's contract — they document and test at once and cannot drift out of date without failing the build. In this repo, the YARD `@example` blocks under `lib/**/*.rb` are the primary suite.
|
|
49
48
|
|
data/README.md
CHANGED
|
@@ -150,7 +150,7 @@ The remaining present-side helpers are soft-deprecated on Options in favor of th
|
|
|
150
150
|
|
|
151
151
|
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.
|
|
152
152
|
|
|
153
|
-
Equality is between Options only: `Some(5) == Some(5)
|
|
153
|
+
Equality is between Options only: `Some(5) == Some(5)` is `true` and `Some(5) == Some(6)` is `false`, and comparing an Option with anything that is not one raises `Errgonomic::TypeMismatchError`. Rust rejects `Some(5) == 5` at compile time; Ruby cannot, and a quiet `false` there is a silent wrong branch, the same failure as a wrapper written into a string. Compare against a wrapped value (`opt == Some(5)`), test the inner value (`opt.some_and? { |v| v == 5 }`), or `unwrap_or` a fallback first. `None() == nil` raises too, pointing at `none?`. See [Strict equality](#strict-equality) for where the raise reaches and where it cannot.
|
|
154
154
|
|
|
155
155
|
Ordering is between Options too, and unlike equality it says so out loud. `None()` sorts before any `Some` and two `Some`s order by their inner values, so a collection of Options sorts. Ordering one against a bare value raises `Errgonomic::TypeMismatchError` naming both operands and the spellings that work: `Some(read_at) <= Time.current` used to answer `nil` from `<=>`, which `Comparable` turned into an `ArgumentError` naming the Option as the operand at fault. Test the inner value (`read_at.some_and? { |t| t <= Time.current }`) or reach for it with `map` or `unwrap_or`. Two Options whose inner values do not compare still answer `nil`, as Ruby expects. That message is the gem's only where the Option is the receiver: with it on the right (`2 < Some(1)`, `[Some(1), 2].max`) `Integer` answers the comparison itself and Ruby raises its own `ArgumentError: comparison of Integer with Errgonomic::Option::Some failed`. Results order the same way, with `ok_and?` in place of `some_and?`.
|
|
156
156
|
|
|
@@ -257,35 +257,37 @@ Errgonomic.with_ambiguous_downstream_errors do
|
|
|
257
257
|
end
|
|
258
258
|
```
|
|
259
259
|
|
|
260
|
-
|
|
260
|
+
### Strict equality
|
|
261
261
|
|
|
262
|
-
|
|
263
|
-
Errgonomic.strict_equality = true
|
|
262
|
+
Cross-type equality is the other pedantic check, and there is no switch for it. A comparison between a wrapper and a value that is not one raises `Errgonomic::TypeMismatchError`, naming both classes and the spelling to reach for:
|
|
264
263
|
|
|
264
|
+
```ruby
|
|
265
265
|
Some(5) == 5 # => raises Errgonomic::TypeMismatchError
|
|
266
266
|
Some(5) != 5 # => raises
|
|
267
267
|
Some(5).eql?(5) # => raises
|
|
268
|
+
Some(5) === 5 # => raises, so `case 5 when Some(5)` raises too
|
|
268
269
|
None() == nil # => raises, pointing at none?
|
|
269
270
|
Ok(1) == 1 # => raises
|
|
270
271
|
Some(1) == Ok(1) # => raises: an Option and a Result are different containers
|
|
271
|
-
Some(5) == Some(5) # => true
|
|
272
|
+
Some(5) == Some(5) # => true
|
|
273
|
+
Some(5) == None() # => false
|
|
272
274
|
|
|
273
275
|
1 == Some(1) # => raises, through Integer's coercion fallback
|
|
274
276
|
nil == None() # => false, quietly
|
|
275
277
|
"a" == Some("a") # => false, quietly
|
|
276
278
|
```
|
|
277
279
|
|
|
278
|
-
A Result is cross-type for an Option and an Option is cross-type for a Result: they are different containers, neither is the other, and the message says to unwrap whichever one you meant. Two Options, or two Results, compare
|
|
280
|
+
A Result is cross-type for an Option and an Option is cross-type for a Result: they are different containers, neither is the other, and the message says to unwrap whichever one you meant. Two Options, or two Results, compare by variant and inner value, and `hash` is untouched, so an Option is a Hash key like any other value.
|
|
279
281
|
|
|
280
|
-
|
|
282
|
+
The raise surface is uneven, because Ruby's collections reach equality three ways, and which side holds the wrapper decides what happens. The paragraph after this one gives the rule for `==` itself.
|
|
281
283
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
284
|
+
- Pairwise `==`: `Array#include?`, `Array#index`, `Array#delete`, `Array#count`, `Array#==`, `Hash#==`, `case/when` and Minitest's `assert_equal` raise when the wrapper is the one asked: a member of the collection searched (`[Some(1)].include?(1)`), a `when` clause, or `assert_equal`'s expected value, which comes back as an error rather than a failure. With the wrapper on the other side the bare value answers. An Integer or another Numeric hands the comparison back, so `[1].include?(Some(1))` raises too; a String, a Symbol or `nil` answers `false` itself, so `["a"].include?(Some("a"))` and `case Some("a") when "a"` stay quiet, and `assert_equal "a", Some("a")` is an ordinary failure.
|
|
285
|
+
- Pairwise `eql?`: `Array#-`, `Array#&` and `Array#|` compare member by member with `eql?` while the arrays are short, and a bare value's `eql?` never hands the comparison back. `[Some(1)] - [1]`, `[Some(1)] & [1]` and `[1] | [Some(1)]` raise; `[1] - [Some(1)]`, `[1] & [Some(1)]` and `[Some(1)] | [1]` stay quiet. Past Ruby's cutoff of 16 members they hash instead and stay quiet: `-` once both arrays are past it, `&` and `|` once either is.
|
|
286
|
+
- Hashing: `Hash#[]`, `Set#include?`, `uniq` and `group_by` compare hash values first and ask `eql?` only of a candidate whose hash matches, so they stay quiet whichever side holds the wrapper.
|
|
287
|
+
|
|
288
|
+
Strict equality never answers wrong; it only sometimes fails to catch.
|
|
287
289
|
|
|
288
|
-
|
|
290
|
+
Strictness fires when the wrapper is the receiver, and also when the left operand hands the comparison over: `1 == Some(1)` raises because `Integer#==` falls back to asking the right-hand side. `nil == Some(1)`, `nil == None()` and `"a" == Some("a")` stay quietly false, because `NilClass` and `String` answer for themselves and never consult the operand, and nothing in the gem can intercept them. Put the wrapper on the left in a test if you want the check to reach every comparison.
|
|
289
291
|
|
|
290
292
|
### Rails integration
|
|
291
293
|
|
|
@@ -413,7 +415,7 @@ It is available on every model, converted or not, because it lifts both ends one
|
|
|
413
415
|
|
|
414
416
|
This is the register of where the gem leaves the Rust idiom, and why. ActiveRecord assumes things about accessors that a strict Rust `Option` cannot satisfy, so the integration carries five deliberate compromises, each one forced by a specific piece of ActiveRecord machinery rather than chosen. Everywhere else, treat a departure from Rust's `Option` semantics as a bug; these five are intended:
|
|
415
417
|
|
|
416
|
-
1. `None#nil?` answers `true`, so ActiveRecord internals and ordinary `.nil?` checks treat an absent value as absent. Equality does not follow suit: `None() == nil` is
|
|
418
|
+
1. `None#nil?` answers `true`, so ActiveRecord internals and ordinary `.nil?` checks treat an absent value as absent. Equality does not follow suit: `None() == nil` raises `Errgonomic::TypeMismatchError`, pointing at `none?`, and `nil == None()` is `false`, answered by `NilClass`. Nor does `Array#compact`, the common collection idiom for dropping absent members: it tests for the `nil` object, so it keeps a `None` where `reject(&:none?)` drops it.
|
|
417
419
|
2. `Some` delegates `persisted?` and `touch_later` to its record, so a `Some` can stand in for it where ActiveRecord reads an association back through its public reader, as a `belongs_to ..., touch: true` does after a save.
|
|
418
420
|
3. An `Option` is unwrapped where a value enters ActiveRecord, above the column type in every case. Quoting and the predicate builder are patched so an `Option` passed into `where`/`quote` is unwrapped at the SQL boundary: `Some(v)` binds exactly as `v`, and `None()` as `nil`, so a hash condition asks for `IS NULL`. An array of Options unwraps too. An Option interpolated into raw SQL (`where("id = ?", opt)`) still raises, as it should. Assignment unwraps on the same principle. A singular association writer takes an Option of a record: `book.author = Some(author)` assigns it and `book.author = None()` clears the association, while a `Some` of the wrong class still raises `AssociationTypeMismatch`. An attribute writer takes an Option of a value, for every column type, and unwraps before the attribute is built, so `book.isbn = other.isbn` round-trips and nothing behind the reader ever holds a wrapper. A value that reaches the database without passing a writer unwraps where it enters ActiveRecord, above the column type in every case: in the ids and conditions `find` and `find_by` are given, on a class, a relation and an association alike; in the rows `update_all`, `insert_all` and `upsert` take; and in a default declared with `attribute :isbn, :string, default: Some('unassigned')`, unwrapped where it is written.
|
|
419
421
|
4. `SomeValidator` asks whether a value is there at all, where `presence` asks whether it amounts to anything: `Some('')` passes `validates :x, some: true` and fails `presence: true`. It lifts what it is handed, so it asks the same question of any model, converted or not.
|
data/Rakefile
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'bundler/gem_tasks'
|
|
4
|
-
require 'shellwords'
|
|
5
4
|
|
|
6
5
|
require 'rake/testtask'
|
|
7
6
|
Rake::TestTask.new(:test) do |t|
|
|
@@ -17,16 +16,9 @@ YARD::Doctest::RakeTask.new do |task|
|
|
|
17
16
|
task.pattern = FileList['lib/**/*.rb'].join(' ')
|
|
18
17
|
end
|
|
19
18
|
|
|
20
|
-
namespace :test do
|
|
21
|
-
desc 'Run the Rails integration suite with strict equality on'
|
|
22
|
-
task :strict do
|
|
23
|
-
ruby '-Ilib', '-Itest', 'test/support/strict_equality.rb', *Shellwords.split(ENV.fetch('TESTOPTS', ''))
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
19
|
# yard:doctest ends the process when it finishes, so anything after it in
|
|
28
20
|
# the default list would never run.
|
|
29
|
-
task default: %i[test
|
|
21
|
+
task default: %i[test yard:doctest]
|
|
30
22
|
|
|
31
23
|
namespace :gems4nix do
|
|
32
24
|
desc 'Regenerate gem-groups.json after Gemfile/Gemfile.lock changes'
|
data/lib/errgonomic/option.rb
CHANGED
|
@@ -66,61 +66,84 @@ module Errgonomic
|
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
# An Option equals another Option of the same class with an equal inner
|
|
69
|
-
# value.
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
#
|
|
69
|
+
# value. Comparing it with anything that is not an Option raises
|
|
70
|
+
# Errgonomic::TypeMismatchError, naming both sides and the spelling to
|
|
71
|
+
# reach for. Some(5) == 5 is the comparison Rust rejects at compile
|
|
72
|
+
# time, and a quiet false there is a silent wrong branch, the same
|
|
73
|
+
# failure as a wrapper written into a string. The raise reaches ==, !=,
|
|
74
|
+
# eql? and ===, and through them every collection operation that
|
|
75
|
+
# compares pairwise. Ruby's hashing compares hash values first and asks
|
|
76
|
+
# eql? only of a candidate whose hash matches, so a Hash lookup, a Set
|
|
77
|
+
# and uniq stay quiet with a wrong-typed key: strict equality never
|
|
78
|
+
# answers wrong, it only sometimes fails to catch. nil == Some(1) is
|
|
79
|
+
# answered by NilClass and cannot be intercepted.
|
|
75
80
|
#
|
|
76
|
-
# None() == nil
|
|
77
|
-
#
|
|
78
|
-
# separately makes None#nil? answer true, as an
|
|
79
|
-
# compromise; equality does not follow it.)
|
|
81
|
+
# None() == nil raises too: None is a value that represents absence,
|
|
82
|
+
# not an absence Ruby can see, and the message points at none?. (The
|
|
83
|
+
# Rails integration separately makes None#nil? answer true, as an
|
|
84
|
+
# ActiveRecord compromise; equality does not follow it.)
|
|
80
85
|
#
|
|
81
86
|
# @example
|
|
82
87
|
# Some(1) == Some(1) # => true
|
|
83
88
|
# Some(1) == Some(2) # => false
|
|
84
89
|
# Some(1) == None() # => false
|
|
85
90
|
# None() == None() # => true
|
|
86
|
-
#
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
# Errgonomic.
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
# rescue Errgonomic::TypeMismatchError => e
|
|
94
|
-
# e.class
|
|
95
|
-
# end
|
|
96
|
-
# end # => Errgonomic::TypeMismatchError
|
|
97
|
-
# Errgonomic.with_strict_equality do
|
|
98
|
-
# begin
|
|
99
|
-
# Some(5) != 5
|
|
100
|
-
# rescue Errgonomic::TypeMismatchError => e
|
|
101
|
-
# e.message.include?("!=")
|
|
102
|
-
# end
|
|
103
|
-
# end # => true
|
|
104
|
-
# Errgonomic.with_strict_equality { Some(5) == Some(5) } # => true
|
|
105
|
-
# Errgonomic.with_strict_equality { Some(5) == None() } # => false
|
|
91
|
+
#
|
|
92
|
+
# @example a cross-type comparison is an error, never a quiet false
|
|
93
|
+
# Some(5) == 5 # => raise Errgonomic::TypeMismatchError, "Errgonomic::Option::Some == Integer, which strict equality refuses.\nCompare Options (opt == Some(5)), test the inner value (opt.some_and? { |v| v == 5 }), or unwrap_or a fallback first."
|
|
94
|
+
# Some(5) != 5 # => raise Errgonomic::TypeMismatchError, "Errgonomic::Option::Some != Integer, which strict equality refuses.\nCompare Options (opt == Some(5)), test the inner value (opt.some_and? { |v| v == 5 }), or unwrap_or a fallback first."
|
|
95
|
+
# Some(5) === 5 # => raise Errgonomic::TypeMismatchError, "Errgonomic::Option::Some === Integer, which strict equality refuses.\nCompare Options (opt == Some(5)), test the inner value (opt.some_and? { |v| v == 5 }), or unwrap_or a fallback first."
|
|
96
|
+
# Some(5) === Some(5) # => true
|
|
97
|
+
# 1 == Some(1) # => raise Errgonomic::TypeMismatchError, "Errgonomic::Option::Some == Integer, which strict equality refuses.\nCompare Options (opt == Some(1)), test the inner value (opt.some_and? { |v| v == 1 }), or unwrap_or a fallback first."
|
|
106
98
|
#
|
|
107
99
|
# @example a Result is another container, not another Option
|
|
108
|
-
# Errgonomic.
|
|
109
|
-
# begin
|
|
110
|
-
# Some(1) == Ok(1)
|
|
111
|
-
# rescue Errgonomic::TypeMismatchError => e
|
|
112
|
-
# e.message.include?("different containers")
|
|
113
|
-
# end
|
|
114
|
-
# end # => true
|
|
100
|
+
# Some(1) == Ok(1) # => raise Errgonomic::TypeMismatchError, "Errgonomic::Option::Some == Errgonomic::Result::Ok, which strict equality refuses.\nAn Option and a Result are different containers, and neither is the other. Unwrap the one you meant (opt.unwrap_or(nil) == res.unwrap_or(nil))."
|
|
115
101
|
#
|
|
116
102
|
# @example nil is another type, and absence here is the discriminant
|
|
117
|
-
# Errgonomic.
|
|
118
|
-
#
|
|
119
|
-
#
|
|
120
|
-
#
|
|
121
|
-
#
|
|
103
|
+
# None() == nil # => raise Errgonomic::TypeMismatchError, "Errgonomic::Option::None == NilClass, which strict equality refuses.\nAbsence here is the discriminant: ask none?, or nil? under the Rails integration."
|
|
104
|
+
#
|
|
105
|
+
# @example the raise reaches every operation that compares pairwise
|
|
106
|
+
# begin
|
|
107
|
+
# [Some(1)].include?(1)
|
|
108
|
+
# rescue Errgonomic::TypeMismatchError => e
|
|
109
|
+
# e.class
|
|
110
|
+
# end # => Errgonomic::TypeMismatchError
|
|
111
|
+
# begin
|
|
112
|
+
# [Some(1)] == [1]
|
|
113
|
+
# rescue Errgonomic::TypeMismatchError => e
|
|
114
|
+
# e.class
|
|
115
|
+
# end # => Errgonomic::TypeMismatchError
|
|
116
|
+
# begin
|
|
117
|
+
# [Some(1), 1] - [1]
|
|
118
|
+
# rescue Errgonomic::TypeMismatchError => e
|
|
119
|
+
# e.class
|
|
120
|
+
# end # => Errgonomic::TypeMismatchError
|
|
121
|
+
# begin
|
|
122
|
+
# { a: Some(1) } == { a: 1 }
|
|
123
|
+
# rescue Errgonomic::TypeMismatchError => e
|
|
124
|
+
# e.class
|
|
125
|
+
# end # => Errgonomic::TypeMismatchError
|
|
126
|
+
# begin
|
|
127
|
+
# case 5
|
|
128
|
+
# when Some(5) then :hit
|
|
122
129
|
# end
|
|
123
|
-
#
|
|
130
|
+
# rescue Errgonomic::TypeMismatchError => e
|
|
131
|
+
# e.class
|
|
132
|
+
# end # => Errgonomic::TypeMismatchError
|
|
133
|
+
#
|
|
134
|
+
# @example hashing compares hash values first, so these stay quiet
|
|
135
|
+
# { Some(1) => :v }[1] # => nil
|
|
136
|
+
# Set[Some(1)].include?(1) # => false
|
|
137
|
+
# [Some(1), 1].uniq # => [Some(1), 1]
|
|
138
|
+
#
|
|
139
|
+
# @example a short array compares member by member with eql?, so the side the wrapper is on decides
|
|
140
|
+
# [1] - [Some(1)] # => [1]
|
|
141
|
+
# [Some(1)] | [1] # => [Some(1), 1]
|
|
142
|
+
# [1] | [Some(1)] # => raise Errgonomic::TypeMismatchError, "Errgonomic::Option::Some eql? Integer, which strict equality refuses.\nCompare Options (opt == Some(1)), test the inner value (opt.some_and? { |v| v == 1 }), or unwrap_or a fallback first."
|
|
143
|
+
#
|
|
144
|
+
# @example nil and String answer for themselves, and never ask the Option
|
|
145
|
+
# nil == Some(1) # => false
|
|
146
|
+
# "a" == Some("a") # => false
|
|
124
147
|
def ==(other)
|
|
125
148
|
strict_equality!(other, '==')
|
|
126
149
|
return false if self.class != other.class
|
|
@@ -129,6 +152,13 @@ module Errgonomic
|
|
|
129
152
|
value == other.value
|
|
130
153
|
end
|
|
131
154
|
|
|
155
|
+
# Object#=== is ==, so a `case value when Some(5)` and a pinned pattern
|
|
156
|
+
# reach the same check, named for the operator that was written.
|
|
157
|
+
def ===(other)
|
|
158
|
+
strict_equality!(other, '===')
|
|
159
|
+
self == other
|
|
160
|
+
end
|
|
161
|
+
|
|
132
162
|
# Hash-based collections (Hash keys, Set, uniq, group_by) use eql? and
|
|
133
163
|
# hash, not ==. Follow the inner value's own eql? semantics, so Options
|
|
134
164
|
# behave as keys exactly like their inner values: Some(1) and Some(1.0)
|
|
@@ -141,22 +171,9 @@ module Errgonomic
|
|
|
141
171
|
# { Some(5) => 1 }[Some(5)] # => 1
|
|
142
172
|
# [Some(1), Some(1), None(), None()].uniq # => [Some(1), None()]
|
|
143
173
|
#
|
|
144
|
-
# @example
|
|
145
|
-
# Errgonomic.
|
|
146
|
-
#
|
|
147
|
-
# Some(5).eql?(5)
|
|
148
|
-
# rescue Errgonomic::TypeMismatchError => e
|
|
149
|
-
# e.class
|
|
150
|
-
# end
|
|
151
|
-
# end # => Errgonomic::TypeMismatchError
|
|
152
|
-
# Errgonomic.with_strict_equality { Some(5).hash == Some(5).hash } # => true
|
|
153
|
-
# Ruby derives != from ==, so a strict-equality message would name the
|
|
154
|
-
# operator the caller did not write.
|
|
155
|
-
def !=(other)
|
|
156
|
-
strict_equality!(other, '!=')
|
|
157
|
-
super
|
|
158
|
-
end
|
|
159
|
-
|
|
174
|
+
# @example a cross-type eql? raises as == does, and hash is untouched
|
|
175
|
+
# Some(5).eql?(5) # => raise Errgonomic::TypeMismatchError, "Errgonomic::Option::Some eql? Integer, which strict equality refuses.\nCompare Options (opt == Some(5)), test the inner value (opt.some_and? { |v| v == 5 }), or unwrap_or a fallback first."
|
|
176
|
+
# Some(5).hash == Some(5).hash # => true
|
|
160
177
|
def eql?(other)
|
|
161
178
|
strict_equality!(other, 'eql?')
|
|
162
179
|
return false if self.class != other.class
|
|
@@ -165,6 +182,13 @@ module Errgonomic
|
|
|
165
182
|
value.eql?(other.value)
|
|
166
183
|
end
|
|
167
184
|
|
|
185
|
+
# Ruby derives != from ==, so a strict-equality message would name the
|
|
186
|
+
# operator the caller did not write.
|
|
187
|
+
def !=(other)
|
|
188
|
+
strict_equality!(other, '!=')
|
|
189
|
+
super
|
|
190
|
+
end
|
|
191
|
+
|
|
168
192
|
# @example
|
|
169
193
|
# Some(5).hash == Some(5).hash # => true
|
|
170
194
|
# None().hash == None().hash # => true
|
|
@@ -866,7 +890,6 @@ module Errgonomic
|
|
|
866
890
|
end
|
|
867
891
|
|
|
868
892
|
def strict_equality!(other, operator)
|
|
869
|
-
return unless Errgonomic.strict_equality?
|
|
870
893
|
return if other.is_a?(Errgonomic::Option::Any)
|
|
871
894
|
|
|
872
895
|
raise Errgonomic::TypeMismatchError,
|
|
@@ -18,9 +18,10 @@ module Errgonomic
|
|
|
18
18
|
#
|
|
19
19
|
# 1. None#nil? answers true, so AR internals and ordinary nil checks
|
|
20
20
|
# treat an absent value as absent. Equality does not follow suit:
|
|
21
|
-
# None() == nil
|
|
22
|
-
# collection idiom for dropping absent
|
|
23
|
-
# nil object, so it keeps a None where
|
|
21
|
+
# None() == nil raises, as any cross-type comparison does. Nor does
|
|
22
|
+
# Array#compact, the common collection idiom for dropping absent
|
|
23
|
+
# members: it tests for the nil object, so it keeps a None where
|
|
24
|
+
# reject(&:none?) drops it.
|
|
24
25
|
# 2. Some delegates persisted? and touch_later to its record, so a Some
|
|
25
26
|
# can stand in for it where ActiveRecord reads an association back
|
|
26
27
|
# through its public reader.
|
data/lib/errgonomic/result.rb
CHANGED
|
@@ -107,7 +107,11 @@ module Errgonomic
|
|
|
107
107
|
RUST_SPELLINGS.key?(name) || super
|
|
108
108
|
end
|
|
109
109
|
|
|
110
|
-
#
|
|
110
|
+
# A Result equals another Result of the same variant with an equal
|
|
111
|
+
# inner value. Comparing it with anything that is not a Result raises
|
|
112
|
+
# Errgonomic::TypeMismatchError, on the terms Option#== states: the
|
|
113
|
+
# raise reaches ==, !=, eql? and ===, hashing stays quiet, and a bare
|
|
114
|
+
# value on the left answers for itself.
|
|
111
115
|
#
|
|
112
116
|
# @param other [Object]
|
|
113
117
|
#
|
|
@@ -115,34 +119,23 @@ module Errgonomic
|
|
|
115
119
|
# Ok(1) == Ok(1) # => true
|
|
116
120
|
# Ok(1) == Err(1) # => false
|
|
117
121
|
# Ok(1).object_id == Ok(1).object_id # => false
|
|
118
|
-
#
|
|
119
|
-
#
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
# Errgonomic.
|
|
123
|
-
#
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
#
|
|
127
|
-
#
|
|
122
|
+
#
|
|
123
|
+
# @example a cross-type comparison is an error, never a quiet false
|
|
124
|
+
# Ok(1) == 1 # => raise Errgonomic::TypeMismatchError, "Errgonomic::Result::Ok == Integer, which strict equality refuses.\nCompare Results (res == Ok(1)), test the inner value (res.ok_and? { |v| v == 1 }), or unwrap_or a fallback first."
|
|
125
|
+
# Ok(1) != 1 # => raise Errgonomic::TypeMismatchError, "Errgonomic::Result::Ok != Integer, which strict equality refuses.\nCompare Results (res == Ok(1)), test the inner value (res.ok_and? { |v| v == 1 }), or unwrap_or a fallback first."
|
|
126
|
+
# Ok(1) === 1 # => raise Errgonomic::TypeMismatchError, "Errgonomic::Result::Ok === Integer, which strict equality refuses.\nCompare Results (res == Ok(1)), test the inner value (res.ok_and? { |v| v == 1 }), or unwrap_or a fallback first."
|
|
127
|
+
# Err() == nil # => raise Errgonomic::TypeMismatchError, "Errgonomic::Result::Err == NilClass, which strict equality refuses.\nCompare Results (res == Ok(nil)), test the inner value (res.ok_and? { |v| v == nil }), or unwrap_or a fallback first."
|
|
128
|
+
# Ok(1) === Ok(1) # => true
|
|
129
|
+
# begin
|
|
130
|
+
# [Ok(1)].include?(1)
|
|
131
|
+
# rescue Errgonomic::TypeMismatchError => e
|
|
132
|
+
# e.class
|
|
128
133
|
# end # => Errgonomic::TypeMismatchError
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
# begin
|
|
132
|
-
# Ok(1) != 1
|
|
133
|
-
# rescue Errgonomic::TypeMismatchError => e
|
|
134
|
-
# e.message.include?("!=")
|
|
135
|
-
# end
|
|
136
|
-
# end # => true
|
|
134
|
+
# { Ok(1) => :v }[1] # => nil
|
|
135
|
+
# nil == Err() # => false
|
|
137
136
|
#
|
|
138
137
|
# @example an Option is another container, not another Result
|
|
139
|
-
# Errgonomic.
|
|
140
|
-
# begin
|
|
141
|
-
# Ok(1) == Some(1)
|
|
142
|
-
# rescue Errgonomic::TypeMismatchError => e
|
|
143
|
-
# e.message.include?("different containers")
|
|
144
|
-
# end
|
|
145
|
-
# end # => true
|
|
138
|
+
# Ok(1) == Some(1) # => raise Errgonomic::TypeMismatchError, "Errgonomic::Result::Ok == Errgonomic::Option::Some, which strict equality refuses.\nA Result and an Option are different containers, and neither is the other.\nUnwrap the one you meant (res.unwrap_or(nil) == opt.unwrap_or(nil))."
|
|
146
139
|
def ==(other)
|
|
147
140
|
strict_equality!(other, '==')
|
|
148
141
|
return false if self.class != other.class
|
|
@@ -150,6 +143,13 @@ module Errgonomic
|
|
|
150
143
|
value == other.value
|
|
151
144
|
end
|
|
152
145
|
|
|
146
|
+
# Object#=== is ==, so a `case value when Ok(1)` and a pinned pattern
|
|
147
|
+
# reach the same check, named for the operator that was written.
|
|
148
|
+
def ===(other)
|
|
149
|
+
strict_equality!(other, '===')
|
|
150
|
+
self == other
|
|
151
|
+
end
|
|
152
|
+
|
|
153
153
|
# Hash-based collections (Hash keys, Set, uniq, group_by) use eql? and
|
|
154
154
|
# hash, not ==. Follow the inner value's own eql? semantics, so Results
|
|
155
155
|
# behave as keys exactly like their inner values.
|
|
@@ -161,15 +161,14 @@ module Errgonomic
|
|
|
161
161
|
# { Ok(5) => 1 }[Ok(5)] # => 1
|
|
162
162
|
# [Err(:a), Err(:a)].uniq # => [Err(:a)]
|
|
163
163
|
#
|
|
164
|
-
# @example
|
|
165
|
-
# Errgonomic.
|
|
166
|
-
#
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
# Errgonomic.with_strict_equality { Ok(5).hash == Ok(5).hash } # => true
|
|
164
|
+
# @example a cross-type eql? raises as == does, and hash is untouched
|
|
165
|
+
# Ok(5).eql?(5) # => raise Errgonomic::TypeMismatchError, "Errgonomic::Result::Ok eql? Integer, which strict equality refuses.\nCompare Results (res == Ok(5)), test the inner value (res.ok_and? { |v| v == 5 }), or unwrap_or a fallback first."
|
|
166
|
+
# Ok(5).hash == Ok(5).hash # => true
|
|
167
|
+
def eql?(other)
|
|
168
|
+
strict_equality!(other, 'eql?')
|
|
169
|
+
self.class == other.class && value.eql?(other.value)
|
|
170
|
+
end
|
|
171
|
+
|
|
173
172
|
# Ruby derives != from ==, so a strict-equality message would name the
|
|
174
173
|
# operator the caller did not write.
|
|
175
174
|
def !=(other)
|
|
@@ -177,11 +176,6 @@ module Errgonomic
|
|
|
177
176
|
super
|
|
178
177
|
end
|
|
179
178
|
|
|
180
|
-
def eql?(other)
|
|
181
|
-
strict_equality!(other, 'eql?')
|
|
182
|
-
self.class == other.class && value.eql?(other.value)
|
|
183
|
-
end
|
|
184
|
-
|
|
185
179
|
# @example
|
|
186
180
|
# Ok(5).hash == Ok(5).hash # => true
|
|
187
181
|
# Ok(5).hash == Err(5).hash # => false
|
|
@@ -568,7 +562,6 @@ module Errgonomic
|
|
|
568
562
|
end
|
|
569
563
|
|
|
570
564
|
def strict_equality!(other, operator)
|
|
571
|
-
return unless Errgonomic.strict_equality?
|
|
572
565
|
return if other.is_a?(Errgonomic::Result::Any)
|
|
573
566
|
|
|
574
567
|
raise Errgonomic::TypeMismatchError,
|
|
@@ -649,7 +642,7 @@ module Errgonomic
|
|
|
649
642
|
# @example
|
|
650
643
|
# Err(:nope).inspect # => "Err(:nope)"
|
|
651
644
|
# Err().inspect # => "Err()"
|
|
652
|
-
#
|
|
645
|
+
# Err(Some(1)).inspect # => "Err(Some(1))"
|
|
653
646
|
def inspect
|
|
654
647
|
return 'Err()' if value.equal?(Arbitrary)
|
|
655
648
|
|
data/lib/errgonomic/version.rb
CHANGED
data/lib/errgonomic.rb
CHANGED
|
@@ -85,41 +85,4 @@ module Errgonomic
|
|
|
85
85
|
ensure
|
|
86
86
|
@give_me_ambiguous_downstream_errors = original_value
|
|
87
87
|
end
|
|
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
|
-
|
|
116
|
-
# Lenient inner value comparison means the inner value of a Some or Ok can be
|
|
117
|
-
# compared to some other non-Result or non-Option value.
|
|
118
|
-
def self.lenient_inner_value_comparison?
|
|
119
|
-
@lenient_inner_value_comparison ||= true
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def self.give_me_lenient_inner_value_comparison=(value)
|
|
123
|
-
@lenient_inner_value_comparison = value
|
|
124
|
-
end
|
|
125
88
|
end
|