From 0ffeb380047205e069a4ecbe2ab510b83f3a4c86 Mon Sep 17 00:00:00 2001 From: Dmitry Afanasiev Date: Fri, 7 Jun 2024 10:34:51 +0300 Subject: [PATCH] add: support different basis types for id generation --- shortuuid.go | 37 ++++++----- shortuuid_test.go | 8 +++ shortuuid_typed.go | 154 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 182 insertions(+), 17 deletions(-) create mode 100644 shortuuid_typed.go diff --git a/shortuuid.go b/shortuuid.go index 7ae1ef5..4e0a9d4 100644 --- a/shortuuid.go +++ b/shortuuid.go @@ -1,8 +1,6 @@ package shortuuid import ( - "strings" - "github.com/google/uuid" ) @@ -18,35 +16,40 @@ type Encoder interface { // New returns a new UUIDv4, encoded with base57. func New() string { - return DefaultEncoder.Encode(uuid.New()) + rv, err := NewTyped(UUID_v4) + if err != nil { + panic(err) + } + return rv } // NewWithEncoder returns a new UUIDv4, encoded with enc. func NewWithEncoder(enc Encoder) string { - return enc.Encode(uuid.New()) + rv, err := NewTypedWithEncoder(UUID_v4, enc) + if err != nil { + panic(err) + } + return rv } // NewWithNamespace returns a new UUIDv5 (or v4 if name is empty), encoded with base57. func NewWithNamespace(name string) string { - var u uuid.UUID - - switch { - case name == "": - u = uuid.New() - case strings.HasPrefix(strings.ToLower(name), "http://"): - u = uuid.NewSHA1(uuid.NameSpaceURL, []byte(name)) - case strings.HasPrefix(strings.ToLower(name), "https://"): - u = uuid.NewSHA1(uuid.NameSpaceURL, []byte(name)) - default: - u = uuid.NewSHA1(uuid.NameSpaceDNS, []byte(name)) + rv, err := NewTypedWithNamespace(UUID_v4, UUID_v5, name) + if err != nil { + panic(err) } - return DefaultEncoder.Encode(u) + return rv } // NewWithAlphabet returns a new UUIDv4, encoded with base57 using the // alternative alphabet abc. func NewWithAlphabet(abc string) string { enc := base57{newAlphabet(abc)} - return enc.Encode(uuid.New()) + rv, err := NewTypedWithAlphabet(UUID_v4, abc, enc) + if err != nil { + panic(err) + } + + return rv } diff --git a/shortuuid_test.go b/shortuuid_test.go index 1bc6492..f5fbfe0 100644 --- a/shortuuid_test.go +++ b/shortuuid_test.go @@ -131,8 +131,16 @@ var testVector = []struct { func TestGeneration(t *testing.T) { tests := []string{ "", + "http", + "http:", + "http:/", + "http_some", "http://www.example.com/", "HTTP://www.example.com/", + "https://www.example.com/", + "HTTPS://www.example.com/", + "HttPS://www.example.com/", + "httpS://www.example.com/", "example.com/", } diff --git a/shortuuid_typed.go b/shortuuid_typed.go new file mode 100644 index 0000000..34008dc --- /dev/null +++ b/shortuuid_typed.go @@ -0,0 +1,154 @@ +package shortuuid + +import ( + "fmt" + "github.com/google/uuid" +) + +type UnderlyingType int + +const ( + UUID_v1 UnderlyingType = 0 + UUID_v3 UnderlyingType = iota + UUID_v4 UnderlyingType = iota + UUID_v5 UnderlyingType = iota + UUID_v6 UnderlyingType = iota + UUID_v7 UnderlyingType = iota +) + +func ut2uuid(ut UnderlyingType) (uuid.UUID, error) { + switch ut { + case UUID_v1: + return uuid.NewUUID() + case UUID_v4: + return uuid.New(), nil + case UUID_v6: + return uuid.NewV6() + case UUID_v7: + return uuid.NewV7() + default: + panic("unknown underlying type") + } +} + +// NewTyped returns a new id (based on ut type), encoded with DefaultEncoder. +func NewTyped(ut UnderlyingType) (string, error) { + rv, err := ut2uuid(ut) + if err != nil { + return "", err + } + + return DefaultEncoder.Encode(rv), nil +} + +// NewTypedWithEncoder returns a new id (based on ut type), encoded with enc. +func NewTypedWithEncoder(ut UnderlyingType, enc Encoder) (string, error) { + rv, err := ut2uuid(ut) + if err != nil { + return "", err + } + + return enc.Encode(rv), nil +} + +// NewTypedWithNamespace returns a new id (based on ut type and name) +// +// when name is empty id will be based on emptyNameUt, +// otherwise id will be based on ut +func NewTypedWithNamespace(emptyNameUt UnderlyingType, ut UnderlyingType, name string) (string, error) { + nameLen := len(name) + + if nameLen == 0 { + rv, err := ut2uuid(emptyNameUt) + if err != nil { + return "", err + } + + return DefaultEncoder.Encode(rv), nil + } + + ns := (func(name string, nameLen int) uuid.UUID { + //returns namespace by name prefix (case-insensitive compare) + var ch uint8 + if nameLen >= len("http://") { + for { + idx := 0 + ch = name[idx] + if ch != 'h' && ch != 'H' { + break + } + + idx++ + ch = name[idx] + if ch != 't' && ch != 'T' { + break + } + + idx++ + ch = name[idx] + if ch != 't' && ch != 'T' { + break + } + + idx++ + ch = name[idx] + if ch != 'p' && ch != 'P' { + break + } + + idx++ + ch = name[idx] + if ch != ':' { + //maybe httpS ? + if nameLen >= len("https://") && (ch == 's' || ch == 'S') { + idx++ + ch = name[idx] + if ch != ':' { + break + } + } else { + break + } + } + + idx++ + ch = name[idx] + if ch != '/' { + break + } + + idx++ + ch = name[idx] + if ch != '/' { + break + } + + // yeah! name starts with "http://" or "https://" + return uuid.NameSpaceURL + } + } + + // default is DNS (backward compatibility) + return uuid.NameSpaceDNS + })(name, nameLen) + + switch ut { + case UUID_v5: + return DefaultEncoder.Encode(uuid.NewSHA1(ns, []byte(name))), nil + case UUID_v3: + return DefaultEncoder.Encode(uuid.NewMD5(ns, []byte(name))), nil + default: + return "", fmt.Errorf("unsupported underlying type [%v] for non-empty name", ut) + } +} + +// NewTypedWithAlphabet returns a new id, encoded with enc using the +// alternative alphabet abc. +func NewTypedWithAlphabet(ut UnderlyingType, abc string, enc Encoder) (string, error) { + rv, err := ut2uuid(ut) + if err != nil { + return "", err + } + + return enc.Encode(rv), nil +}