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
9 changes: 2 additions & 7 deletions encoding.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package typeid

import (
"errors"
"fmt"

"github.com/gofrs/uuid/v5"
Expand Down Expand Up @@ -48,10 +47,6 @@ func scan[T idImplementation[P], P Prefix](dst *T, src any) error {
return nil
}

var (
errNilScan = errors.New("cannot scan NULL into *typeid.TypeID")
)

func textValue[T idImplementation[P], P Prefix](id T) (pgtype.Text, error) {
return pgtype.Text{
String: id.String(),
Expand All @@ -63,7 +58,7 @@ func scanText[T idImplementation[P], P Prefix](dst *T, v pgtype.Text) error {
var err error

if !v.Valid {
return errNilScan
return fmt.Errorf("cannot scan NULL into %T", dst)
}

*dst, err = FromString[T](v.String)
Expand All @@ -85,7 +80,7 @@ func scanUUID[T idImplementation[P], P Prefix](dst *T, v pgtype.UUID) error {
var err error

if !v.Valid {
return errNilScan
return fmt.Errorf("cannot scan NULL into %T", dst)
}

*dst, err = FromUUIDBytes[T](v.Bytes[:])
Expand Down
7 changes: 4 additions & 3 deletions encoding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,9 @@ func TestTypeID_Pgx_Scan(t *testing.T) {
if err == nil {
t.Error("must error on a nil scan")
}
if !strings.Contains(err.Error(), "cannot scan NULL into *typeid.TypeID") {
t.Error("error must be cannot scan NULL into *typeid.TypeID")
expect := "cannot scan NULL into *typeid.Random[github.com/sumup/typeid.userPrefix]"
if !strings.Contains(err.Error(), expect) {
t.Errorf("error must be %q, was %q", expect, err.Error())
}
})
}
Expand Down Expand Up @@ -205,7 +206,7 @@ func TestTypeID_SQL_Value(t *testing.T) {
}

func TestJSON(t *testing.T) {
str := "account_00041061050r3gg28a1c60t3gf"
str := "system_account_00041061050r3gg28a1c60t3gf"
tid := Must(FromString[AccountID](str))

encoded, err := json.Marshal(tid)
Expand Down
9 changes: 5 additions & 4 deletions generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,16 @@ func validatePrefix(prefix string) error {
}

if len(prefix) > MaxPrefixLen {
return fmt.Errorf("invalid prefix: %s. Prefix length is %d, expected <= %d", prefix, len(prefix), MaxPrefixLen)
return fmt.Errorf("invalid prefix: %s, prefix length is %d, expected <= %d", prefix, len(prefix), MaxPrefixLen)
}

// Ensure that the prefix has only lowercase ASCII characters
// Ensure that the prefix has only lowercase ASCII characters and underscores
for _, c := range prefix {
if c < 'a' || c > 'z' {
return fmt.Errorf("invalid prefix: '%s'. Prefix should match [a-z]{0,%d}", prefix, MaxPrefixLen)
if c != '_' && (c < 'a' || c > 'z') {
return fmt.Errorf("invalid prefix: '%s', prefix should match [a-z_]{0,%d}", prefix, MaxPrefixLen)
}
}

return nil
}

Expand Down
40 changes: 11 additions & 29 deletions typeid.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import (
"github.com/gofrs/uuid/v5"
)

var (
ErrParse = errors.New("parse typeid")
)

const (
// MaxPrefixLen is the maximum string length of a [Prefix]. Any generation or parsing of an ID type with a longer prefix will fail.
MaxPrefixLen = 63
Expand Down Expand Up @@ -81,38 +85,16 @@ func MustNew[T instance[P], P Prefix]() T {
}

func FromString[T instance[P], P Prefix](s string) (T, error) {
prefix, suffix, ok := strings.Cut(s, "_")
if !ok {
// If there is no prefix, the first string part is the suffix.
return fromUnprefixString[T](prefix)
prefix := getPrefix[P]()
if prefix != "" && !strings.HasPrefix(s, prefix+"_") {
return Nil[T](), fmt.Errorf("%w: invalid prefix for %T, expected %q", ErrParse, T{}, prefix)
}

return fromPrefixedString[T](prefix, suffix)
}

func fromPrefixedString[T instance[P], P Prefix](prefix, suffix string) (T, error) {
if prefix == "" {
return Nil[T](), errors.New("typeid prefix cannot be empty when there's a separator")
} else if prefix != getPrefix[P]() {
return Nil[T](), fmt.Errorf("invalid prefix `%s` for typeid %T. Expected %s", prefix, T{}, getPrefix[P]())
}

tid, err := from[P](suffix, T{}.processor())
if err != nil {
return Nil[T](), fmt.Errorf("parse typeid suffix `%s`: %w", suffix, err)
}
return T{tid}, nil
}

func fromUnprefixString[T instance[P], P Prefix](suffix string) (T, error) {
// Unprefixed ID strings are only valid, if the type ids prefix is the empty string
if getPrefix[P]() != "" {
return Nil[T](), fmt.Errorf("no prefix in id string %s for type %T. Expected %s", suffix, T{}, getPrefix[P]())
}
suffix := strings.TrimPrefix(s, prefix+"_")

tid, err := from[P](suffix, T{}.processor())
if err != nil {
return Nil[T](), fmt.Errorf("parse typeid suffix `%s`: %w", suffix, err)
return Nil[T](), fmt.Errorf("%w: invalid suffix %q: %s", ErrParse, suffix, err.Error())
}
return T{tid}, nil
}
Expand All @@ -128,15 +110,15 @@ func FromUUID[T instance[P], P Prefix](u uuid.UUID) (T, error) {
func FromUUIDStr[T instance[P], P Prefix](uuidStr string) (T, error) {
u, err := uuid.FromString(uuidStr)
if err != nil {
return Nil[T](), fmt.Errorf("typeid from uuid string: %w", err)
return Nil[T](), fmt.Errorf("%w: uuid from string: %s", ErrParse, err.Error())
}
return FromUUID[T](u)
}

func FromUUIDBytes[T instance[P], P Prefix](bytes []byte) (T, error) {
u, err := uuid.FromBytes(bytes)
if err != nil {
return Nil[T](), fmt.Errorf("typeid from uuid: %w", err)
return Nil[T](), fmt.Errorf("%w: uuid from bytes: %s", ErrParse, err.Error())
}
return FromUUID[T](u)
}
7 changes: 5 additions & 2 deletions typeid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (

const (
userIDPrefix = "user"
accountIDPrefix = "account"
accountIDPrefix = "system_account"

emptyID = "00000000000000000000000000" // empty is suffix is 26 zeros
)
Expand Down Expand Up @@ -70,7 +70,7 @@ func TestTypeID_Nil(t *testing.T) {
}

nilAccountID := Nil[AccountID]()
if "account_"+emptyID != nilAccountID.String() {
if "system_account_"+emptyID != nilAccountID.String() {
t.Errorf("expected nil account id, got: %s", nilAccountID.String())
}

Expand All @@ -88,6 +88,9 @@ func TestTypeID_ToFrom(t *testing.T) {

type SortableID = AccountID
t.Run("typeid.Sortable", runToFromQuickTests[SortableID])

type EmptyPrefixID = NilID
t.Run("empty prefix", runToFromQuickTests[EmptyPrefixID])
}

func runToFromQuickTests[T idImplementation[P], P Prefix](t *testing.T) {
Expand Down