Skip to content

Commit

Permalink
feat: publicid package
Browse files Browse the repository at this point in the history
  • Loading branch information
samialdury committed Mar 22, 2024
1 parent 23ab8ea commit 6f17b72
Show file tree
Hide file tree
Showing 2 changed files with 179 additions and 0 deletions.
75 changes: 75 additions & 0 deletions pkg/publicid/publicid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package publicid

import (
"fmt"
"strings"

nanoid "github.com/matoous/go-nanoid/v2"
)

const (
Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" // base58
Length = 12
)

// New generates a unique public ID.
func New() (string, error) {
return nanoid.Generate(Alphabet, Length)
}

// NewWithPrefix generates a unique public ID with a given prefix.
func NewWithPrefix(prefix string) (string, error) {
id, err := New()
if err != nil {
return "", err
}

return fmt.Sprintf("%s_%s", prefix, id), nil
}

// Must is the same as New, but panics on error.
func Must() string {
return nanoid.MustGenerate(Alphabet, Length)
}

// MustWithPrefix is the same as NewWithPrefix, but panics on error.
func MustWithPrefix(prefix string) string {
id := Must()
return fmt.Sprintf("%s_%s", prefix, id)
}

// Validate checks if a given field name’s public ID value is valid according to
// the constraints defined by package publicid.
func Validate(fieldName, id string) error {
if id == "" {
return fmt.Errorf("%s cannot be blank", fieldName)
}

if len(id) != Length {
return fmt.Errorf("%s should be %d characters long", fieldName, Length)
}

if strings.Trim(id, Alphabet) != "" {
return fmt.Errorf("%s has invalid characters", fieldName)
}

return nil
}

// ValidateWithPrefix checks if a given field name’s public ID value with a prefix is valid according to
// the constraints defined by package publicid.
func ValidateWithPrefix(fieldName, id, prefix string) error {
if id == "" {
return fmt.Errorf("%s cannot be blank", fieldName)
}

expectedPrefix := fmt.Sprintf("%s_", prefix)
if !strings.HasPrefix(id, expectedPrefix) {
return fmt.Errorf("%s must start with the prefix %s", fieldName, prefix)
}

// Remove the prefix and the underscore from the ID for further validation.
trimmedID := strings.TrimPrefix(id, expectedPrefix)

return Validate(fieldName, trimmedID)
}
104 changes: 104 additions & 0 deletions pkg/publicid/publicid_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package publicid

import (
"fmt"
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

func TestNew(t *testing.T) {
id, err := New()
assert.NoError(t, err)
assert.Len(t, id, Length, fmt.Sprintf("ID should be %d characters long", Length))
assert.Equal(t, "", strings.Trim(id, Alphabet), "ID should only contain characters from the Alphabet")
}

func TestNewWithPrefix(t *testing.T) {
prefix := "test"
id, err := NewWithPrefix(prefix)
assert.NoError(t, err)
assert.True(t, strings.HasPrefix(id, prefix+"_"), "ID should have the correct prefix")
assert.Len(t, id, len(prefix)+1+Length, fmt.Sprintf("ID should be %d characters long", len(prefix)+1+Length))
assert.Equal(t, "", strings.Trim(id[len(prefix)+1:], Alphabet), "ID should only contain characters from the Alphabet after the prefix")
}

func TestMust(t *testing.T) {
assert.NotPanics(t, func() {
id := Must()
assert.Len(t, id, Length, fmt.Sprintf("ID should be %d characters long", Length))
assert.Equal(t, "", strings.Trim(id, Alphabet), "ID should only contain characters from the Alphabet")
})
}

func TestMustWithPrefix(t *testing.T) {
prefix := "must"
assert.NotPanics(t, func() {
id := MustWithPrefix(prefix)
assert.True(t, strings.HasPrefix(id, prefix+"_"), "ID should have the correct prefix")
assert.Len(t, id, len(prefix)+1+Length, fmt.Sprintf("ID should be %d characters long", len(prefix)+1+Length))
assert.Equal(t, "", strings.Trim(id[len(prefix)+1:], Alphabet), "ID should only contain characters from the Alphabet after the prefix")
})
}

func TestValidate(t *testing.T) {
validId, _ := New()

testCases := []struct {
name string
fieldName string
id string
wantErr bool
errContains string
}{
{"Valid ID", "field", validId, false, ""},
{"Empty ID", "field", "", true, "cannot be blank"},
{"Incorrect Length", "field", "123", true, "should be 12 characters long"},
{"Invalid Characters", "field", "invalidchar!", true, "has invalid characters"},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := Validate(tc.fieldName, tc.id)
if tc.wantErr {
assert.Error(t, err)
assert.Contains(t, err.Error(), tc.errContains)
} else {
assert.NoError(t, err)
}
})
}
}

func TestValidateWithPrefix(t *testing.T) {
prefix := "prefix"
validIdWithPrefix, _ := NewWithPrefix(prefix)

testCases := []struct {
name string
fieldName string
id string
prefix string
wantErr bool
errContains string
}{
{"Valid ID with Prefix", "field", validIdWithPrefix, prefix, false, ""},
{"Empty ID", "field", "", prefix, true, "cannot be blank"},
{"No Prefix", "field", validIdWithPrefix[len(prefix)+1:], prefix, true, "must start with the prefix"},
{"Wrong Prefix", "field", "wrong_" + validIdWithPrefix, prefix, true, "must start with the prefix"},
{"Invalid Characters with Prefix", "field", prefix + "_invalidchar!", prefix, true, "has invalid characters"},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := ValidateWithPrefix(tc.fieldName, tc.id, tc.prefix)
if tc.wantErr {
assert.Error(t, err)
assert.Contains(t, err.Error(), tc.errContains)
} else {
assert.NoError(t, err)
}
})
}
}

0 comments on commit 6f17b72

Please sign in to comment.