errgonomic 0.9.0 → 0.9.2

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: a1d2acdb3fb3ef7682b897113199fa17107edd3bb4a00aec5eea57afdcc899f0
4
- data.tar.gz: 9f1e08884a73e3735c612486dc84308fce3ab5a274f85cf1a67191b34da4d30a
3
+ metadata.gz: 2b6ae05f6601a1d5470b1cb9cd9266e3937b91fd2ea2bab8eeac469959b24e90
4
+ data.tar.gz: 5b3095f01409a6275b500df7d08e0c8b23b50062924d61ada8a4a6c98e0b69fe
5
5
  SHA512:
6
- metadata.gz: 676ef637409475913d89cb971c645e38ead97fbcd5e5cd56096b11a07175310c6e1a90b296d49db33b8ef04fcdf3085468e6e52fb4b902f512408f144fc1e1cc
7
- data.tar.gz: 19cde3c8a522a57fdfa5ac367751f48657c9319e74ff5869d3ba4d965abe847ee63c168c1419fbe491bbbb871f05cbc83298ee995198ee486949a43d04c70ac6
6
+ metadata.gz: a74d4cdf58805801e853a59329b076ca43c553da3a670fc56762fea5d755da898c6a6951e2c697fcb91b28930382d2f0ff6e3e2a85038ad33a5a3a0d4567c6c3
7
+ data.tar.gz: d636451a6addbfc3e2af62974e659fe7de490dd7b4176f3cf2f53c6f93dd6d620244f14e99a684817a160a4715c02ed4b5d0b003bded913f5962e64987d674cc
data/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.9.2] - 2026-09-10
4
+
5
+ This release makes `Option#and`, `#xor`, `#zip` and `#zip_with` check their operand the way `or` already did.
6
+
7
+ ### Upgrading from 0.9.1
8
+
9
+ `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.
10
+
11
+ ### Changes
12
+
13
+ - [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?`
14
+
15
+ ## [0.9.1] - 2026-09-10
16
+
17
+ 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.
18
+
19
+ ### Upgrading from 0.9.0
20
+
21
+ `to_s` on an Option or a Result raises `Errgonomic::SerializeError` again, so a string built from a wrapped reader fails where it is built rather than writing `Some("...")` or `None` into it. Code written against 0.9.0's rendering, whether a string interpolation, an `Array#join`, a `format`, a `String()` or a bare ERB `<%= %>`, has to take the value first: `unwrap_or` or `expect!` for the value, or `inspect` for a log line. A `rescue` that interpolates a wrapper into its message writes `inspect` there. A `rescue Errgonomic::SerializeError` written against 0.8.x still matches. A `logger.info(opt)` that rendered through 0.9.0 still renders through a plain `Logger`, but raises under Rails' `TaggedLogging` once a tag such as `request_id` is set, so it can pass in tests and raise in production: write `logger.info(opt.inspect)`. The README's Option section describes this and a `case/in` that matches nothing on a wrapper, whose `NoMatchingPatternError` no longer prints its subject.
22
+
23
+ ### Changes
24
+
25
+ - [Behavior change] `to_s` on an Option or a Result raises `Errgonomic::SerializeError` where 0.9.0 rendered it as `inspect` does. The message names the value with its `inspect`, bounded to 60 characters, says `to_s` is refused, and names `inspect` for a log line and `unwrap_or` / `expect!` for the value: `Some(1) refuses to_s; use inspect for a log line, or unwrap_or / expect! for the value`. 0.9.0's rendering wrote wrapper text into data at every site that built a string from a wrapped reader, with no exception to find the site by: a UNIQUE identity column, a hostname, a hashed auth token and a customer-facing page. A raise that names the remedy serves the log-line case 0.9.0 traded for, and `inspect` is unchanged
26
+ - An Option or a Result in a Hash key raises on its way to JSON again. The json gem and ActiveSupport's `as_json` both stringify a key with `to_s`, so 0.9.0's rendering let `{ Some(1) => 2 }.as_json` write `{"Some(1)" => 2}` where a value position had always raised
27
+ - `Result#unwrap_err!` on an `Ok` raises an `Errgonomic::UnwrapError` whose message is the Ok's value as `inspect` renders it, bounded to 60 characters, and whose `value` is the value itself. The message used to be the value's `to_s`, so once `to_s` refuses, `Ok(Some(1)).unwrap_err!` printed only the class name and its `message` raised. A String value is quoted in the message, as `inspect` quotes it
28
+
3
29
  ## [0.9.0] - 2026-09-08
4
30
 
5
31
  This release turns the ActiveRecord integration from a set of wrapped readers into a full set of boundaries, covering readers, writers, query binds, validation and serialization, with the behavior changes named in the bullets below.
data/README.md CHANGED
@@ -122,9 +122,13 @@ in Errgonomic::Option::None
122
122
  end
123
123
  ```
124
124
 
125
- An unhandled Option refuses to leak into your output: `to_json` and `as_json` raise `Errgonomic::SerializeError`, so you handle the inner value deliberately rather than shipping `#<Errgonomic::Option::Some...>` to a user. The refusal names what it was carrying (`cannot serialize an unwrapped Some("cell-a1b2")`), so a payload built out of many values says which one went unhandled; the value's `inspect` is bounded to 60 characters, with an ellipsis past that. The refusal covers `as_json` because Hash and Array serialization recurses through that method, and an Option nested in a payload would otherwise serialize as `{"value": ...}`. A converted ActiveRecord model is the one exception, at the model boundary: it unwraps each attribute as it serializes, so a record's own `as_json` says what an unconverted record's says. See [Rails integration](#rails-integration).
125
+ An unhandled Option refuses to leak into your output: `to_s`, `to_json` and `as_json` raise `Errgonomic::SerializeError`, so you handle the inner value deliberately rather than shipping `Some("...")` to a user. The refusal names what it was carrying (`cannot serialize an unwrapped Some("cell-a1b2")`), so a payload built out of many values says which one went unhandled; the value's `inspect` is bounded to 60 characters, with an ellipsis past that. The refusal covers `as_json` because Hash and Array serialization recurses through that method, and an Option nested in a payload would otherwise serialize as `{"value": ...}`. A converted ActiveRecord model is the one exception, at the model boundary: it unwraps each attribute as it serializes, so a record's own `as_json` says what an unconverted record's says. See [Rails integration](#rails-integration).
126
126
 
127
- `to_s` renders rather than refusing: `Some(1).to_s` is `"Some(1)"` and `None().to_s` is `"None"`, matching `inspect`, and the same holds for `Ok` and `Err`. Rust gives `Option` a `Debug` and no `Display`, so raising was the faithful reading, but a `to_s` that raises replaces the real exception while a `rescue` builds its log line, which is the worst possible place to be strict. The rendered form is unambiguous: a `Some(1)` in a log says a wrapper arrived where a value was meant.
127
+ The `to_s` refusal is the loudest guard of the three, because a string is where a wrapper turns into data: string interpolation, `Array#join`, `format`, `String()`, a bare ERB `<%= %>` and the key of a Hash on its way to JSON all reach the value through `to_s`, and every one of them raises rather than writing `Some("...")` into a hostname, a column or a page. Rust gives `Option` a `Debug` and no `Display`, and `inspect` is the `Debug` here: it renders `Some(1)`, and it is what a log line or a `rescue` should call (`"got #{opt.inspect}"`). The refusal says so: `Some(1) refuses to_s; use inspect for a log line, or unwrap_or / expect! for the value`.
128
+
129
+ Two places reach `to_s` on your behalf, and read badly when it refuses. The first is a `case/in` with more than one `in` branch where none matches. Ruby raises `NoMatchingPatternError` with the unmatched Option or Result itself as the message, and printing that message calls `to_s`. Uncaught, it prints as a bare `NoMatchingPatternError` with no subject, and inside a Minitest test the reporter crashes on it with `Some(1) refuses to_s; …` before it prints the test's name or the run summary. The subject is a variant the `case` has no branch for, such as a Result handed to Option patterns, so the fix is the missing `in` branch, or an `else`; an `inspect` has nowhere to go. A single-branch `case/in` and a rightward `=>` build their message with `inspect` and print normally.
130
+
131
+ The second is a logger. `logger.info(opt)` renders `Some(1)` through a plain `Logger`, whose formatter calls `inspect` on a message that is not a String, but raises through Rails' `ActiveSupport::TaggedLogging` once a tag is set, because the tagged formatter interpolates the message. Rails' generated `production.rb` tags every request with `log_tags = [:request_id]`, so the same line passes in tests and raises in production. Write `logger.info(opt.inspect)`.
128
132
 
129
133
  `expect!` also takes a block, on an Option and a Result alike, so a message that interpolates is built only on the branch that raises: `tier.expect! { "no tier for #{account.id}" }`. `present_or_raise!` takes one on the same terms. The positional form is unchanged.
130
134
 
@@ -177,7 +181,7 @@ in Errgonomic::Result::Err, Exception => e
177
181
  end
178
182
  ```
179
183
 
180
- Like Options, unwrapped Results refuse `to_json` and `as_json`, and render `to_s` as `inspect` does. And `Object#result?` / `Object#assert_result!` help enforce at runtime that a value is a Result.
184
+ Like Options, unwrapped Results refuse `to_s`, `to_json` and `as_json`, and render through `inspect`. And `Object#result?` / `Object#assert_result!` help enforce at runtime that a value is a Result.
181
185
 
182
186
  ### Optional collections
183
187
 
@@ -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.new, "other must be an Option, was Integer"
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
- raise ArgumentError, "other must be an Option, was #{other.class.name}" unless other.is_a?(Any)
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,25 +679,32 @@ 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)
678
689
  Some(other)
679
690
  end
680
691
 
681
- # Render as inspect does. Rust gives Option a Debug and no Display, so
682
- # refusing was faithful, but a to_s that raises replaces the real
683
- # exception while a rescue builds its log line, and the rendered form
684
- # says plainly that a wrapper arrived where a value was meant.
692
+ # Refuse to render as a String. Rust gives Option a Debug and no
693
+ # Display: a wrapper that reaches a string went unhandled, and a string
694
+ # is where it turns into data, a hostname, a hash key or a page. The
695
+ # refusal names the value and says how to log it or take it.
685
696
  #
686
697
  # @example
687
- # Some(1).to_s # => "Some(1)"
688
- # Some("x").to_s # => "Some(\"x\")"
689
- # None().to_s # => "None"
690
- # "value: #{Some(1)}" # => "value: Some(1)"
698
+ # Some(1).to_s # => raise Errgonomic::SerializeError, "Some(1) refuses to_s; use inspect for a log line, or unwrap_or / expect! for the value"
699
+ # None().to_s # => raise Errgonomic::SerializeError, "None refuses to_s; use inspect for a log line, or unwrap_or / expect! for the value"
700
+ # "value: #{Some(1)}" # => raise Errgonomic::SerializeError, "Some(1) refuses to_s; use inspect for a log line, or unwrap_or / expect! for the value"
701
+ # [Some("org"), Some("metrics")].join("/") # => raise Errgonomic::SerializeError, "Some(\"org\") refuses to_s; use inspect for a log line, or unwrap_or / expect! for the value"
702
+ # format("%s", None()) # => raise Errgonomic::SerializeError, "None refuses to_s; use inspect for a log line, or unwrap_or / expect! for the value"
703
+ # String(Some(1)) # => raise Errgonomic::SerializeError, "Some(1) refuses to_s; use inspect for a log line, or unwrap_or / expect! for the value"
704
+ # Some("a" * 100).to_s # => raise Errgonomic::SerializeError, "Some(\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa... refuses to_s; use inspect for a log line, or unwrap_or / expect! for the value"
705
+ # Some(1).inspect # => "Some(1)"
691
706
  def to_s
692
- inspect
707
+ raise Errgonomic::SerializeError, to_s_refusal
693
708
  end
694
709
 
695
710
  # Refuse to serialize an unwrapped Option as JSON. Not only should we
@@ -763,8 +778,10 @@ module Errgonomic
763
778
  # Some(:left).xor(Some(:right)) # => None()
764
779
  # Some(:left).xor(None()) #=> Some(:left)
765
780
  # None().xor(Some(:right)) #=> Some(:right)
766
- #
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"
767
783
  def xor(other)
784
+ option_operand!(other)
768
785
  return self if some? && other.none?
769
786
  return other if other.some? && none?
770
787
 
@@ -773,12 +790,27 @@ module Errgonomic
773
790
 
774
791
  private
775
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
+
801
+ def to_s_refusal
802
+ "#{bounded_inspect} refuses to_s; use inspect for a log line, or unwrap_or / expect! for the value"
803
+ end
804
+
805
+ def serialize_refusal
806
+ "cannot serialize an unwrapped #{bounded_inspect}"
807
+ end
808
+
776
809
  # Name the value the caller failed to handle, bounded: an inspect of a
777
810
  # record or a long payload would bury the message carrying it.
778
- def serialize_refusal
811
+ def bounded_inspect
779
812
  rendered = inspect
780
- rendered = "#{rendered[0, 57]}..." if rendered.length > 60
781
- "cannot serialize an unwrapped #{rendered}"
813
+ rendered.length > 60 ? "#{rendered[0, 57]}..." : rendered
782
814
  end
783
815
 
784
816
  def presence_nudge(from, to)
@@ -241,12 +241,16 @@ module Errgonomic
241
241
  end
242
242
 
243
243
  # Return the inner value of an Err, else raise an exception when Ok.
244
+ # The message is the Ok's value as inspect renders it, bounded, so an
245
+ # Ok holding an Option or a Result still has a message to print.
244
246
  #
245
247
  # @example
246
- # Ok(1).unwrap_err! # => raise Errgonomic::UnwrapError, 1
248
+ # Ok(1).unwrap_err! # => raise Errgonomic::UnwrapError, "1"
249
+ # Ok(Some(1)).unwrap_err! # => raise Errgonomic::UnwrapError, "Some(1)"
250
+ # Ok("a" * 100).unwrap_err! # => raise Errgonomic::UnwrapError, "\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa..."
247
251
  # Err(:e).unwrap_err! # => :e
248
252
  def unwrap_err!
249
- raise Errgonomic::UnwrapError, value unless err?
253
+ raise Errgonomic::UnwrapError.new(bounded_inspect(value), value) unless err?
250
254
 
251
255
  @value
252
256
  end
@@ -405,18 +409,22 @@ module Errgonomic
405
409
  Err(block.call(value))
406
410
  end
407
411
 
408
- # Render as inspect does. Rust gives Result a Debug and no Display, so
409
- # refusing was faithful, but a to_s that raises replaces the real
410
- # exception while a rescue builds its log line, and the rendered form
411
- # says plainly that a wrapper arrived where a value was meant.
412
+ # Refuse to render as a String. Rust gives Result a Debug and no
413
+ # Display: a wrapper that reaches a string went unhandled, and a string
414
+ # is where it turns into data, a hostname, a hash key or a page. The
415
+ # refusal names the value and says how to log it or take it.
412
416
  #
413
417
  # @example
414
- # Ok(1).to_s # => "Ok(1)"
415
- # Err(:nope).to_s # => "Err(:nope)"
416
- # Err().to_s # => "Err()"
417
- # "outcome: #{Ok(1)}" # => "outcome: Ok(1)"
418
+ # Ok(1).to_s # => raise Errgonomic::SerializeError, "Ok(1) refuses to_s; use inspect for a log line, or unwrap_or / expect! for the value"
419
+ # Err(:nope).to_s # => raise Errgonomic::SerializeError, "Err(:nope) refuses to_s; use inspect for a log line, or unwrap_or / expect! for the value"
420
+ # "outcome: #{Ok(1)}" # => raise Errgonomic::SerializeError, "Ok(1) refuses to_s; use inspect for a log line, or unwrap_or / expect! for the value"
421
+ # [Ok(1), Err(:x)].join(",") # => raise Errgonomic::SerializeError, "Ok(1) refuses to_s; use inspect for a log line, or unwrap_or / expect! for the value"
422
+ # format("%s", Err(:x)) # => raise Errgonomic::SerializeError, "Err(:x) refuses to_s; use inspect for a log line, or unwrap_or / expect! for the value"
423
+ # String(Ok(1)) # => raise Errgonomic::SerializeError, "Ok(1) refuses to_s; use inspect for a log line, or unwrap_or / expect! for the value"
424
+ # Err("a" * 100).to_s # => raise Errgonomic::SerializeError, "Err(\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa... refuses to_s; use inspect for a log line, or unwrap_or / expect! for the value"
425
+ # Err(:nope).inspect # => "Err(:nope)"
418
426
  def to_s
419
- inspect
427
+ raise Errgonomic::SerializeError, to_s_refusal
420
428
  end
421
429
 
422
430
  # Refuse to serialize an unwrapped Result as JSON. Not only should we
@@ -471,6 +479,17 @@ module Errgonomic
471
479
 
472
480
  private
473
481
 
482
+ def to_s_refusal
483
+ "#{bounded_inspect} refuses to_s; use inspect for a log line, or unwrap_or / expect! for the value"
484
+ end
485
+
486
+ # Name the value the caller failed to handle, bounded: an inspect of a
487
+ # record or a long payload would bury the message carrying it.
488
+ def bounded_inspect(object = self)
489
+ rendered = object.inspect
490
+ rendered.length > 60 ? "#{rendered[0, 57]}..." : rendered
491
+ end
492
+
474
493
  def strict_equality!(other, operator)
475
494
  return unless Errgonomic.strict_equality?
476
495
  return if other.is_a?(Errgonomic::Result::Any)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Errgonomic
4
- VERSION = '0.9.0'
4
+ VERSION = '0.9.2'
5
5
  end
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.9.0
4
+ version: 0.9.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Zadrozny
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
10
+ date: 1980-01-01 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: concurrent-ruby