Skip to content

Commit

Permalink
feat: add default UUIDv8 generation with New method
Browse files Browse the repository at this point in the history
  • Loading branch information
ashwingopalsamy authored Dec 13, 2024
2 parents e767226 + 6d0892a commit ad348e3
Show file tree
Hide file tree
Showing 2 changed files with 166 additions and 11 deletions.
38 changes: 36 additions & 2 deletions uuidv8.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
package uuidv8

import (
"crypto/rand"
"encoding/binary"
"encoding/json"
"fmt"
"time"
)

// Constants for the variant and version of UUIDs based on the RFC4122 specification.
Expand Down Expand Up @@ -32,7 +35,38 @@ type UUIDv8 struct {
Node []byte // The node component of the UUID (typically 6 bytes).
}

// NewUUIDv8 generates a new UUIDv8 based on the provided timestamp, clock sequence, and node.
// New generates a UUIDv8 with default parameters.
//
// Default behavior:
// - Timestamp: Current time in nanoseconds.
// - ClockSeq: Random 12-bit value.
// - Node: Random 6-byte node identifier.
//
// Returns:
// - A string representation of the generated UUIDv8.
// - An error if any component generation fails.
func New() (string, error) {
// Current timestamp
timestamp := uint64(time.Now().UnixNano())

// Random clock sequence
clockSeq := make([]byte, 2)
if _, err := rand.Read(clockSeq); err != nil {
return "", fmt.Errorf("failed to generate random clock sequence: %w", err)
}
clockSeqValue := binary.BigEndian.Uint16(clockSeq) & 0x0FFF // Mask to 12 bits

// Random node
node := make([]byte, 6)
if _, err := rand.Read(node); err != nil {
return "", fmt.Errorf("failed to generate random node: %w", err)
}

// Generate UUIDv8
return NewWithParams(timestamp, clockSeqValue, node, TimestampBits48)
}

// NewWithParams generates a new UUIDv8 based on the provided timestamp, clock sequence, and node.
//
// Parameters:
// - timestamp: A 32-, 48-, or 60-bit timestamp value (depending on `timestampBits`).
Expand All @@ -43,7 +77,7 @@ type UUIDv8 struct {
// Returns:
// - A string representation of the generated UUIDv8.
// - An error if the input parameters are invalid (e.g., incorrect node length or unsupported timestamp size).
func NewUUIDv8(timestamp uint64, clockSeq uint16, node []byte, timestampBits int) (string, error) {
func NewWithParams(timestamp uint64, clockSeq uint16, node []byte, timestampBits int) (string, error) {
if len(node) != 6 {
return "", fmt.Errorf("node must be 6 bytes, got %d bytes", len(node))
}
Expand Down
139 changes: 130 additions & 9 deletions uuidv8_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@ import (
"github.com/ash3in/uuidv8"
)

func TestNew_DefaultBehavior(t *testing.T) {
t.Run("Generate UUIDv8 with default settings", func(t *testing.T) {
uuid, err := uuidv8.New()
if err != nil {
t.Fatalf("New() failed: %v", err)
}

// Check if the UUID is valid
if !uuidv8.IsValidUUIDv8(uuid) {
t.Errorf("New() generated an invalid UUID: %s", uuid)
}
})
}

func TestNewUUIDv8(t *testing.T) {
node := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}
timestamp := uint64(1633024800000000000) // Fixed timestamp for deterministic tests
Expand All @@ -27,7 +41,7 @@ func TestNewUUIDv8(t *testing.T) {

for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
uuid, err := uuidv8.NewUUIDv8(timestamp, clockSeq, node, test.timestampBits)
uuid, err := uuidv8.NewWithParams(timestamp, clockSeq, node, test.timestampBits)
if (err != nil) != test.expectedErr {
t.Errorf("Expected error: %v, got: %v", test.expectedErr, err)
}
Expand All @@ -49,7 +63,7 @@ func TestNewUUIDv8_NodeValidation(t *testing.T) {

for _, node := range invalidNodes {
t.Run("Invalid node length", func(t *testing.T) {
_, err := uuidv8.NewUUIDv8(1633024800, 0, node, uuidv8.TimestampBits48)
_, err := uuidv8.NewWithParams(1633024800, 0, node, uuidv8.TimestampBits48)
if err == nil {
t.Errorf("Expected error for invalid node: %v", node)
}
Expand Down Expand Up @@ -84,9 +98,9 @@ func TestFromString(t *testing.T) {
timestamp := uint64(1633024800000000000) // Fixed timestamp
clockSeq := uint16(0)

uuid, err := uuidv8.NewUUIDv8(timestamp, clockSeq, node, uuidv8.TimestampBits48)
uuid, err := uuidv8.NewWithParams(timestamp, clockSeq, node, uuidv8.TimestampBits48)
if err != nil {
t.Fatalf("NewUUIDv8 failed: %v", err)
t.Fatalf("NewWithParams failed: %v", err)
}

parsed, err := uuidv8.FromString(uuid)
Expand Down Expand Up @@ -164,7 +178,7 @@ func TestConcurrencySafety(t *testing.T) {

timestamp := uint64(time.Now().UnixNano()) + uint64(index)

uuid, err := uuidv8.NewUUIDv8(timestamp, clockSeq, node, uuidv8.TimestampBits48)
uuid, err := uuidv8.NewWithParams(timestamp, clockSeq, node, uuidv8.TimestampBits48)
if err != nil {
t.Errorf("Failed to generate UUIDv8 in concurrent environment: %v", err)
}
Expand Down Expand Up @@ -194,7 +208,7 @@ func TestEdgeCases(t *testing.T) {
node := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}

t.Run("Minimum timestamp and clock sequence", func(t *testing.T) {
uuid, err := uuidv8.NewUUIDv8(0, 0, node, uuidv8.TimestampBits48)
uuid, err := uuidv8.NewWithParams(0, 0, node, uuidv8.TimestampBits48)
if err != nil || uuid == "" {
t.Error("Failed to generate UUID with minimal timestamp and clock sequence")
}
Expand All @@ -203,7 +217,7 @@ func TestEdgeCases(t *testing.T) {
t.Run("Maximum timestamp and clock sequence", func(t *testing.T) {
maxTimestamp := uint64(1<<48 - 1)
maxClockSeq := uint16(1<<12 - 1)
uuid, err := uuidv8.NewUUIDv8(maxTimestamp, maxClockSeq, node, uuidv8.TimestampBits48)
uuid, err := uuidv8.NewWithParams(maxTimestamp, maxClockSeq, node, uuidv8.TimestampBits48)
if err != nil || uuid == "" {
t.Error("Failed to generate UUID with maximum timestamp and clock sequence")
}
Expand All @@ -220,7 +234,7 @@ func TestMarshalJSON(t *testing.T) {
clockSeq := uint16(0)

// Generate a valid UUIDv8
uuidStr, err := uuidv8.NewUUIDv8(timestamp, clockSeq, node, uuidv8.TimestampBits48)
uuidStr, err := uuidv8.NewWithParams(timestamp, clockSeq, node, uuidv8.TimestampBits48)
if err != nil {
t.Fatalf("Failed to generate UUIDv8: %v", err)
}
Expand Down Expand Up @@ -285,7 +299,7 @@ func TestUnmarshalJSON(t *testing.T) {
clockSeq := uint16(0)

// Generate a valid UUIDv8
uuidStr, err := uuidv8.NewUUIDv8(timestamp, clockSeq, node, uuidv8.TimestampBits48)
uuidStr, err := uuidv8.NewWithParams(timestamp, clockSeq, node, uuidv8.TimestampBits48)
if err != nil {
t.Fatalf("Failed to generate UUIDv8: %v", err)
}
Expand Down Expand Up @@ -335,3 +349,110 @@ func TestUnmarshalInvalidJSON(t *testing.T) {
}
}
}

func TestNew_Uniqueness(t *testing.T) {
const numUUIDs = 1000
uuidSet := make(map[string]struct{})

for i := 0; i < numUUIDs; i++ {
uuid, err := uuidv8.New()
if err != nil {
t.Fatalf("New() failed: %v", err)
}

if _, exists := uuidSet[uuid]; exists {
t.Errorf("Duplicate UUID generated: %s", uuid)
}
uuidSet[uuid] = struct{}{}
}

if len(uuidSet) != numUUIDs {
t.Errorf("Expected %d unique UUIDs, but got %d", numUUIDs, len(uuidSet))
}
}

func TestNew_ConcurrencySafety(t *testing.T) {
const concurrencyLevel = 100
var wg sync.WaitGroup
uuidSet := sync.Map{}

for i := 0; i < concurrencyLevel; i++ {
wg.Add(1)
go func() {
defer wg.Done()
uuid, err := uuidv8.New()
if err != nil {
t.Errorf("New() failed in concurrent environment: %v", err)
}
uuidSet.Store(uuid, true)
}()
}

wg.Wait()

// Verify uniqueness
count := 0
uuidSet.Range(func(_, _ interface{}) bool {
count++
return true
})

if count != concurrencyLevel {
t.Errorf("Expected %d unique UUIDs, but got %d", concurrencyLevel, count)
}
}

func TestNew_IntegrationWithParsing(t *testing.T) {
uuid, err := uuidv8.New()
if err != nil {
t.Fatalf("New() failed: %v", err)
}

parsed, err := uuidv8.FromString(uuid)
if err != nil {
t.Errorf("FromString failed to parse UUID generated by New(): %v", err)
}

if parsed == nil {
t.Error("Parsed UUID is nil")
}
}

func TestNew_EdgeCases(t *testing.T) {
t.Run("Minimal possible timestamp and clock sequence", func(t *testing.T) {
uuid, err := uuidv8.New()
if err != nil {
t.Fatalf("New() failed: %v", err)
}

parsed, _ := uuidv8.FromString(uuid)
if parsed.Timestamp == 0 || parsed.ClockSeq == 0 {
t.Errorf("New() generated UUID with invalid minimal values: %s", uuid)
}
})
}

func TestNew_JSONSerializationIntegration(t *testing.T) {
uuid, err := uuidv8.New()
if err != nil {
t.Fatalf("New() failed: %v", err)
}

// Serialize to JSON
jsonData, err := json.Marshal(uuid)
if err != nil {
t.Errorf("Failed to marshal UUID to JSON: %v", err)
}

// Deserialize from JSON
var parsedUUID uuidv8.UUIDv8
err = json.Unmarshal(jsonData, &parsedUUID)
if err != nil {
t.Errorf("Failed to unmarshal JSON to UUIDv8: %v", err)
}

// Ensure the deserialized UUID matches the original
if uuidv8.ToString(&parsedUUID) != uuid {
t.Errorf("Mismatch between original and deserialized UUID: original %s, deserialized %s", uuid, uuidv8.ToString(&parsedUUID))
}
}

0 comments on commit ad348e3

Please sign in to comment.