add SubjectAs/IssSub accessors for the sub_id pointer/value asymmetry#15
Merged
Conversation
SET.Subject is typed subjectid.SubjectIdentifier, an interface whose
dynamic type depends on how the SET was built. Parse delegates sub_id to
subjectid.Parse, whose registry constructors return pointers, so a parsed
SET carries the pointer form (*subjectid.IssSubID). A SET built in Go
naturally holds the value form (subjectid.IssSubID), because the concrete
types satisfy the interface with value receivers. The result is an
asymmetric round-trip: a consumer writes IssSubID{} but reads back
*IssSubID, and the obvious assertion set.Subject.(subjectid.IssSubID)
silently takes the !ok branch after a parse and drops the event. It is the
kind of bug that passes a hand-built unit test (value form) and fails on
real wire input (pointer form). A downstream Shared Signals consumer hit
exactly this.
SubjectAs[T] absorbs the distinction once for all consumers: it tries a
direct assertion to T (the value form) and, failing that, peels a single
pointer indirection via reflection to match the pointer form, returning
the value either way. A type assertion to *T will not compile for an
arbitrary type parameter, so the pointer case goes through reflect; the
nil-pointer guard keeps reflect.Value.Elem from panicking on a typed nil
in the interface. IssSub is the non-generic shorthand for the
overwhelmingly common iss_sub format. The SET.Subject godoc now states the
parsed dynamic type so the asymmetry is discoverable from godoc alone.
This is the non-breaking, consumer-side half of the fix. Normalizing the
canonical form in go-subjectid's Parse is a wider, possibly breaking
decision and is tracked separately.
Tests cover the value form, the pointer form, the absent / nil-receiver /
typed-nil / wrong-format misses, and the acceptance round-trip
(SET{Subject: IssSubID{...}} through Encode and Parse back to an equal
IssSubID via the accessor). A runnable ExampleSET_IssSub documents the
parse-then-read flow. No wire-format change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
What
Adds an ergonomic, typed accessor over
SET.Subjectso consumers don't have to handle a value-vs-pointer dynamic-type asymmetry by hand.SubjectAs[T subjectid.SubjectIdentifier](s *SET) (T, bool)— the general accessor.(*SET) IssSub() (subjectid.IssSubID, bool)— non-generic shorthand for the common iss_sub case.SET.Subjectgodoc now states the parsed dynamic type (pointer form).ExampleSET_IssSub.Why
SET.Subjectis typedsubjectid.SubjectIdentifier(an interface). Its dynamic type depends on how the SET was built:Parsedelegatessub_idtosubjectid.Parse, whose registry constructors return pointers, soset.Subjectis*subjectid.IssSubID.subjectid.IssSubID{}.A consumer therefore writes
IssSubID{}but reads back*IssSubID; the obvious assertionset.Subject.(subjectid.IssSubID)silently takes the!okbranch after a parse and drops the event — a footgun that passes a hand-built unit test and fails on real wire input (or vice-versa).SubjectAstries the value form first, then peels a single pointer indirection via reflection, returning the value either way (a type assertion to*Twon't compile for an arbitrary type parameter; a nil-pointer guard keepsreflect.Value.Elemfrom panicking on a typed nil).This is the non-breaking, consumer-side half. Normalizing the canonical form in
go-subjectid'sParseis a wider, possibly breaking decision and is tracked separately.Tests
Value form, pointer form, absent / nil-receiver / typed-nil / wrong-format misses, and the acceptance round-trip (
SET{Subject: IssSubID{...}}→Encode→Parse→ equalIssSubIDvia the accessor). No wire-format change.🤖 Generated with Claude Code