-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
166 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
// Package utils contains utility functions for the application | ||
package idgen | ||
|
||
import ( | ||
"io" | ||
"regexp" | ||
"sync" | ||
"time" | ||
|
||
"github.com/oklog/ulid/v2" | ||
"golang.org/x/exp/rand" | ||
) | ||
|
||
var ( | ||
entropy io.Reader | ||
entropyOnce sync.Once | ||
) | ||
|
||
// DefaultEntropy returns a reader that generates ULID entropy. | ||
// The default entropy function utilizes math/rand.Rand, which is not safe for concurrent use by multiple goroutines. | ||
// Therefore, this function employs x/exp/rand, as recommended by the authors of the library. | ||
func DefaultEntropy() io.Reader { | ||
entropyOnce.Do(func() { | ||
seed := uint64(time.Now().UnixNano()) | ||
source := rand.NewSource(seed) | ||
rng := rand.New(source) | ||
|
||
entropy = &ulid.LockedMonotonicReader{ | ||
MonotonicReader: ulid.Monotonic(rng, 0), | ||
} | ||
}) | ||
return entropy | ||
} | ||
|
||
// IsULID checks if the given string is a valid ULID | ||
// ULID pattern: | ||
// | ||
// 01AN4Z07BY 79KA1307SR9X4MV3 | ||
// |----------| |----------------| | ||
// Timestamp Randomness | ||
// | ||
// 10 characters 16 characters | ||
// Crockford's Base32 is used (excludes I, L, O, and U to avoid confusion and abuse) | ||
func isULID(s string) bool { | ||
ulidRegex := `^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$` | ||
matched, _ := regexp.MatchString(ulidRegex, s) | ||
return matched | ||
} | ||
|
||
// ValidID checks if the given id is valid | ||
func ValidID(id string) bool { | ||
_, err := ulid.Parse(id) | ||
|
||
return err == nil && isULID(id) | ||
} | ||
|
||
// GenerateID generates a new universal ID | ||
func GenerateID() string { | ||
entropy := DefaultEntropy() | ||
now := time.Now() | ||
ts := ulid.Timestamp(now) | ||
return ulid.MustNew(ts, entropy).String() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
package idgen | ||
|
||
import ( | ||
"sync" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestValidID(t *testing.T) { | ||
validULID := GenerateID() | ||
|
||
tests := []struct { | ||
id string | ||
expected bool | ||
}{ | ||
{validULID, true}, | ||
{"0", false}, | ||
{"invalidulid", false}, | ||
{"invalidulid", false}, | ||
{"01B4E6BXY0PRJ5G420D25MWQY!", false}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.id, func(t *testing.T) { | ||
if got := ValidID(tt.id); got != tt.expected { | ||
assert.Equal(t, tt.expected, got) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestGenerateUniqueID(t *testing.T) { | ||
Generator := func() string { return GenerateID() } | ||
|
||
t.Run("uniqueness", func(t *testing.T) { | ||
id1 := Generator() | ||
id2 := Generator() | ||
assert.NotEqual(t, id1, id2) | ||
}) | ||
|
||
t.Run("concurrent uniqueness", func(t *testing.T) { | ||
var wg sync.WaitGroup | ||
ids := make(map[string]struct{}) | ||
mu := sync.Mutex{} | ||
|
||
generateAndStoreID := func() { | ||
defer wg.Done() | ||
id := Generator() | ||
mu.Lock() | ||
defer mu.Unlock() | ||
ids[id] = struct{}{} | ||
} | ||
|
||
numIDs := 10000 | ||
|
||
wg.Add(numIDs) | ||
for i := 0; i < numIDs; i++ { | ||
go generateAndStoreID() | ||
} | ||
|
||
wg.Wait() | ||
|
||
assert.Equal(t, numIDs, len(ids)) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters