Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add default UUIDv8 generation with New method #7

Merged
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
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 @@
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)
}

Check warning on line 56 in uuidv8.go

View check run for this annotation

Codecov / codecov/patch

uuidv8.go#L55-L56

Added lines #L55 - L56 were not covered by tests
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)
}

Check warning on line 63 in uuidv8.go

View check run for this annotation

Codecov / codecov/patch

uuidv8.go#L62-L63

Added lines #L62 - L63 were not covered by tests

// 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 @@
// 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))
}
}
Loading