Skip to content

Commit

Permalink
Add support for V7 UUIDs
Browse files Browse the repository at this point in the history
  • Loading branch information
rowanseymour committed Jul 31, 2024
1 parent 359c607 commit 70c7263
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 51 deletions.
81 changes: 52 additions & 29 deletions uuids/uuid.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,42 @@ package uuids

import (
"math/rand"
"regexp"

"github.com/nyaruka/gocommon/random"

"github.com/google/uuid"
"github.com/nyaruka/gocommon/dates"
"github.com/nyaruka/gocommon/random"
)

// V4Regex matches a string containing a valid v4 UUID
var V4Regex = regexp.MustCompile(`[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}`)

// V4OnlyRegex matches a string containing only a valid v4 UUID
var V4OnlyRegex = regexp.MustCompile(`^` + V4Regex.String() + `$`)
// UUID is a UUID encoded as a 36 character string using lowercase hex characters
type UUID string

// New returns a new v4 UUID
func New() UUID {
return currentGenerator.Next()
// NewV4 returns a new v4 UUID
func NewV4() UUID {
return currentGenerator.NextV4()
}

// IsV4 returns whether the given string contains only a valid v4 UUID
func IsV4(s string) bool {
return V4OnlyRegex.MatchString(s)
// NewV4 returns a new v7 UUID
func NewV7() UUID {
return currentGenerator.NextV7()
}

// UUID is a 36 character UUID
type UUID string

// Generator is something that can generate a UUID
// Generator is something that can generate UUIDs
type Generator interface {
Next() UUID
NextV4() UUID
NextV7() UUID
}

// defaultGenerator generates a random v4 UUID using a 3rd party library
type defaultGenerator struct{}

// Next returns the next random UUID
func (g defaultGenerator) Next() UUID {
u := uuid.Must(uuid.NewRandom())
return UUID(u.String())
// NextV4 returns the next v4 UUID
func (g defaultGenerator) NextV4() UUID {
return must(uuid.NewRandom())
}

// NextV7 returns the next v7 UUID
func (g defaultGenerator) NextV7() UUID {
return must(uuid.NewV7())
}

// DefaultGenerator is the default generator for calls to NewUUID
Expand All @@ -54,15 +52,40 @@ func SetGenerator(generator Generator) {
// generates a seedable random v4 UUID using math/rand
type seededGenerator struct {
rnd *rand.Rand
now dates.NowSource
}

// NewSeededGenerator creates a new UUID generator that uses the given seed for the random component and the time source
// for the time component (only applies to v7)
func NewSeededGenerator(seed int64, now dates.NowSource) Generator {
return &seededGenerator{rnd: random.NewSeededGenerator(seed), now: now}
}

// NewSeededGenerator creates a new seeded UUID4 generator from the given seed
func NewSeededGenerator(seed int64) Generator {
return &seededGenerator{rnd: random.NewSeededGenerator(seed)}
// NextV4 returns the next v4 UUID
func (g *seededGenerator) NextV4() UUID {
return must(uuid.NewRandomFromReader(g.rnd))
}

// Next returns the next random UUID
func (g *seededGenerator) Next() UUID {
// NextV7 returns the next v7 UUID
func (g *seededGenerator) NextV7() UUID {
u := uuid.Must(uuid.NewRandomFromReader(g.rnd))
return UUID(u.String())

nano := g.now.Now().UnixNano()
t := nano / 1_000_000
s := (nano - t*1_000_000) >> 8

u[0] = byte(t >> 40)
u[1] = byte(t >> 32)
u[2] = byte(t >> 24)
u[3] = byte(t >> 16)
u[4] = byte(t >> 8)
u[5] = byte(t)
u[6] = 0x70 | (0x0F & byte(s>>8))
u[7] = byte(s)

return must(u, nil)
}

func must(u uuid.UUID, err error) UUID {
return UUID(uuid.Must(u, err).String())
}
46 changes: 24 additions & 22 deletions uuids/uuid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,52 @@ package uuids_test

import (
"testing"
"time"

"github.com/nyaruka/gocommon/dates"
"github.com/nyaruka/gocommon/uuids"

"github.com/stretchr/testify/assert"
)

func TestIsV4(t *testing.T) {
assert.False(t, uuids.IsV4(""))
assert.True(t, uuids.IsV4("182faeb1-eb29-41e5-b288-c1af671ee671"))
assert.False(t, uuids.IsV4("182faeb1-eb29-41e5-b288-c1af671ee67x"))
assert.False(t, uuids.IsV4("182faeb1-eb29-41e5-b288-c1af671ee67"))
assert.False(t, uuids.IsV4("182faeb1-eb29-41e5-b288-c1af671ee6712"))
}

func TestNew(t *testing.T) {
uuid1 := uuids.New()
uuid2 := uuids.New()
func TestNewV4(t *testing.T) {
uuid1 := uuids.NewV4()
uuid2 := uuids.NewV4()

assert.True(t, uuids.IsV4(string(uuid1)))
assert.True(t, uuids.IsV4(string(uuid2)))
assert.NotEqual(t, uuid1, uuid2)
}

func TestNewV7(t *testing.T) {
uuid1 := uuids.NewV7()
uuid2 := uuids.NewV7()

assert.True(t, uuids.IsV7(string(uuid1)))
assert.True(t, uuids.IsV7(string(uuid2)))
assert.NotEqual(t, uuid1, uuid2)
}

func TestSeededGenerator(t *testing.T) {
defer uuids.SetGenerator(uuids.DefaultGenerator)

uuids.SetGenerator(uuids.NewSeededGenerator(123456))
uuids.SetGenerator(uuids.NewSeededGenerator(123456, dates.NewSequentialNowSource(time.Date(2024, 7, 32, 17, 29, 30, 123456, time.UTC))))

uuid1 := uuids.New()
uuid2 := uuids.New()
uuid3 := uuids.New()
uuid1 := uuids.NewV4()
uuid2 := uuids.NewV7()
uuid3 := uuids.NewV4()

assert.True(t, uuids.IsV4(string(uuid1)))
assert.True(t, uuids.IsV4(string(uuid2)))
assert.True(t, uuids.IsV7(string(uuid2)))
assert.True(t, uuids.IsV4(string(uuid3)))

assert.Equal(t, uuids.UUID(`d2f852ec-7b4e-457f-ae7f-f8b243c49ff5`), uuid1)
assert.Equal(t, uuids.UUID(`692926ea-09d6-4942-bd38-d266ec8d3716`), uuid2)
assert.Equal(t, uuids.UUID(`01910efd-5890-71e2-bd38-d266ec8d3716`), uuid2)
assert.Equal(t, uuids.UUID(`8720f157-ca1c-432f-9c0b-2014ddc77094`), uuid3)

uuids.SetGenerator(uuids.NewSeededGenerator(123456))
uuids.SetGenerator(uuids.NewSeededGenerator(123456, dates.NewSequentialNowSource(time.Date(2024, 7, 32, 17, 29, 30, 123456, time.UTC))))

// should get same sequence again for same seed
assert.Equal(t, uuids.UUID(`d2f852ec-7b4e-457f-ae7f-f8b243c49ff5`), uuids.New())
assert.Equal(t, uuids.UUID(`692926ea-09d6-4942-bd38-d266ec8d3716`), uuids.New())
assert.Equal(t, uuids.UUID(`8720f157-ca1c-432f-9c0b-2014ddc77094`), uuids.New())
assert.Equal(t, uuids.UUID(`d2f852ec-7b4e-457f-ae7f-f8b243c49ff5`), uuids.NewV4())
assert.Equal(t, uuids.UUID(`01910efd-5890-71e2-bd38-d266ec8d3716`), uuids.NewV7())
assert.Equal(t, uuids.UUID(`8720f157-ca1c-432f-9c0b-2014ddc77094`), uuids.NewV4())
}
21 changes: 21 additions & 0 deletions uuids/validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package uuids

import "regexp"

var (
V4Regex = regexp.MustCompile(`[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}`)
V7Regex = regexp.MustCompile(`[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}`)

V4OnlyRegex = regexp.MustCompile(`^` + V4Regex.String() + `$`)
V7OnlyRegex = regexp.MustCompile(`^` + V7Regex.String() + `$`)
)

// IsV4 returns whether the given string contains only a valid v4 UUID
func IsV4(s string) bool {
return V4OnlyRegex.MatchString(s)
}

// IsV7 returns whether the given string contains only a valid v7 UUID
func IsV7(s string) bool {
return V7OnlyRegex.MatchString(s)
}
26 changes: 26 additions & 0 deletions uuids/validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package uuids_test

import (
"testing"

"github.com/nyaruka/gocommon/uuids"
"github.com/stretchr/testify/assert"
)

func TestIsV4(t *testing.T) {
assert.False(t, uuids.IsV4(""))
assert.True(t, uuids.IsV4("182faeb1-eb29-41e5-b288-c1af671ee671"))
assert.False(t, uuids.IsV4("182faeb1-eb29-71e5-b288-c1af671ee671"))
assert.False(t, uuids.IsV4("182faeb1-eb29-41e5-b288-c1af671ee67x"))
assert.False(t, uuids.IsV4("182faeb1-eb29-41e5-b288-c1af671ee67"))
assert.False(t, uuids.IsV4("182faeb1-eb29-41e5-b288-c1af671ee6712"))
}

func TestIsV7(t *testing.T) {
assert.False(t, uuids.IsV7(""))
assert.True(t, uuids.IsV7("182faeb1-eb29-71e5-b288-c1af671ee671"))
assert.False(t, uuids.IsV7("182faeb1-eb29-41e5-b288-c1af671ee671"))
assert.False(t, uuids.IsV7("182faeb1-eb29-71e5-b288-c1af671ee67x"))
assert.False(t, uuids.IsV7("182faeb1-eb29-71e5-b288-c1af671ee67"))
assert.False(t, uuids.IsV7("182faeb1-eb29-71e5-b288-c1af671ee6712"))
}

0 comments on commit 70c7263

Please sign in to comment.