Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ A value without one of the type's prefixes fails with a `*WrongPrefixError`, whi

`ObjectType`, `Separator` and `Prefix` are named string types, so an object type, a separator and a prefix cannot be swapped by mistake. `Bare()` and the parse inputs stay `string`, because they are the wire and storage boundary.

### Work with UUIDs

`FromUUID[T](u)` builds an ID from a `uuid.UUID`, and `id.UUID()` returns the bare value as a `uuid.UUID`. `Scan` and `DecodeSpanner` also accept `uuid.UUID` and `[16]byte`, for drivers and columns that carry native UUIDs. These need `StorageFormat: FormatDropPrefix`, because a native UUID has no prefix.

## Emitted forms

| Method | Emits | Controlled by |
Expand Down
13 changes: 13 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"encoding/json"
"fmt"

"github.com/google/uuid"

"github.com/TrogonStack/trogonobjectid"
)

Expand Down Expand Up @@ -44,3 +46,14 @@ func ExampleID_EncodeSpanner() {
// 0199e3a1-7c2b-7d4e-9f10-2a3b4c5d6e7f
// true
}

func ExampleFromUUID() {
u := uuid.MustParse("0199e3a1-7c2b-7d4e-9f10-2a3b4c5d6e7f")
id, _ := trogonobjectid.FromUUID[invoiceType](u)
back, _ := id.UUID()
fmt.Println(id)
fmt.Println(back == u)
// Output:
// inv_0199e3a1-7c2b-7d4e-9f10-2a3b4c5d6e7f
// true
}
6 changes: 6 additions & 0 deletions objectid.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"reflect"
"strings"
"sync"

"github.com/google/uuid"
)

// ObjectType names an object ID type, for example "user". It is lowercase snake_case
Expand Down Expand Up @@ -385,6 +387,10 @@ func (id *ID[T]) Scan(src any) error {
value = v
case []byte:
value = string(v)
case uuid.UUID:
value = v.String()
case [16]byte:
value = uuid.UUID(v).String()
default:
return fmt.Errorf("%w: cannot scan %T", ErrInvalidFormat, src)
}
Expand Down
25 changes: 25 additions & 0 deletions uuid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package trogonobjectid

import (
"fmt"

"github.com/google/uuid"
)

// FromUUID returns the ID for u, after the type's Validate.
func FromUUID[T Definer](u uuid.UUID) (ID[T], error) {
return New[T](u.String())
}

// UUID returns the bare value as a uuid.UUID, so callers that work with UUIDs do not
// parse Bare themselves. The zero ID fails with ErrEmptyValue.
func (id ID[T]) UUID() (uuid.UUID, error) {
if id.IsZero() {
return uuid.Nil, &ValidationError{ObjectTypes: mustSpec[T]().objectTypes, Err: ErrEmptyValue}
}
u, err := uuid.Parse(id.id)
if err != nil {
return uuid.Nil, &ValidationError{ObjectTypes: mustSpec[T]().objectTypes, Value: id.id, Err: fmt.Errorf("%w: %w", ErrInvalidUUID, err)}
}
return u, nil
}
63 changes: 63 additions & 0 deletions uuid_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package trogonobjectid_test

import (
"testing"

"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/TrogonStack/trogonobjectid"
)

func TestFromUUID(t *testing.T) {
u := uuid.MustParse(invoiceUUID)

id, err := trogonobjectid.FromUUID[invoiceType](u)
require.NoError(t, err)
assert.Equal(t, trogonobjectid.MustNew[invoiceType](invoiceUUID), id)

t.Run("validates against the type", func(t *testing.T) {
_, err := trogonobjectid.FromUUID[seqType](u)
require.ErrorIs(t, err, trogonobjectid.ErrInvalidInteger)
})
}

func TestIDUUID(t *testing.T) {
got, err := trogonobjectid.MustNew[invoiceType](invoiceUUID).UUID()
require.NoError(t, err)
assert.Equal(t, uuid.MustParse(invoiceUUID), got)

t.Run("the zero ID has no UUID", func(t *testing.T) {
_, err := InvoiceID{}.UUID()
require.ErrorIs(t, err, trogonobjectid.ErrEmptyValue)
})

t.Run("a bare value that is not a UUID has no UUID", func(t *testing.T) {
_, err := trogonobjectid.MustNew[userType]("abc-123").UUID()
require.ErrorIs(t, err, trogonobjectid.ErrInvalidUUID)
})
}

func TestScanNativeUUID(t *testing.T) {
want := trogonobjectid.MustNew[invoiceType](invoiceUUID)
u := uuid.MustParse(invoiceUUID)

for name, src := range map[string]any{"uuid.UUID": u, "[16]byte": [16]byte(u)} {
t.Run("Scan accepts "+name, func(t *testing.T) {
var got InvoiceID
require.NoError(t, got.Scan(src))
assert.Equal(t, want, got)
})
t.Run("DecodeSpanner accepts "+name, func(t *testing.T) {
var got InvoiceID
require.NoError(t, got.DecodeSpanner(src))
assert.Equal(t, want, got)
})
}

t.Run("a full storage format rejects a native UUID", func(t *testing.T) {
var got UserID
require.ErrorIs(t, got.Scan(u), trogonobjectid.ErrInvalidFormat)
})
}
Loading