package tind import ( "crypto/rand" "errors" "log" ) // Config is a configuration for generating and working with TinD id's of // possibly varying sizes and runesets type Config struct { size int runes []rune runesize int mod byte } const ( defaultRunes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" defaultSize = 4 ) // New starts off a factory pattern to build a TinD // with either the default values, or setting custom values // using the `With` functions // Example: // // Get a default TinD id // `id := New().Gen()` // // Get a TinD with 8 bytes but the default runeset // `id := New().WithSize(8).Gen()` // // Get a TinD of default size with a special runeset // `id := New().WithRuneset("😃😡🕰️🧰BGD").Gen()` // // You can always define the configuration and create new TinD's // using it. // Example: // // myNumberOnlyConfig := New().WithSize(8).WithRuneset("0123456789") func New() *Config { c := &Config{ size: defaultSize, runes: []rune(defaultRunes), runesize: len(defaultRunes), } c.calcMod() return c } // NewFrom will build a configuration from a byte slice. Because this is a // byte slice, the provided input doesn't matter, it's more about the length, func NewConfigFrom(in []byte) *Config { func NewFrom(in []byte) *Config { c := New() c.size = len(in) c.calcMod() return c } // WithRuneset requires a string as input, where each character must be unique. // The string can be comprised of any unique unicode character. // Will filter out duplicate runes before storing it, meaning if you provide a // runeset with duplicates, the size of the runeset will be reduced to ensure // consistency. Dont' rely on len(runeset) of your custom runeset to be // accurate. // // This does slow down initialization, but the final product still runs fast to // generate ids // When updating a runeset, the values of runesize and mod will be updated func (c *Config) WithRuneset(r []rune) *Config { c.runes = ensureUniqueRunes(r) c.runesize = len(c.runes) c.calcMod() return c } func (c *Config) calcMod() { c.mod = byte(c.runesize - 1) } // ensureUniqueRunes loops through the provided runeset to make sure only unique // runes are included. This ensures that, given the same runeset in the same // order, a TinD can be decoded from a string. func ensureUniqueRunes(r []rune) []rune { unq := make(map[rune]int) var index int // loop through the runes to find duplicates for _, v := range r { // if the current rune hasn't been seen yet, assign it if _, ok := unq[v]; !ok { unq[v] = index index++ } } rr := make([]rune, len(unq)) for k, n := range unq { rr[n] = k } return rr } // WithSize sets the number of bytes in the TinD ID func (c *Config) WithSize(s int) *Config { c.size = s return c } // Zero returns a zero value TinD ID of the configured size func (c *Config) Zero() TinD { t := TinD{ bytes: make([]byte, c.size), config: c, } return t } // Gen generates a TinD with the given configuration func (c *Config) Gen() TinD { bytes := make([]byte, c.size) runes := make([]rune, c.size) _, err := rand.Read(bytes) if err != nil { log.Fatalln(err) } // Make sure each byte fits the rune so it can be encoded and decoded for i := 0; i < c.size; i++ { bytes[i] = bytes[i] & c.mod runes[i] = c.runes[bytes[i]] } t := TinD{ bytes: bytes, runes: runes, config: c, } return t } // FromString takes a string representation of a TinD id and returns // the actual TinD as bytes func (c *Config) FromString(in string) (TinD, error) { // Make sure the number of characters matches what's needed if len(in) != c.size { return c.Zero(), errors.New("given id isn't the same size as the configuration") } t := TinD{ bytes: make([]byte, c.size), config: c, } r := []rune(in) for i := 0; i < c.size; i++ { for j := 0; j < c.runesize; j++ { if r[i] == c.runes[j] { t.bytes[i] = byte(j) break } } } return TinD(t), nil } // Load imports a TinD byte string, or really any byte string, that fits within // the constraints of the configuration. You can't load an 8byte slice into a // 4byte TinD configuration. // To generate a configuration from a given byte slice, use NewConfigFrom. func (c *Config) Load(in []byte) (TinD, error) { if len(in) != c.size { return c.Zero(), errors.New("given id isn't the same size as the configuration") } t := TinD{ bytes: in, config: c, } return t, nil }