Skip to content

Commit ae3f94a

Browse files
committed
feat: Major refactor and renaming of functions
This is major refactor of gonanoid for version 2. More tests were added and also some bugs fixed on the way, mainly related to non-ascii alphabets for IDs. BREAKING CHANGES: Nanoid() and ID() functions were removed, use New() instead. MustID() was removed, use Must() instead.
1 parent 554e6f8 commit ae3f94a

File tree

10 files changed

+172
-278
lines changed

10 files changed

+172
-278
lines changed

.github/workflows/test.yml

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,21 @@ jobs:
55
name: Test
66
runs-on: ubuntu-latest
77
steps:
8-
- name: Set up Go 1.12
9-
uses: actions/setup-go@v1
8+
- uses: actions/setup-go@v1
109
with:
11-
go-version: 1.12.3
12-
id: go
13-
14-
- name: Check out code
15-
uses: actions/checkout@v1
16-
10+
go-version: 1.15.2
11+
- uses: actions/checkout@v1
1712
- name: Test
1813
run: make test
1914

2015
benchmark:
2116
name: Benchmark
2217
runs-on: ubuntu-latest
2318
steps:
24-
- name: Set up Go 1.12
25-
uses: actions/setup-go@v1
19+
- uses: actions/setup-go@v1
2620
with:
27-
go-version: 1.12.3
21+
go-version: 1.15.2
2822
id: go
29-
30-
- name: Check out code
31-
uses: actions/checkout@v1
32-
23+
- uses: actions/checkout@v1
3324
- name: Benchmark
3425
run: make bench

.goreleaser.yml

Lines changed: 0 additions & 28 deletions
This file was deleted.

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
help: ## Show help/documentation for the Makefile
44
@grep -Eh '^[0-9a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
55

6-
configure:
6+
configure: ## Download dependencies
77
go mod download
88

99
lint: configure ## Lint the repository with golang-ci lint

README.md

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,15 @@ $ go get github.com/matoous/go-nanoid
3030
Generate ID
3131

3232
``` go
33-
id, err := gonanoid.Nanoid()
33+
id, err := gonanoid.New()
3434
```
3535

36-
Generate ID with custom alphabet and length
36+
Generate ID with a custom alphabet and length
3737

3838
``` go
3939
id, err := gonanoid.Generate("abcde", 54)
4040
```
4141

42-
## Testing
43-
44-
``` bash
45-
$ go test
46-
```
47-
4842
## Notice
4943

5044
If you use Go Nanoid in your project, please let me know!

examples/simple_example.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,19 @@ package main
33
import (
44
"fmt"
55

6-
"github.com/matoous/go-nanoid"
6+
gonanoid "github.com/matoous/go-nanoid/v2"
77
)
88

99
func main() {
1010
// Simple usage
11-
id, err := gonanoid.Nanoid()
11+
id, err := gonanoid.New()
1212
if err != nil {
1313
panic(err)
1414
}
1515
fmt.Printf("Generated id: %s\n", id)
1616

1717
// Custom length
18-
id, err = gonanoid.ID(5)
18+
id, err = gonanoid.New(5)
1919
if err != nil {
2020
panic(err)
2121
}
@@ -34,4 +34,7 @@ func main() {
3434
panic(err)
3535
}
3636
fmt.Printf("Generated id: %s\n", id)
37+
38+
fmt.Printf("Generated id: %s\n", gonanoid.Must())
39+
fmt.Printf("Generated id: %s\n", gonanoid.MustGenerate("🚀💩🦄🤖", 4))
3740
}

go.mod

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
go 1.15
22

3-
module github.com/matoous/go-nanoid
3+
module github.com/matoous/go-nanoid/v2
4+
5+
require (
6+
github.com/matoous/go-nanoid v1.5.0
7+
github.com/stretchr/testify v1.6.1
8+
)

go.sum

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
2+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/matoous/go-nanoid v1.5.0 h1:VRorl6uCngneC4oUQqOYtO3S0H5QKFtKuKycFG3euek=
4+
github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U=
5+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
6+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
7+
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
8+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
9+
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
10+
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
11+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
12+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
13+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
14+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

gonanoid.go

Lines changed: 39 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -3,74 +3,56 @@ package gonanoid
33
import (
44
"crypto/rand"
55
"errors"
6-
"fmt"
76
"math"
87
)
98

9+
// defaultAlphabet is the alphabet used for ID characters by default.
1010
var defaultAlphabet = []rune("_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
11+
1112
const (
12-
defaultSize = 21
13-
defaultMaskSize = 5
13+
defaultSize = 21
1414
)
1515

16-
// Generator function
17-
type Generator func([]byte) (int, error)
18-
19-
// BytesGenerator is the default bytes generator
20-
var BytesGenerator Generator = rand.Read
21-
22-
func initMasks(params ...int) []uint {
23-
var size int
24-
if len(params) == 0 {
25-
size = defaultMaskSize
26-
} else {
27-
size = params[0]
28-
}
29-
masks := make([]uint, size)
30-
for i := 0; i < size; i++ {
31-
shift := 3 + i
32-
masks[i] = (2 << uint(shift)) - 1
33-
}
34-
return masks
35-
}
36-
37-
func getMask(alphabet []rune, masks []uint) int {
38-
for i := 0; i < len(masks); i++ {
39-
curr := int(masks[i])
40-
if curr >= len(alphabet)-1 {
41-
return curr
16+
// getMask generates bit mask used to obtain bits from the random bytes that are used to get index of random character
17+
// from the alphabet. Example: if the alphabet has 6 = (110)_2 characters it is sufficient to use mask 7 = (111)_2
18+
func getMask(alphabetSize int) int {
19+
for i := 1; i <= 8; i++ {
20+
mask := (2 << uint(i)) - 1
21+
if mask >= alphabetSize-1 {
22+
return mask
4223
}
4324
}
4425
return 0
4526
}
4627

4728
// Generate is a low-level function to change alphabet and ID size.
48-
func Generate(rawAlphabet string, size int) (string, error) {
49-
alphabet := []rune(rawAlphabet)
29+
func Generate(alphabet string, size int) (string, error) {
30+
chars := []rune(alphabet)
5031

5132
if len(alphabet) == 0 || len(alphabet) > 255 {
52-
return "", fmt.Errorf("alphabet must not empty and contain no more than 255 chars. Current len is %d", len(alphabet))
33+
return "", errors.New("alphabet must not be empty and contain no more than 255 chars")
5334
}
5435
if size <= 0 {
55-
return "", fmt.Errorf("size must be positive integer")
36+
return "", errors.New("size must be positive integer")
5637
}
5738

58-
masks := initMasks(size)
59-
mask := getMask(alphabet, masks)
39+
mask := getMask(len(chars))
40+
// estimate how many random bytes we will need for the ID, we might actually need more but this is tradeoff
41+
// between average case and worst case
6042
ceilArg := 1.6 * float64(mask*size) / float64(len(alphabet))
6143
step := int(math.Ceil(ceilArg))
6244

6345
id := make([]rune, size)
6446
bytes := make([]byte, step)
6547
for j := 0; ; {
66-
_, err := BytesGenerator(bytes)
48+
_, err := rand.Read(bytes)
6749
if err != nil {
6850
return "", err
6951
}
7052
for i := 0; i < step; i++ {
7153
currByte := bytes[i] & byte(mask)
72-
if currByte < byte(len(alphabet)) {
73-
id[j] = alphabet[currByte]
54+
if currByte < byte(len(chars)) {
55+
id[j] = chars[currByte]
7456
j++
7557
if j == size {
7658
return string(id[:size]), nil
@@ -80,22 +62,32 @@ func Generate(rawAlphabet string, size int) (string, error) {
8062
}
8163
}
8264

83-
// Nanoid generates secure URL-friendly unique ID.
84-
func Nanoid(param ...int) (string, error) {
65+
// MustGenerate is the same as Generate but panics on error.
66+
func MustGenerate(alphabet string, size int) string {
67+
id, err := Generate(alphabet, size)
68+
if err != nil {
69+
panic(err)
70+
}
71+
return id
72+
}
73+
74+
// New generates secure URL-friendly unique ID.
75+
// Accepts optional parameter - length of the ID to be generated (21 by default).
76+
func New(l ...int) (string, error) {
8577
var size int
8678
switch {
87-
case len(param) == 0:
79+
case len(l) == 0:
8880
size = defaultSize
89-
case len(param) == 1:
90-
size = param[0]
81+
case len(l) == 1:
82+
size = l[0]
9183
if size < 0 {
9284
return "", errors.New("negative id length")
9385
}
9486
default:
9587
return "", errors.New("unexpected parameter")
9688
}
9789
bytes := make([]byte, size)
98-
_, err := BytesGenerator(bytes)
90+
_, err := rand.Read(bytes)
9991
if err != nil {
10092
return "", err
10193
}
@@ -106,24 +98,9 @@ func Nanoid(param ...int) (string, error) {
10698
return string(id[:size]), nil
10799
}
108100

109-
// ID provides more golang idiomatic interface for generating IDs.
110-
// Calling ID is shorter yet still clear `gonanoid.ID(20)` and it requires the lengths parameter by default.
111-
func ID(l int) (string, error) {
112-
return Nanoid(l)
113-
}
114-
115-
// MustID is the same as ID but panics on error.
116-
func MustID(l int) string {
117-
id, err := Nanoid(l)
118-
if err != nil {
119-
panic(err)
120-
}
121-
return id
122-
}
123-
124-
// MustGenerate is the same as Generate but panics on error.
125-
func MustGenerate(rawAlphabet string, size int) string {
126-
id, err := Generate(rawAlphabet, size)
101+
// Must is the same as New but panics on error.
102+
func Must(l ...int) string {
103+
id, err := New(l...)
127104
if err != nil {
128105
panic(err)
129106
}

gonanoid_internal_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package gonanoid
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func TestHasNoCollisions(t *testing.T) {
10+
tries := 100_000
11+
used := make(map[string]bool)
12+
for i := 0; i < tries; i++ {
13+
id := Must()
14+
require.False(t, used[id], "shouldn't return colliding IDs")
15+
used[id] = true
16+
}
17+
}
18+
19+
func TestFlatDistribution(t *testing.T) {
20+
tries := 100_000
21+
alphabet := "abcdefghij"
22+
size := 10
23+
chars := make(map[rune]int)
24+
for i := 0; i < tries; i++ {
25+
id := MustGenerate(alphabet, size)
26+
for _, r := range id {
27+
chars[r]++
28+
}
29+
}
30+
31+
for _, count := range chars {
32+
require.InEpsilon(t, size*tries/len(alphabet), count, .01, "should have flat distribution")
33+
}
34+
}
35+
36+
// Benchmark nanoid generator
37+
func BenchmarkNanoid(b *testing.B) {
38+
for n := 0; n < b.N; n++ {
39+
_, _ = New()
40+
}
41+
}

0 commit comments

Comments
 (0)