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 ) // NewTinDConfig 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().Get()` // // Get a TinD with 8 bytes but the default runeset // `id := New().WithSize(8).Get()` // // Get a TinD of default size with a special runeset // `id := New().WithRuneset("😃😡🕰️🧰BGD").Get()` // // You can always define the configuration and create new TinD's // using it. // Example: // // myNumberOnlyConfig := New().WithSize(8).WithRuneset("0123456789") func NewTinDConfig() *Config { return &Config{ size: defaultSize, runes: []rune(defaultRunes), runesize: len(defaultRunes), mod: byte(len(defaultRunes) - 1), } } // 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 // // This does slow down initialization, but the final product still runs fast to // generate ids func (c *Config) WithRuneset(r []rune) *Config { c.runes = ensureUniqueRunes(r) c.runesize = len(c.runes) c.mod = byte(c.runesize - 1) return c } 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 } 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 }