Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ee86250
feat: normalize bare values and encode IDs for Spanner
enilsen16 Oct 22, 2026
87bf9aa
feat: add proto definition options and cache definitions per type
enilsen16 Oct 22, 2026
383a653
feat!: name object types and separators, and check definitions early
enilsen16 Oct 22, 2026
ca69a49
feat: build IDs from UUIDs and scan native UUID values
enilsen16 Oct 22, 2026
a600fe5
feat: add objectidtest for deterministic IDs in tests
enilsen16 Oct 22, 2026
cd30639
feat!: narrow unions with UnionAs and name member lists Types
enilsen16 Oct 22, 2026
86dfc59
refactor: share UUID parsing, encode unions for Spanner, and trim tests
enilsen16 Oct 22, 2026
4162f19
Merge branch 'feat/normalize-and-spanner' into feat/proto-definition-…
enilsen16 Oct 22, 2026
8cf62c1
refactor: share the spec cache, store pointers, and take a base Defin…
enilsen16 Oct 22, 2026
1dab7e6
perf: skip rebuilding canonical UUIDs and share UUID parsing
enilsen16 Oct 22, 2026
f3f8ae1
Merge branch 'feat/normalize-and-spanner' into feat/proto-definition-…
enilsen16 Oct 22, 2026
37fbfbc
Merge branch 'feat/proto-definition-options' into feat/named-types-an…
enilsen16 Oct 22, 2026
f68966b
refactor: check object types with a loop and list accepted prefixes i…
enilsen16 Oct 22, 2026
ec1eb1d
refactor: add a Prefix type and list ObjectTypes on validation errors
enilsen16 Oct 22, 2026
2d05350
Merge branch 'feat/named-types-and-checks' into feat/typed-uuid-access
enilsen16 Oct 22, 2026
76f106d
Merge branch 'feat/named-types-and-checks' into feat/typed-uuid-access
enilsen16 Oct 22, 2026
d193488
Merge branch 'feat/typed-uuid-access' into feat/objectidtest
enilsen16 Oct 22, 2026
a091f89
Merge branch 'feat/objectidtest' into feat/union-as
enilsen16 Oct 22, 2026
7dc80af
Merge remote-tracking branch 'origin/main' into feat/proto-definition…
enilsen16 Oct 22, 2026
2a567cd
refactor: rename TypeOf back to MemberOf
enilsen16 Oct 22, 2026
bef78d5
Merge remote-tracking branch 'origin/main' into feat/proto-definition…
enilsen16 Oct 22, 2026
bac0683
Merge branch 'feat/proto-definition-options' into feat/named-types-an…
enilsen16 Oct 22, 2026
d7fea09
Merge branch 'feat/named-types-and-checks' into feat/typed-uuid-access
enilsen16 Oct 22, 2026
658fd8c
docs: drop the Normalize mention from FromUUID
enilsen16 Oct 22, 2026
e2ba276
Merge branch 'feat/typed-uuid-access' into feat/objectidtest
enilsen16 Oct 22, 2026
a86472e
Merge branch 'feat/objectidtest' into feat/union-as
enilsen16 Oct 22, 2026
bd3a274
Merge remote-tracking branch 'origin/main' into feat/union-as
enilsen16 Oct 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ A `Union` holds one ID of any of several types and picks the type by prefix:
```go
type principalUnion struct{}

func (principalUnion) Members() []trogonobjectid.MemberType {
func (principalUnion) Types() []trogonobjectid.MemberType {
return []trogonobjectid.MemberType{
trogonobjectid.MemberOf[userType](),
trogonobjectid.MemberOf[systemType](),
Expand All @@ -61,9 +61,8 @@ func (principalUnion) Members() []trogonobjectid.MemberType {
type PrincipalID = trogonobjectid.Union[principalUnion]

p, err := trogonobjectid.ParseUnion[principalUnion]("system_cron")
switch m := p.Member().(type) {
case SystemID:
// m.Bare() == "cron"
if sys, ok := trogonobjectid.UnionAs[systemType](p); ok {
// sys.Bare() == "cron"
}
```

Expand Down
15 changes: 11 additions & 4 deletions union.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func MemberOf[T Definer]() MemberType {
//
// type principalUnion struct{}
//
// func (principalUnion) Members() []trogonobjectid.MemberType {
// func (principalUnion) Types() []trogonobjectid.MemberType {
// return []trogonobjectid.MemberType{
// trogonobjectid.MemberOf[userType](),
// trogonobjectid.MemberOf[systemType](),
Expand All @@ -52,11 +52,11 @@ func MemberOf[T Definer]() MemberType {
//
// type PrincipalID = trogonobjectid.Union[principalUnion]
type UnionDefiner interface {
Members() []MemberType
Types() []MemberType
}

// Union holds one ID of any of its member types, chosen by prefix when parsing.
// Use a type switch on Member to get the typed ID back.
// Use UnionAs, or a type switch on Member, to get the typed ID back.
//
// Every encoding uses the member's full form, because the prefix is the only thing
// that tells the member types apart. The zero value is an absent ID.
Expand All @@ -77,7 +77,7 @@ func unionSpecFor[U UnionDefiner]() (*unionSpec, error) {

func newUnionSpec[U UnionDefiner]() (unionSpec, error) {
var marker U
members := marker.Members()
members := marker.Types()
if len(members) == 0 {
return unionSpec{}, fmt.Errorf("%w: %T has no members", ErrInvalidDefinition, marker)
}
Expand Down Expand Up @@ -169,6 +169,13 @@ func MustParseUnion[U UnionDefiner](str string) Union[U] {
// Member returns the wrapped ID, or nil for the zero union.
func (u Union[U]) Member() Member { return u.member }

// UnionAs returns u's member as an ID[T], and false when u holds another type or
// is zero. It narrows a union without a type switch.
func UnionAs[T Definer, U UnionDefiner](u Union[U]) (ID[T], bool) {
id, ok := u.member.(ID[T])
return id, ok
}

// IsZero reports whether u is the zero (absent) union.
func (u Union[U]) IsZero() bool { return u.member == nil }

Expand Down
25 changes: 20 additions & 5 deletions union_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type SystemID = trogonobjectid.ID[systemType]

type principalUnion struct{}

func (principalUnion) Members() []trogonobjectid.MemberType {
func (principalUnion) Types() []trogonobjectid.MemberType {
return []trogonobjectid.MemberType{
trogonobjectid.MemberOf[userType](),
trogonobjectid.MemberOf[systemType](),
Expand All @@ -48,7 +48,7 @@ func (acmeAdminType) Definition() trogonobjectid.Definition {

type overlapUnion struct{}

func (overlapUnion) Members() []trogonobjectid.MemberType {
func (overlapUnion) Types() []trogonobjectid.MemberType {
return []trogonobjectid.MemberType{
trogonobjectid.MemberOf[acmeType](),
trogonobjectid.MemberOf[acmeAdminType](),
Expand All @@ -57,11 +57,11 @@ func (overlapUnion) Members() []trogonobjectid.MemberType {

type emptyUnion struct{}

func (emptyUnion) Members() []trogonobjectid.MemberType { return nil }
func (emptyUnion) Types() []trogonobjectid.MemberType { return nil }

type duplicateUnion struct{}

func (duplicateUnion) Members() []trogonobjectid.MemberType {
func (duplicateUnion) Types() []trogonobjectid.MemberType {
return []trogonobjectid.MemberType{
trogonobjectid.MemberOf[userType](),
trogonobjectid.MemberOf[userType](),
Expand All @@ -70,7 +70,7 @@ func (duplicateUnion) Members() []trogonobjectid.MemberType {

type badMemberUnion struct{}

func (badMemberUnion) Members() []trogonobjectid.MemberType {
func (badMemberUnion) Types() []trogonobjectid.MemberType {
return []trogonobjectid.MemberType{trogonobjectid.MemberOf[badType]()}
}

Expand Down Expand Up @@ -249,6 +249,21 @@ func TestUnionZero(t *testing.T) {
assert.Empty(t, p.String())
}

func TestUnionAs(t *testing.T) {
user := trogonobjectid.MustNew[userType]("abc-123")
u := trogonobjectid.MustNewUnion[principalUnion](user)

got, ok := trogonobjectid.UnionAs[userType](u)
require.True(t, ok)
assert.Equal(t, user, got)

_, ok = trogonobjectid.UnionAs[systemType](u)
assert.False(t, ok)

_, ok = trogonobjectid.UnionAs[userType](trogonobjectid.Union[principalUnion]{})
assert.False(t, ok)
}

func TestUnionSpanner(t *testing.T) {
want := trogonobjectid.MustNewUnion[principalUnion](trogonobjectid.MustNew[userType]("abc-123"))

Expand Down
Loading