diff --git a/uuids/uuid.go b/uuids/uuid.go index adc795e..98f69dd 100644 --- a/uuids/uuid.go +++ b/uuids/uuid.go @@ -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 @@ -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()) } diff --git a/uuids/uuid_test.go b/uuids/uuid_test.go index 9bdb513..7e7e5bd 100644 --- a/uuids/uuid_test.go +++ b/uuids/uuid_test.go @@ -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()) } diff --git a/uuids/validation.go b/uuids/validation.go new file mode 100644 index 0000000..903f3e1 --- /dev/null +++ b/uuids/validation.go @@ -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) +} diff --git a/uuids/validation_test.go b/uuids/validation_test.go new file mode 100644 index 0000000..30c64ab --- /dev/null +++ b/uuids/validation_test.go @@ -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")) +}