tind/config.go
2024-02-24 13:16:04 -07:00

177 lines
4.4 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}