commit d00ac81a29daee9eb4896dec4f7ddedcd82ef732 Author: Matthew Stobbs Date: Thu Feb 1 17:44:01 2024 -0700 Initial commit Not tested, only written code. Tests will come next to verify that it works diff --git a/config.go b/config.go new file mode 100644 index 0000000..c12ef04 --- /dev/null +++ b/config.go @@ -0,0 +1,133 @@ +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 string) *Config { + c.runes = ensureUniqueRunes(r) + c.runesize = len(c.runes) + c.mod = byte(c.runesize - 1) + return c +} + +func ensureUniqueRunes(r string) []rune { + unq := make(map[rune]int) + var index int + // loop through the runes to find duplicates + for _, v := range []rune(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, + } + t.String() // set the string so it's faster to recall + return t +} + +// Gen generates a TinD with the given configuration +func (c *Config) Gen() TinD { + t := TinD{ + bytes: make([]byte, c.size), + config: c, + } + _, err := rand.Read(t.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++ { + t.bytes[i] = t.bytes[i] & c.mod + } + 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 := make([]byte, c.size) + r := []rune(in) + for i := 0; i < l; i++ { + for j := 0; j < c.runesize; j++ { + if r[i] == alphabet[j] { + t[i] = byte(j) + break + } + } + } + return TinD(t), nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4238dcb --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module stobbsm/tind + +go 1.21.6 diff --git a/tind.go b/tind.go new file mode 100644 index 0000000..012913c --- /dev/null +++ b/tind.go @@ -0,0 +1,58 @@ +package tind + +// TinD: Because a UUID is too big for a lot of people +// a TinD is a defaults to a 4 byte ID made up of ONLY Alphabetical characters. +// To ensure more randomness, lower case and upper case letters are used +// providing up to 52 differenct characters for use in each single position. +// Arbitrary TinD sizes can also be used, by calling +// `id := NewTinDOfLength()` +// +// TinD is at it's core a byte slice. It's default alphabet can be replaced with +// whatever unicode characters you want, as the alphabet it uses is just a slice +// of runes +import ( + "crypto/rand" + "errors" + "log" +) + +// defaults are just a call to NewConfig() +var defaults = NewTinDConfig() + +// TinD represents a very simple `size` byte unique ID that is generated +// at random. TinD stands for Tiny iDentifier, and is pronouced "tind" +// Each byte can be the value 0 -> len(alphabet) that is used, to ensure +// values can be encoded and decoded reliably. The longer the alphabet +// used, the more randomness can be done. +type TinD struct{ + bytes []byte + str string + config *Config +} + +// Gen generates a new TinD id using default values +func Gen() TinD { + return defaults.Gen() +} + +// String returns the Human Readable form of the TinD +func (t TinD) String() string { + if len(t.str) == t.config.size { + return t.str + } + t.str = makeString(t.bytes, t.config.size, t.config.runset) + return t.str +} + +// Bytes returns the raw bytes of the TinD +func (t TinD) Bytes() []byte { + return t.bytes +} + +func makeString(bytes []byte, size int, runeset []rune) string { + r := make([]rune, size) + for i := 0; i < size; i++ { + r[i] = runeset[bytes[i]] + } + return string(r) +}