diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dd33554 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.obsidian diff --git a/config.go b/config.go index b1ebf70..cfd0838 100644 --- a/config.go +++ b/config.go @@ -26,13 +26,13 @@ const ( // Example: // // Get a default TinD id -// `id := New().Get()` +// `id := New().Gen()` // // Get a TinD with 8 bytes but the default runeset -// `id := New().WithSize(8).Get()` +// `id := New().WithSize(8).Gen()` // // Get a TinD of default size with a special runeset -// `id := New().WithRuneset("😃😡🕰️🧰BGD").Get()` +// `id := New().WithRuneset("😃😡🕰️🧰BGD").Gen()` // // You can always define the configuration and create new TinD's // using it. @@ -48,12 +48,28 @@ func NewTinDConfig() *Config { } } +// NewConfigFrom 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, +// not how you use it 😉 +func NewConfigFrom(in []byte) *Config { + return &Config{ + size: len(in), + 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 +// 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) @@ -138,6 +154,10 @@ func (c *Config) FromString(in string) (TinD, error) { 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") diff --git a/docs/TinD - Tiny iDentifier, an alternative to UUID.md b/docs/TinD - Tiny iDentifier, an alternative to UUID.md new file mode 100644 index 0000000..30c9da5 --- /dev/null +++ b/docs/TinD - Tiny iDentifier, an alternative to UUID.md @@ -0,0 +1,10 @@ +A UUID is amazing. It's almost garunteed that it will always be unique, and makes sense in a lot of use cases, but doesn't lead to a very human readable identifier. +It used to be the case that using an auto incrementing integer based ID was the best way to go. It was simple, easy to understand, and hard to screw up. +But auto incrementing ID's have a lot of drawbacks when you want something to be unpredictable in order to prevent data leaks. Not only that, but when using sharding, partitioning, or even different databases in the same application, auto incrementing may just plain break your application. + +In came UUID's to fix it, and fix it they did! A UUID is at it's core a 128bit long random identifier. This solves the issues of database partitioning and using different databases in general, but leads to a hard to read ID, not really suitable for things that shouldn't just be machine readable. +As an example, this is a UUID: `402b5a52-3751-4daa-bf33-f89b3822d595`. It's stored as a series of 16bytes, represented using hexidecimal characters `0123456789abcdef`, which hyphens to make it more *readable*. + +TinD seeks to solves these issues, by having a customizable length of bytes, that then get's mapped to a list of `runes` or `characters`. An example of a default TinD generated ID is `CBzF`. Much easier to read, much easier to understand. + +TinD doesn't seek to be the most unique identifier in the world. It will not compete at all with UUID's for uniqueness globally, but for a small application, like say invoicing for a small to medium sized company, it provides a balance of uniqueness to readability, with a completely customizable set of characters to use. \ No newline at end of file diff --git a/docs/TinD default values.md b/docs/TinD default values.md new file mode 100644 index 0000000..7105958 --- /dev/null +++ b/docs/TinD default values.md @@ -0,0 +1,3 @@ +TinD is highly configurable, and defaults to an extremely simple set of values: +- Default size of 4 bytes, or 32bits +- Default character set of upper and lower case alphabet characters `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ` \ No newline at end of file diff --git a/tind.go b/tind.go index d0fd5be..5543020 100644 --- a/tind.go +++ b/tind.go @@ -4,8 +4,6 @@ package tind // 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 diff --git a/tind_test.go b/tind_test.go index e7dc078..0b8b91e 100644 --- a/tind_test.go +++ b/tind_test.go @@ -44,16 +44,7 @@ func Test_GenTinD_With_VaryingSizes(t *testing.T) { } } -//func Test_Generate_With_Collisions(t *testing.T) { -// log.Println("Test TinD for collisions, base on number of iterations and time") -// -// maxSize := 16 -// for size := 4; size < maxSize; size++ { -// CheckCollisions(size) -// } -//} - -func CheckCollisions(tindSize int) { +func checkCollisions(tindSize int) { ids := make(map[string]struct{}) collided := false tc := NewTinDConfig().WithSize(tindSize) @@ -76,3 +67,38 @@ func CheckCollisions(tindSize int) { } log.Printf("Collision found after %v and %d iterations with size of %d", time.Since(start), iters, tindSize) } + +func Test_GenerateConfigFromByteSlice(t *testing.T) { + tests := []struct { + name string + bytes []byte + expectedSize int + expectedMod byte + runeset []rune + }{ + { + name: "Default runeset with size of 10", + bytes: []byte("0123456789"), + expectedSize: 10, + expectedMod: 51, + runeset: []rune(defaultRunes), + }, + { + name: "Custom runeset of 8 runes with size of 14", + bytes: []byte("01234567890123"), + expectedSize: 14, + expectedMod: 7, + runeset: []rune("aBcDeFgH"), + }, + } + for _, v := range tests { + t.Log(v.name) + tt := NewConfigFrom(v.bytes).WithRuneset(v.runeset) + if tt.mod != v.expectedMod { + t.Errorf("expected mod %d, but got %d", v.expectedMod, tt.mod) + } + if tt.size != v.expectedSize { + t.Errorf("expected size of %d, got %d", v.expectedSize, tt.size) + } + } +}