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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d39f466feedb411b53780fa853b509dc6a40db2f12aef63235a6817881ce2d95
4
- data.tar.gz: bc81bba0f57ddaac60178b0319533feeb054be296614332b8a9744d72b0ae9d1
3
+ metadata.gz: 543390fc5f85546ffde4785cfc41951400800094506078271cfe32535e9d784a
4
+ data.tar.gz: 8d5cdf061dbf7026eaab5ba8cab628368c600943699dbad04235c9d1d9bd3f5a
5
5
  SHA512:
6
- metadata.gz: 26b24e1089898ad573927824616829559a6c9fa5c169abf71addcad1284a345597ae12928f4a22153ce1d06f64db3572e7e4d0d6c596df2dffef4a1eb260e72e
7
- data.tar.gz: 5f20b741f3feb1c4f5b84daee2f3934edc09f89f962a459c485dfed8356d63e84297f778b3b74f761ada2c1be2ea5c371c73b318a2ef5aa54c8ba635612ba450
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`, `rake test` and `rake test:strict` on the latest Ruby 3.4.x on ubuntu-latest, matching the Ruby pinned by the flake.
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)`, 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).
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
- Cross-type equality is the other pedantic check, and it is off by default because a quiet `false` is what every Ruby object answers. Turn it on and a comparison between a wrapper and a value that is not one raises `Errgonomic::TypeMismatchError`, naming both classes and the spelling to reach for:
260
+ ### Strict equality
261
261
 
262
- ```ruby
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, as always
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 as they always did, and `hash` is untouched, so an Option stays usable as a Hash key with it on.
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
- 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 == None()` and `"a" == Some("a")` stay quietly false, because `NilClass` and `String` answer for themselves and never consult the operand. Put the wrapper on the left in a test if you want the check to reach every comparison. It is meant for a test suite or CI, not for production, and there is a block form for scoping it the way the ambiguous-error opt-out is scoped:
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
- ```ruby
283
- Errgonomic.with_strict_equality do
284
- assert_equal Some(5), book.pages
285
- end
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
- This gem runs its own Rails integration suite that way, as `rake test:strict`.
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 still `false`. 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.
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 test:strict yard:doctest]
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'
@@ -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. Anything else, including nil and the raw inner value, is not
70
- # equal: quietly false, never an error. Rust rejects Some(5) == 5 at
71
- # compile time; Ruby cannot, and raising here would break the many
72
- # places Ruby compares heterogeneous operands (Array#include?,
73
- # assertion diffs, dirty tracking). Compare Options (opt == Some(5)) or
74
- # test the inner value (opt.some_and? { |v| v == 5 }) instead.
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 is likewise false: None is a value that represents
77
- # absence, not an absence Ruby can see. (The Rails integration
78
- # separately makes None#nil? answer true, as an ActiveRecord
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
- # Some(1) == 1 # => false
87
- # None() == nil # => false
88
- #
89
- # @example strict equality makes a cross-type comparison an error
90
- # Errgonomic.with_strict_equality do
91
- # begin
92
- # Some(5) == 5
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.with_strict_equality do
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.with_strict_equality do
118
- # begin
119
- # None() == nil
120
- # rescue Errgonomic::TypeMismatchError => e
121
- # e.message.include?("none?")
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
- # end # => true
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 strict equality reaches eql?, and leaves hash alone
145
- # Errgonomic.with_strict_equality do
146
- # begin
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 stays false. Nor does Array#compact, the common
22
- # collection idiom for dropping absent members: it tests for the
23
- # nil object, so it keeps a None where reject(&:none?) drops it.
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.
@@ -107,7 +107,11 @@ module Errgonomic
107
107
  RUST_SPELLINGS.key?(name) || super
108
108
  end
109
109
 
110
- # Equality comparison for Result objects is based on value not reference.
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
- # Ok(1) == 1 # => false
119
- # Err() == nil # => false
120
- #
121
- # @example strict equality makes a cross-type comparison an error
122
- # Errgonomic.with_strict_equality do
123
- # begin
124
- # Ok(1) == 1
125
- # rescue Errgonomic::TypeMismatchError => e
126
- # e.class
127
- # end
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
- # Errgonomic.with_strict_equality { Ok(1) == Ok(1) } # => true
130
- # Errgonomic.with_strict_equality do
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.with_strict_equality do
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 strict equality reaches eql?, and leaves hash alone
165
- # Errgonomic.with_strict_equality do
166
- # begin
167
- # Ok(5).eql?(5)
168
- # rescue Errgonomic::TypeMismatchError => e
169
- # e.class
170
- # end
171
- # end # => Errgonomic::TypeMismatchError
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
- # Errgonomic.with_strict_equality { Err(Some(1)).inspect } # => "Err(Some(1))"
645
+ # Err(Some(1)).inspect # => "Err(Some(1))"
653
646
  def inspect
654
647
  return 'Err()' if value.equal?(Arbitrary)
655
648
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Errgonomic
4
- VERSION = '0.10.0'
4
+ VERSION = '0.10.1'
5
5
  end
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
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.10.0
4
+ version: 0.10.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Zadrozny