diff --git a/CHANGELOG.md b/CHANGELOG.md index b8890cb..190839c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,21 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [0.2.0] - 2026-06-21 + +### Changed + +- **Breaking:** Upgraded [`go-subjectid`](https://github.com/hstern/go-subjectid) + to v0.2.0, whose `Parse` now returns Subject Identifiers in their value form + (for example `subjectid.IssSubID`) rather than the pointer form. A parsed + `SET` now holds the same value form a `SET` built in Go holds, so a type + switch on `SET.Subject` no longer has to special-case the parsed shape — but + code that asserted the parsed subject as `*subjectid.IssSubID` must now assert + the value form. The `SubjectAs[T]` and `SET.IssSub` accessors are unchanged: + they still return the value form and still absorb a hand-built pointer-form + subject, so consumers already using them need no changes. The `SET.Subject` + documentation now describes the value form as the parsed type. + ## [0.1.1] - 2026-06-21 ### Added @@ -42,6 +57,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). (such as OpenID CAEP and RISC) plug in typed decoders while unknown event types stay raw. -[Unreleased]: https://github.com/hstern/go-secevent/compare/v0.1.1...HEAD +[Unreleased]: https://github.com/hstern/go-secevent/compare/v0.2.0...HEAD +[0.2.0]: https://github.com/hstern/go-secevent/compare/v0.1.1...v0.2.0 [0.1.1]: https://github.com/hstern/go-secevent/compare/v0.1.0...v0.1.1 [0.1.0]: https://github.com/hstern/go-secevent/releases/tag/v0.1.0 diff --git a/conformance_test.go b/conformance_test.go index c9e8639..aa93de1 100644 --- a/conformance_test.go +++ b/conformance_test.go @@ -222,12 +222,12 @@ func assertSubject(t *testing.T, set *SET, tc conformanceCase) { return } - got, ok := set.Subject.(*subjectid.IssSubID) + got, ok := set.Subject.(subjectid.IssSubID) if !ok { - t.Fatalf("Subject is %T, want *subjectid.IssSubID", set.Subject) + t.Fatalf("Subject is %T, want subjectid.IssSubID", set.Subject) } - if *got != tc.wantSubject { - t.Errorf("Subject = %+v, want %+v", *got, tc.wantSubject) + if got != tc.wantSubject { + t.Errorf("Subject = %+v, want %+v", got, tc.wantSubject) } } diff --git a/example_test.go b/example_test.go index 79d0e43..2b5c8ce 100644 --- a/example_test.go +++ b/example_test.go @@ -101,10 +101,9 @@ func Example_encode() { // {"iss":"https://idp.example.com/","iat":1615305600,"jti":"set-0002","aud":"https://receiver.example.com/","sub_id":{"format":"iss_sub","iss":"https://idp.example.com/","sub":"user-7f3e2a"},"events":{"https://schemas.openid.net/secevent/caep/event-type/session-revoked":{"initiating_entity":"policy"}}} } -// ExampleSET_IssSub reads the typed sub_id of a parsed SET. Parse hands back -// the pointer form of the Subject Identifier (a *subjectid.IssSubID), but -// IssSub returns the iss_sub value directly, so a consumer never has to handle -// the pointer/value distinction itself. +// 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. func ExampleSET_IssSub() { // Verified, base64url-decoded claims-set bytes carrying an iss_sub subject. payload := []byte(`{ diff --git a/go.mod b/go.mod index 6ed2f65..26e9f3b 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,4 @@ module github.com/hstern/go-secevent go 1.26 -require github.com/hstern/go-subjectid v0.0.0-20260525222327-b47140763585 +require github.com/hstern/go-subjectid v0.2.0 diff --git a/go.sum b/go.sum index 186f8b9..a1cc940 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,2 @@ -github.com/hstern/go-subjectid v0.0.0-20260525222327-b47140763585 h1:dDFXd9djpbmlAOSlzIbFunmu6YVnzMcOnxPH+ySINZ0= -github.com/hstern/go-subjectid v0.0.0-20260525222327-b47140763585/go.mod h1:KS0TexS0PZQe7CnntjRRzcufxLdMji1DMVpxiBqCvRM= +github.com/hstern/go-subjectid v0.2.0 h1:kakUgtYVbUjIIVLaJcwWnQ9QKFwIw+AQxCZKOGLiOWU= +github.com/hstern/go-subjectid v0.2.0/go.mod h1:KS0TexS0PZQe7CnntjRRzcufxLdMji1DMVpxiBqCvRM= diff --git a/interop_test.go b/interop_test.go index d3bb8d9..f43d24b 100644 --- a/interop_test.go +++ b/interop_test.go @@ -120,12 +120,12 @@ func TestInteropCAEPSessionRevoked(t *testing.T) { } wantSubject := subjectid.IssSubID{Iss: "https://idp.example.com/", Sub: "user-7f3e2a"} - got, isIssSub := set.Subject.(*subjectid.IssSubID) + got, isIssSub := set.Subject.(subjectid.IssSubID) if !isIssSub { - t.Fatalf("Subject is %T, want *subjectid.IssSubID", set.Subject) + t.Fatalf("Subject is %T, want subjectid.IssSubID", set.Subject) } - if *got != wantSubject { - t.Errorf("Subject = %+v, want %+v", *got, wantSubject) + if got != wantSubject { + t.Errorf("Subject = %+v, want %+v", got, wantSubject) } event, ok, err := set.Events.Typed(caepSessionRevokedURI) @@ -189,13 +189,13 @@ func TestInteropRISCAccountDisabled(t *testing.T) { if err != nil { t.Fatalf("decode per-event subject: %v", err) } - issSub, isIssSub := subject.(*subjectid.IssSubID) + issSub, isIssSub := subject.(subjectid.IssSubID) if !isIssSub { - t.Fatalf("per-event subject is %T, want *subjectid.IssSubID", subject) + t.Fatalf("per-event subject is %T, want subjectid.IssSubID", subject) } want := subjectid.IssSubID{Iss: "https://idp.example.com/", Sub: "user-7f3e2a"} - if *issSub != want { - t.Errorf("per-event subject = %+v, want %+v", *issSub, want) + if issSub != want { + t.Errorf("per-event subject = %+v, want %+v", issSub, want) } } diff --git a/parse_test.go b/parse_test.go index 2dc2cd2..910bc5b 100644 --- a/parse_test.go +++ b/parse_test.go @@ -159,9 +159,9 @@ func TestParseSubID(t *testing.T) { if got, want := set.Subject.Format(), "iss_sub"; got != want { t.Errorf("Subject.Format() = %q, want %q", got, want) } - iss, ok := set.Subject.(*subjectid.IssSubID) + iss, ok := set.Subject.(subjectid.IssSubID) if !ok { - t.Fatalf("Subject has type %T, want *subjectid.IssSubID", set.Subject) + t.Fatalf("Subject has type %T, want subjectid.IssSubID", set.Subject) } if iss.Iss != "https://issuer.example.com/" || iss.Sub != "145234573" { t.Errorf("Subject = {Iss:%q Sub:%q}, want {iss/ 145234573}", iss.Iss, iss.Sub) diff --git a/secevent.go b/secevent.go index 50fa6af..4a6f570 100644 --- a/secevent.go +++ b/secevent.go @@ -58,12 +58,11 @@ type SET struct { // field. A nil Subject means the claim is absent. OPTIONAL (RFC 8417 §2.2, // RFC 9493 §3). // - // The interface's dynamic type depends on how the SET was built: Parse - // yields the pointer form (for example *subjectid.IssSubID, because - // go-subjectid's registry constructors return pointers), whereas a SET - // built in Go naturally holds the value form (subjectid.IssSubID). Use - // SubjectAs or IssSub to read the concrete subject without handling both - // forms by hand. + // 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. Subject subjectid.SubjectIdentifier // TransactionID (txn) optionally correlates the SET with related events or diff --git a/subject.go b/subject.go index 58e6d41..f1b0eba 100644 --- a/subject.go +++ b/subject.go @@ -12,15 +12,15 @@ import ( // 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 a pointer/value asymmetry in the Subject field. A SET -// produced by Parse carries the pointer form of its identifier (for example -// *subjectid.IssSubID), because go-subjectid's registry constructors return -// pointers; a SET built in Go naturally holds the value form -// (subjectid.IssSubID), because the concrete types satisfy the interface with -// value receivers. A consumer that type-asserts SET.Subject directly therefore -// has to handle both forms. 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. +// 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. // // if iss, ok := secevent.SubjectAs[subjectid.IssSubID](set); ok { // // iss is a subjectid.IssSubID regardless of how set was built @@ -38,7 +38,7 @@ func SubjectAs[T subjectid.SubjectIdentifier](s *SET) (T, bool) { return v, true } - // The pointer form (a SET from Parse) is *T; dereference it. A type + // 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) diff --git a/subject_test.go b/subject_test.go index 739b7e3..81a0281 100644 --- a/subject_test.go +++ b/subject_test.go @@ -37,12 +37,12 @@ func TestSETSubjectRoundTrip(t *testing.T) { t.Fatalf("subjectid.Parse: %v", err) } - gotIssSub, ok := got.(*subjectid.IssSubID) + gotIssSub, ok := got.(subjectid.IssSubID) if !ok { - t.Fatalf("parsed Subject is %T, want *subjectid.IssSubID", got) + t.Fatalf("parsed Subject is %T, want subjectid.IssSubID", got) } - if *gotIssSub != want { - t.Errorf("Subject round-trip = %+v, want %+v", *gotIssSub, want) + if gotIssSub != want { + t.Errorf("Subject round-trip = %+v, want %+v", gotIssSub, want) } if got.Format() != "iss_sub" { t.Errorf("Format() = %q, want %q", got.Format(), "iss_sub") @@ -74,8 +74,8 @@ func TestSubjectAsValueForm(t *testing.T) { } // TestSubjectAsPointerForm checks that SubjectAs and IssSub also read the -// pointer form — the shape Parse produces, since go-subjectid's registry -// constructors return pointers. This is the asymmetry the accessor absorbs. +// 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} @@ -90,8 +90,8 @@ func TestSubjectAsPointerForm(t *testing.T) { // TestSubjectAsRoundTrip is the acceptance round-trip: a SET built in Go with // the value form of the subject, encoded and parsed back, yields an equal -// subject through the accessor — even though Parse hands back the pointer form. -// A consumer never has to write a pointer/value type switch. +// subject through the accessor. A consumer never has to write a pointer/value +// type switch. func TestSubjectAsRoundTrip(t *testing.T) { want := subjectid.IssSubID{Iss: "https://issuer.example.com/", Sub: "145234573"} s := SET{ @@ -114,10 +114,10 @@ func TestSubjectAsRoundTrip(t *testing.T) { t.Fatalf("Parse: %v", err) } - // Sanity: the parsed subject is the pointer form, which is exactly why the - // accessor is needed. - if _, ok := parsed.Subject.(*subjectid.IssSubID); !ok { - t.Fatalf("parsed Subject is %T, want *subjectid.IssSubID", parsed.Subject) + // Sanity: the parsed subject is the value form go-subjectid's Parse returns, + // the same form fed in — the accessor still spares consumers the type switch. + if _, ok := parsed.Subject.(subjectid.IssSubID); !ok { + t.Fatalf("parsed Subject is %T, want subjectid.IssSubID", parsed.Subject) } got, ok := parsed.IssSub()