commit 0b7579712a60d124264891ac9d3edf10d0af5598
Author: Yongbin Kim <iam@yongbin.kim>
Date: Fri, 25 Aug 2023 08:20:44 +0900
First Commit
Signed-off-by: Yongbin Kim <iam@yongbin.kim>
Diffstat:
9 files changed, 288 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,25 @@
+### Go.AllowList template
+# Allowlisting gitignore template for GO projects prevents us
+# from adding various unwanted local files, such as generated
+# files, developer configurations or IDE-specific files etc.
+#
+# Recommended: Go.AllowList.gitignore
+
+# Ignore everything
+*
+
+# But not these files...
+!/.gitignore
+
+!*.go
+!go.sum
+!go.mod
+
+!README
+!LICENSE
+
+# !Makefile
+
+# ...even if they are in subdirectories
+!*/
+
diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2023 Yongbin Kim
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+\ No newline at end of file
diff --git a/README b/README
@@ -0,0 +1,12 @@
+A small, unique, URL-safe ID generator written in Go.
+
+## Usage
+
+```go
+import "go.lair.cx/yuid"
+
+generator := yuid.NewGenerator()
+
+var id yuid.ID
+generator.Next(&id)
+```
diff --git a/generator.go b/generator.go
@@ -0,0 +1,113 @@
+package yuid
+
+import (
+ "io"
+ "sync"
+ "time"
+)
+
+const (
+ Characters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_" // Exactly 64 characters
+)
+
+type Generator struct {
+ sync.Mutex
+ Source io.Reader
+ Now func() int64
+ sequence uint8
+ timestamp int64
+}
+
+type GeneratorOption func(*Generator)
+
+func NewGenerator(opts ...GeneratorOption) *Generator {
+ g := Generator{
+ Source: InsecureSource{},
+ Now: Now,
+ }
+
+ for _, opt := range opts {
+ opt(&g)
+ }
+
+ return &g
+}
+
+func WithSource(source io.Reader) GeneratorOption {
+ return func(g *Generator) {
+ g.Source = source
+ }
+}
+
+func WithNowFunc(now func() int64) GeneratorOption {
+ return func(g *Generator) {
+ g.Now = now
+ }
+}
+
+func (g *Generator) Next(id *ID) error {
+ var ts int64
+ var seq uint8
+
+ now := g.Now()
+
+ g.Lock()
+
+ if now > g.timestamp {
+ g.timestamp = now
+ g.sequence = 0
+ } else if g.sequence == 255 {
+ g.timestamp++
+ g.sequence = 0
+ } else {
+ g.sequence++
+ }
+
+ ts = g.timestamp
+ seq = g.sequence
+
+ _, err := io.ReadFull(g.Source, id[12:])
+
+ g.Unlock()
+
+ if err != nil {
+ return err
+ }
+
+ putHeader(id, ts, seq)
+ setBody(id)
+
+ return nil
+}
+
+func putHeader(id *ID, ts int64, seq uint8) {
+ id[0] = Characters[(ts>>58)&63]
+ id[1] = Characters[(ts>>52)&63]
+ id[2] = Characters[(ts>>46)&63]
+ id[3] = Characters[(ts>>40)&63]
+ id[4] = Characters[(ts>>34)&63]
+ id[5] = Characters[(ts>>28)&63]
+ id[6] = Characters[(ts>>22)&63]
+ id[7] = Characters[(ts>>16)&63]
+ id[8] = Characters[(ts>>10)&63]
+ id[9] = Characters[(ts>>4)&63]
+ id[10] = Characters[uint8((ts&15)<<2)|(seq>>6)]
+ id[11] = Characters[seq&63]
+}
+
+func setBody(id *ID) {
+ for i := 12; i < IDSize; i++ {
+ id[i] = Characters[id[i]&63]
+ }
+}
+
+func (g *Generator) MustNext(id *ID) {
+ err := g.Next(id)
+ if err != nil {
+ panic(err)
+ }
+}
+
+func Now() int64 {
+ return time.Now().UnixMilli()
+}
diff --git a/generator_test.go b/generator_test.go
@@ -0,0 +1,68 @@
+package yuid
+
+import (
+ "testing"
+)
+
+func TestGenerator_Next(t *testing.T) {
+ g := NewGenerator()
+ m := make(map[ID]struct{})
+ id := ID{}
+
+ for i := 0; i < 1000000; i++ {
+ err := g.Next(&id)
+ if err != nil {
+ t.Fatalf("Failed to generate ID: %v", err)
+ }
+
+ if _, ok := m[id]; ok {
+ t.Fatalf("Duplicate ID found: %v", id)
+ }
+
+ m[id] = struct{}{}
+ }
+}
+
+func TestGenerator_MustNext(t *testing.T) {
+ defer func() {
+ if r := recover(); r != nil {
+ t.Errorf("Generator panicked: %v", r)
+ }
+ }()
+
+ g := NewGenerator()
+ id := ID{}
+
+ // Just calling it many times to see if any panic occurs
+ for i := 0; i < 1000000; i++ {
+ g.MustNext(&id)
+ }
+}
+
+func BenchmarkGenerator_Next(b *testing.B) {
+ g := NewGenerator()
+ id := ID{}
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _ = g.Next(&id)
+ }
+}
+
+func BenchmarkPutHeader(b *testing.B) {
+ id := ID{}
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ putHeader(&id, 1234567890, 123)
+ }
+}
+
+func BenchmarkSetBody(b *testing.B) {
+ id := ID{}
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ setBody(&id)
+ }
+}
diff --git a/go.mod b/go.mod
@@ -0,0 +1,3 @@
+module go.lair.cx/yuid
+
+go 1.21.0
diff --git a/id.go b/id.go
@@ -0,0 +1,11 @@
+package yuid
+
+const IDSize = 20
+
+type ID [IDSize]byte
+
+//goland:noinspection GoUnusedGlobalVariable
+var (
+ // Nil is a nil ID.
+ Nil ID
+)
diff --git a/mathrand.go b/mathrand.go
@@ -0,0 +1,24 @@
+package yuid
+
+import "math/rand"
+
+type InsecureSource struct{}
+
+func (InsecureSource) Read(p []byte) (n int, err error) {
+ var (
+ val uint64
+ pos int8
+ )
+
+ for n = 0; n < len(p); n++ {
+ if pos == 0 {
+ val = rand.Uint64()
+ pos = 8
+ }
+ p[n] = byte(val)
+ val >>= 8
+ pos--
+ }
+
+ return
+}
diff --git a/mathrand_test.go b/mathrand_test.go
@@ -0,0 +1,12 @@
+package yuid
+
+import "testing"
+
+func BenchmarkMathRand_Read(b *testing.B) {
+ r := InsecureSource{}
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _, _ = r.Read(make([]byte, 256))
+ }
+}