diff --git a/CHANGELOG.md b/CHANGELOG.md index 190839c..131cf4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Changed + +- `SubjectAs[T]` and `SET.IssSub` now match the value form of the `sub_id` + Subject Identifier only. go-subjectid v0.2.0 made the value form the single + dynamic type `Parse` returns and a Go-built `SET` should hold, so the + reflection-based pointer-form fallback the accessors carried is unreachable + through any supported flow; it is removed along with its `reflect` import. A + `SET` hand-built with a `*subjectid.IssSubID` Subject now reports `ok=false` + from these accessors instead of being dereferenced. Consumers reading subjects + produced by `Parse`, or building SETs with value-form subjects, are unaffected. + ## [0.2.0] - 2026-06-21 ### Changed diff --git a/encode_test.go b/encode_test.go index 591b974..da778d0 100644 --- a/encode_test.go +++ b/encode_test.go @@ -23,7 +23,7 @@ func TestEncodeFullyPopulated(t *testing.T) { IssuedAt: time.Unix(1700000000, 0).UTC(), JWTID: "abc123", Audience: Audience{"https://rp.example.com"}, - Subject: &subjectid.IssSubID{ + Subject: subjectid.IssSubID{ Iss: "https://issuer.example.com/", Sub: "145234573", }, diff --git a/example_test.go b/example_test.go index 2b5c8ce..1ff5592 100644 --- a/example_test.go +++ b/example_test.go @@ -102,8 +102,7 @@ func Example_encode() { } // ExampleSET_IssSub reads the typed sub_id of a parsed SET. IssSub returns the -// iss_sub value directly regardless of whether the held identifier is the value -// or pointer form, so a consumer never has to handle the distinction itself. +// iss_sub value directly, so a consumer never has to type-assert SET.Subject. func ExampleSET_IssSub() { // Verified, base64url-decoded claims-set bytes carrying an iss_sub subject. payload := []byte(`{ diff --git a/secevent.go b/secevent.go index 4a6f570..7bda879 100644 --- a/secevent.go +++ b/secevent.go @@ -59,10 +59,8 @@ type SET struct { // RFC 9493 §3). // // Parse yields the value form of the identifier (for example - // subjectid.IssSubID), the same form a SET built in Go naturally holds; a - // SET may also be hand-built with the pointer form (*subjectid.IssSubID). - // Use SubjectAs or IssSub to read the concrete subject without handling - // both forms by hand. + // subjectid.IssSubID), the same form a SET built in Go holds. Use SubjectAs + // or IssSub to read the concrete subject as its typed value. Subject subjectid.SubjectIdentifier // TransactionID (txn) optionally correlates the SET with related events or diff --git a/subject.go b/subject.go index f1b0eba..bca8cbe 100644 --- a/subject.go +++ b/subject.go @@ -3,27 +3,19 @@ package secevent -import ( - "reflect" - - "github.com/hstern/go-subjectid" -) +import "github.com/hstern/go-subjectid" // SubjectAs returns the SET's sub_id as the concrete Subject Identifier type T, // reporting ok=false when the subject is absent or of a different format. // -// It exists to absorb the value/pointer distinction in the Subject field. // go-subjectid's Parse returns identifiers in their value form (for example -// subjectid.IssSubID), the same form a SET built in Go naturally holds, so the -// two share a dynamic type. A SET may nonetheless be hand-built with the -// pointer form (*subjectid.IssSubID also satisfies the interface), and a -// consumer that type-asserts SET.Subject directly would have to handle both. -// SubjectAs handles them once: it accepts the value type as T (for example -// SubjectAs[subjectid.IssSubID]) and matches whether the held identifier is the -// value or the pointer form, always returning the value. +// subjectid.IssSubID), the same form a SET built in Go holds, so T is the value +// type — SubjectAs[subjectid.IssSubID]. It is a typed, nil-safe read of the +// SET.Subject interface: a checked type assertion that also tolerates a nil SET +// or an absent subject. // // if iss, ok := secevent.SubjectAs[subjectid.IssSubID](set); ok { -// // iss is a subjectid.IssSubID regardless of how set was built +// // iss is a subjectid.IssSubID // } // // For the overwhelmingly common iss_sub case, (*SET).IssSub is a shorthand. @@ -32,30 +24,16 @@ func SubjectAs[T subjectid.SubjectIdentifier](s *SET) (T, bool) { if s == nil || s.Subject == nil { return zero, false } - - // The value form (a SET built in Go) matches directly. if v, ok := s.Subject.(T); ok { return v, true } - - // The pointer form (a hand-built SET) is *T; dereference it. A type - // assertion to *T will not compile for an arbitrary type parameter, so - // reach for reflection to peel exactly one pointer indirection. - rv := reflect.ValueOf(s.Subject) - if rv.Kind() == reflect.Pointer && !rv.IsNil() { - if v, ok := reflect.TypeAssert[T](rv.Elem()); ok { - return v, true - } - } - return zero, false } // IssSub returns the SET's sub_id as a subjectid.IssSubID when the subject is // present and in the iss_sub format, reporting ok=false otherwise. It is a // shorthand for SubjectAs[subjectid.IssSubID] covering the most common subject -// format, and it transparently handles both the value and pointer forms of the -// held identifier (see SubjectAs). +// format. func (s *SET) IssSub() (subjectid.IssSubID, bool) { return SubjectAs[subjectid.IssSubID](s) } diff --git a/subject_test.go b/subject_test.go index 81a0281..4e4ff35 100644 --- a/subject_test.go +++ b/subject_test.go @@ -73,18 +73,19 @@ func TestSubjectAsValueForm(t *testing.T) { } } -// TestSubjectAsPointerForm checks that SubjectAs and IssSub also read the -// pointer form — the shape a SET hand-built with *subjectid.IssSubID carries. -// This is the value/pointer distinction the accessor absorbs. -func TestSubjectAsPointerForm(t *testing.T) { - want := subjectid.IssSubID{Iss: "https://issuer.example.com/", Sub: "145234573"} - s := SET{Subject: &want} +// TestSubjectAsPointerFormUnmatched pins the value-only contract: a SET +// hand-built with the pointer form of an identifier (*subjectid.IssSubID) is +// reported as not found rather than dereferenced. go-subjectid v0.2.0 made the +// value form the single shape Parse returns and a Go-built SET should hold, so +// the accessors match the value form only. +func TestSubjectAsPointerFormUnmatched(t *testing.T) { + s := SET{Subject: &subjectid.IssSubID{Iss: "https://issuer.example.com/", Sub: "145234573"}} - if got, ok := SubjectAs[subjectid.IssSubID](&s); !ok || got != want { - t.Errorf("SubjectAs[IssSubID] = (%+v, %v), want (%+v, true)", got, ok, want) + if got, ok := SubjectAs[subjectid.IssSubID](&s); ok { + t.Errorf("SubjectAs[IssSubID] with pointer-form Subject = (%+v, true), want ok false", got) } - if got, ok := s.IssSub(); !ok || got != want { - t.Errorf("IssSub() = (%+v, %v), want (%+v, true)", got, ok, want) + if got, ok := s.IssSub(); ok { + t.Errorf("IssSub() with pointer-form Subject = (%+v, true), want ok false", got) } } @@ -147,8 +148,8 @@ func TestSubjectAsMismatch(t *testing.T) { t.Run("typed nil subject", func(t *testing.T) { // A typed nil pointer in the interface is not == nil, so it slips past - // the s.Subject == nil guard and reaches the reflection path; the - // !rv.IsNil() check is what keeps rv.Elem() from panicking. + // the s.Subject == nil guard; the value-form type assertion then fails + // without dereferencing it, so there is no panic. var p *subjectid.IssSubID s := SET{Subject: p} if got, ok := SubjectAs[subjectid.IssSubID](&s); ok {