Repository navigation
Conversation
0.9.0 made Option#to_s render as inspect does, to keep a raise out of the rescue block that interpolates a wrapper into its log line. In a mass conversion of every model in an application, every site that built a string from a reader then wrote wrapper text into data with no exception: a UNIQUE identity column, a hostname, a hashed auth token and a customer-facing page. Each of those raised under 0.8.3. The raise comes back as the same Errgonomic::SerializeError, so an existing rescue still matches, and its message now carries the remedy the log-line case needs: the value via the bounded inspect, that to_s is refused, and inspect for a log line or unwrap_or / expect! for the value. The bounded inspect moves into its own helper so the to_json refusal and this one name the value the same way.
The same reversal as for Option, with the same message shape: the value via a bounded inspect, that to_s is refused, and the spellings to reach for. Result had no bounded inspect helper, so one is added privately for the to_s refusal; the to_json and as_json refusals keep their fixed message.
Each reproduction from the 0.9.0 field reports is a test: join, interpolation, format and String() for both containers; a bare ERB <%= %> through ActionView, whose template error carries the refusal as its cause; and an Option or Result in a Hash key, which both the json gem and ActiveSupport's as_json stringify through to_s, so the raising to_s closes the key position that 0.9.0 opened. A None in a bare ERB tag renders as nothing rather than raising: the Rails integration's None answers nil?, and ActionView's output buffer skips a nil before it asks for to_s. The test records that as it is.
The changelog names the to_s reversal as a behavior change with an upgrading note for anyone who wrote against 0.9.0's rendering. The 0.9.0 section stays as history.
or already refused a bare operand with Errgonomic::ArgumentError. and, xor, zip and zip_with did not: and handed the bare operand back out of the Option chain, xor and zip fell into a NoMethodError on some? or none?, and whether anything fired at all depended on the receiver's variant, so a None-heavy path never learned its operand was wrong. The check now runs before the discriminant is consulted, on all five combinators, with or's message.
Some, Ok and Err exposed value as a public accessor, so a wrapper could be mutated through an alias, a Hash key could move buckets under its own hash, and the reader reached the inner value with no None branch, the ambiguous failure the type exists to prevent. The reader is now protected, for the sibling reads equality, ordering and zip need, and every instance freezes on construction. A call to value or value= from outside reaches method_missing and gets the combinator teaching. sequence_options and sequence_results were the only readers outside the classes; they collect through tap_some and tap_ok instead.
nz
changed the base branch from
2026-09-10-errgonomic-0.9.x-triage/and-xor-zip
to
main
September 10, 2026 16:06
This was referenced Sep 10, 2026
unwrap_err! passed the Ok's value as the exception message, so the message was the value's to_s. With to_s refused on an Option or a Result, Ok(Some(1)).unwrap_err! printed only the class name and its message raised. The message is now the value's inspect, bounded as the to_s refusal bounds it, and the error carries the value as unwrap!'s does.
The join test covered the same shape with different values, and the group_by test grouped bare Options rather than a converted model's reader. Both lines now appear as #75 writes them: a join of a Some and a None, and a group_by over a converted model's optional association on its way to JSON.
With ActiveSupport loaded, Hash#to_json reaches a key through as_json,
which refused an Option under 0.9.0 as well, so the two to_json
assertions in the hash-key test passed before the to_s change and did
not test it. They move to a child process that loads the json gem and
errgonomic alone, where 0.9.0 rendered {"Some(1)":2} and a raising to_s
refuses.
A case/in with several branches and none matching raises NoMatchingPatternError with the subject itself as its message, so on an Option or a Result the message refuses. The top-level printer then shows only the class name, and Minitest's reporter crashes before it names the test. The README says so and that the fix is the missing branch, and a test pins each of the three behaviors so the text cannot drift from them. The json-gem child test shares the new run_ruby helper.
Logger's own formatter inspects a message that is not a String, so logger.info(opt) renders. ActiveSupport::TaggedLogging interpolates the message once a tag is set, and Rails tags every production request with its request_id, so the same call passes in tests and raises in production. The README and the 0.9.1 upgrading note say to write logger.info(opt.inspect), and a test pins both loggers' behavior.
…6-09-10-errgonomic-0.9.x-triage/and-xor-zip
…26-09-10-errgonomic-0.9.x-triage/value-is-not-a-slot
The changelog and README said every instance is frozen, but a copy that skips construction is not: dup, Marshal.load, a YAML load and ActiveSupport's deep_dup all return an unfrozen instance, while clone keeps it frozen. With no writer, such a copy changes only through instance_variable_set. The wording now states that guarantee.
nz
force-pushed
the
2026-09-10-errgonomic-0.9.x-triage/value-is-not-a-slot
branch
from
September 10, 2026 18:31
0021ce4 to
8ce681e
Compare
nz
marked this pull request as ready for review
September 10, 2026 18:32
nz
enabled auto-merge
September 10, 2026 18:32
lutzcc1
previously approved these changes
Sep 10, 2026
Member
Author
|
Superseded by #87, which landed this release's commits on main as a linear history (merged 2026-09-10). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Errgonomic 0.9.3 removes the public
valuereader and writer fromSome,OkandErr, and freezes every Option and Result on construction. The reader reached the inner value with noNonebranch. The writer mutated a wrapper through an alias and moved a Hash key out from under its own bucket, where the README already said an Option is a value rather than a slot. This pull request stacks on the 0.9.2 branch (#83) and targetsmain, so until #83 merges its diff also shows the earlier releases' commits; it merges after that one.Closes #76.
What changes
valueandvalue=are gone fromSome,OkandErr. A call to either from outside reachesmethod_missingand raisesErrgonomic::UnwrappedAccessError, aNoMethodError, naming the combinators to reach for.unwrap_or,expect!,map,and_thenor a pattern for a read, a newSome(v)assigned where the old one lived for a write.How it works
lib/errgonomic/option.rbandlib/errgonomic/result.rb:valueis a protected reader onSomeand onResult::Any, for the sibling reads==,eql?,<=>andzipneed; the constructors freeze. Every reproduction line in Some#value= is a public mutable slot and #value an unguarded unwrap — contradicts the value-not-slot design #76 is a doctest on the constructor.lib/errgonomic/core_ext/enumerable.rb:sequence_optionsandsequence_resultswere the only readers ofvalueoutside the classes, and collect throughtap_someandtap_okinstead.README.md: the value-not-a-slot paragraph says whyvalueis absent and what to reach for.The Rails integration reads
valueonly from inside the classes (delegate :persisted?, to: :valueandtry), which the protected reader still serves, so no seam changed there.Evidence
Run locally on 2026-09-10, aarch64-darwin, inside the flake devshell:
bundle exec rubocop: 24 files inspected, 6 offenses, all six pre-existing onmain(Metrics onmethod_missinginoption.rbandresult.rb, Metrics onoptional_dig, Style/Documentation onErrgonomic::Rails).bundle exec yard doctest: 179 runs, 459 assertions, 0 failures, 0 errors.bundle exec rake test: 132 runs, 464 assertions, 0 failures, 0 errors. The Rails integration suite passed unchanged with frozen wrappers.bundle exec rake test:strict: 132 runs, 464 assertions, 0 failures, 0 errors.bundle exec rake: the same three counts.nix build .#errgonomic: builterrgonomic-0.9.3.nix flake check --all-systems: all checks passed.CI runs
yard doctest,rake testandrake test:stricton Ruby 3.4 on ubuntu-latest.What is left open
The evidence that freezing breaks nothing ActiveRecord does to a wrapper comes from this gem's own suite. The Sprout validation run measures it against an application. Recommendation: accept, and read that count before the release is cut.
Background
Project charter:
cowork/projects/2026-09-10-errgonomic-0.9.x-triage/README.md, decisiondecisions/2026-09-10-value-is-not-a-slot.md.After review
Review found that "every Option and Result is frozen" overstated the guarantee: a copy that skips construction, from
dup,Marshal.load, a YAML load or ActiveSupport'sdeep_dup, comes back unfrozen, and with no writer it changes only throughinstance_variable_set.clonekeeps it frozen. The changelog and README now state that guarantee; the code is unchanged. The #81 and #83 review fixes are merged forward. Gates, 2026-09-10: rubocop 6 offenses, the same six;yard doctest179 runs, 461 assertions;rake testandrake test:strict136 runs, 480 assertions each;nix buildandnix flake check --all-systemspass. CI passed on0021ce4.