package id import ( "crypto/rand" "encoding/binary" "errors" "fmt" "time" uuidv8 "github.com/ash3in/uuidv8" "github.com/google/uuid" nanoid "github.com/matoous/go-nanoid" ) var generatedNode [6]byte func init() { if _, err := rand.Read(generatedNode[:]); err != nil { panic(err) } } type Type int const ( TypeUnspecified Type = iota TypeNanoid TypeUUIDv8 ) // DefaultNanoidAlphabet is the alphabet used for ID characters by default var DefaultNanoidAlphabet = "6789BCDFGHJKLMNPQRTWbcdfghjkmnpqrtwz" // DefaultNanoidSize is the size used for ID by default // To get uuid like collision specify 21 var DefaultNanoidSize = 16 type Generator struct { opts Options } func (g *Generator) MustNew() string { id, err := g.New() if err != nil { panic(err) } return id } func (g *Generator) New() (string, error) { switch g.opts.Type { case TypeNanoid: if len(g.opts.NanoidAlphabet) == 0 || len(g.opts.NanoidAlphabet) > 255 { return "", errors.New("invalid option, NanoidAlphabet must not be empty and contain no more than 255 chars") } if g.opts.NanoidSize <= 0 { return "", errors.New("invalid option, NanoidSize must be positive integer") } return nanoid.Generate(g.opts.NanoidAlphabet, g.opts.NanoidSize) case TypeUUIDv8: timestamp := uint64(time.Now().UnixNano()) 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 return uuidv8.NewWithParams(timestamp, clockSeqValue, g.opts.UUIDNode[:], uuidv8.TimestampBits48) } return "", errors.New("invalid option, Type unspecified") } // New returns new id or error func New(opts ...Option) (string, error) { options := NewOptions(opts...) switch options.Type { case TypeNanoid: if len(options.NanoidAlphabet) == 0 || len(options.NanoidAlphabet) > 255 { return "", errors.New("invalid option, NanoidAlphabet must not be empty and contain no more than 255 chars") } if options.NanoidSize <= 0 { return "", errors.New("invalid option, NanoidSize must be positive integer") } return nanoid.Generate(options.NanoidAlphabet, options.NanoidSize) case TypeUUIDv8: timestamp := uint64(time.Now().UnixNano()) 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 return uuidv8.NewWithParams(timestamp, clockSeqValue, options.UUIDNode[:], uuidv8.TimestampBits48) } return "", errors.New("invalid option, Type unspecified") } func ToUUID(s string) uuid.UUID { return uuid.MustParse(s) } // Must is the same as New but fatals on error func MustNew(opts ...Option) string { id, err := New(opts...) if err != nil { panic(err) } return id } // Options contains id deneration options type Options struct { Type Type NanoidAlphabet string NanoidSize int UUIDNode [6]byte } // Option func signature type Option func(*Options) // WithNanoidAlphabet specifies alphabet to use func WithNanoidAlphabet(alphabet string) Option { return func(o *Options) { o.NanoidAlphabet = alphabet } } // WithNanoidSize specifies generated id size func WithNanoidSize(size int) Option { return func(o *Options) { o.NanoidSize = size } } // WithUUIDNode specifies node component for UUIDv8 func WithUUIDNode(node [6]byte) Option { return func(o *Options) { o.UUIDNode = node } } // NewOptions returns new Options struct filled by opts func NewOptions(opts ...Option) Options { options := Options{ Type: TypeUUIDv8, NanoidAlphabet: DefaultNanoidAlphabet, NanoidSize: DefaultNanoidSize, UUIDNode: generatedNode, } for _, o := range opts { o(&options) } return options }