From cf2b1cb0911c550ede75277ff88ed6865a8cd8b9 Mon Sep 17 00:00:00 2001 From: jackspirou Date: Sat, 14 Sep 2024 22:34:42 -0500 Subject: [PATCH] init open source commit --- .github/workflows/ci.yaml | 46 +++++ .gitignore | 33 ++++ Makefile | 78 +++++++++ README.md | 183 ++++++++++++++++++++ devbox.json | 14 ++ devbox.lock | 345 +++++++++++++++++++++++++++++++++++++ generate.go | 3 + go.mod | 5 + go.sum | 10 ++ publicid.go | 91 ++++++++++ publicid_benchmark_test.go | 85 +++++++++ publicid_test.go | 106 ++++++++++++ 12 files changed, 999 insertions(+) create mode 100644 .github/workflows/ci.yaml create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 devbox.json create mode 100644 devbox.lock create mode 100644 generate.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 publicid.go create mode 100644 publicid_benchmark_test.go create mode 100644 publicid_test.go diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..169ebde --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,46 @@ +name: Test, Vet, and Coverage + +on: + push: + branches: [ "master" ] + tags: [ "v*" ] + pull_request: + branches: [ "master" ] + +env: + GITHUB_ORG: ${{ github.repository_owner }} + +jobs: + lint-vet-build-test: + runs-on: ubuntu-latest + strategy: + matrix: + go-version: ['1.22'] + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + + - name: Run go mod download + run: go mod download + + - name: Lint Go Code + uses: golangci/golangci-lint-action@v3 + with: + version: latest + + - name: Run go vet + run: make vet + + - name: Run Tests and Generate Coverage + run: go test -race -coverprofile=coverage.txt -covermode=atomic ./... + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..99c059a --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +# darwin system files +.DS_Store + +# devbox files +.devbox/ + +# End of https://www.toptal.com/developers/gitignore/api/go \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9ba8044 --- /dev/null +++ b/Makefile @@ -0,0 +1,78 @@ +.PHONY: all +all: generate + +##@ General + +# The help target prints out all targets with their descriptions organized +# beneath their categories. The categories are represented by '##@' and the +# target descriptions by '##'. The awk command is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI control characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php + +.PHONY: help +help: ## Display the list of targets and their descriptions + @awk 'BEGIN {FS = ":.*##"; printf "\n\033[1mUsage:\033[0m\n make \033[36m\033[0m\n"} \ + /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 } \ + /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } \ + /^###/ { printf " \033[90m%s\033[0m\n", substr($$0, 4) }' $(MAKEFILE_LIST) + +##@ Tooling + +.PHONY: install-devbox +install-devbox: ## Install Devbox + @echo "Installing Devbox..." + @curl -fsSL https://get.jetify.dev | bash + +.PHONY: devbox-update +devbox-update: ## Update Devbox + @devbox update + +.PHONY: devbox +devbox: ## Run Devbox shell + @devbox shell + +##@ Installation + +.PHONY: install +install: ## Download go modules + @echo "Downloading go modules..." + go mod download + +##@ Development + +.PHONY: fmt +fmt: ## Run go fmt + @echo "Running go fmt..." + go fmt ./... + +.PHONY: generate +generate: ## Generate and embed go documentation into README.md + @echo "Generating and embedding go documentation into README.md..." + go generate ./... + +.PHONY: vet +vet: ## Run go vet + @echo "Running go vet..." + go vet ./... + +.PHONY: lint +lint: ## Run golangci-lint + @echo "Running golangci-lint..." + golangci-lint run ./... + +##@ Testing & Benchmarking + +.PHONY: test +test: ## Run Go tests + @echo "Running go tests..." + go test ./... -tags=test + +.PHONY: bench +bench: ## Run Go benchmarks + @echo "Running go benchmarks..." + go test ./... -tags=bench -bench=. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..88967e2 --- /dev/null +++ b/README.md @@ -0,0 +1,183 @@ +```sh + ____ _ _ _ ___ ____ + | _ \ _ _| |__ | (_) ___|_ _| _ \ + | |_) | | | | '_ \| | |/ __|| || | | | + | __/| |_| | |_) | | | (__ | || |_| | + |_| \__,_|_.__/|_|_|\___|___|____/ +``` + +[![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](https://pkg.go.dev/github.com/agentstation/publicid) +[![Go Report Card](https://goreportcard.com/badge/github.com/agentstation/publicid?style=flat-square)](https://goreportcard.com/report/github.com/agentstation/publicid) +[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/agentstation/publicid/ci.yaml?style=flat-square)](https://github.com/agentstation/publicid/actions) +[![codecov](https://codecov.io/gh/agentstation/publicid/branch/master/graph/badge.svg?token=35UM5QX1Q3)](https://codecov.io/gh/agentstation/publicid) +[![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/agentstation/publicid/master/LICENSE) + + + +The `publicid` package generates and validates NanoID strings designed to be publicly exposed. + +## Installation + +```sh +go get github.com/agentstation/publicid +``` + +## Usage + +To use the `publicid` package, you can import it into your Go project and call the `New` or `NewLong` functions to generate a public ID. + +```go +import ( + "github.com/agentstation/publicid" +) + +id, err := publicid.New() +if err != nil { + log.Fatalf("Failed to generate public ID: %v", err) +} + +fmt.Println("Generated public ID:", id) +``` + +The `New` function generates a public ID with a length of 8 characters, while the `NewLong` function generates a public ID with a length of 12 characters. + +You can also use the `Attempts` option to specify the number of attempts to generate a unique public ID. + +```go +id, err := publicid.New(publicid.Attempts(5)) +if err != nil { + log.Fatalf("Failed to generate public ID: %v", err) +} + +fmt.Println("Generated public ID:", id) +``` + + + + + +# publicid + +```go +import "github.com/agentstation/publicid" +``` + +## Index + +- [func New\(opts ...Option\) \(string, error\)](<#New>) +- [func NewLong\(opts ...Option\) \(string, error\)](<#NewLong>) +- [func Validate\(id string\) error](<#Validate>) +- [func ValidateLong\(fieldName, id string\) error](<#ValidateLong>) +- [type Option](<#Option>) + - [func Attempts\(n int\) Option](<#Attempts>) + + + +## func [New]() + +```go +func New(opts ...Option) (string, error) +``` + +New generates a unique nanoID with a length of 8 characters and the given options. + + +## func [NewLong]() + +```go +func NewLong(opts ...Option) (string, error) +``` + +NewLong generates a unique nanoID with a length of 12 characters and the given options. + + +## func [Validate]() + +```go +func Validate(id string) error +``` + +Validate checks if a given field name's public ID value is valid according to the constraints defined by package publicid. + + +## func [ValidateLong]() + +```go +func ValidateLong(fieldName, id string) error +``` + +validateLong checks if a given field name's public ID value is valid according to the constraints defined by package publicid. + + +## type [Option]() + +Option is a function type for configuring ID generation. + +```go +type Option func(*config) +``` + + +### func [Attempts]() + +```go +func Attempts(n int) Option +``` + +Attempts returns an Option to set the number of attempts for ID generation. + +Generated by [gomarkdoc]() + + + + +## Makefile + +```sh +make help + +Usage: + make + +General + help Display the list of targets and their descriptions + +Tooling + install-devbox Install Devbox + devbox-update Update Devbox + devbox Run Devbox shell + +Installation + install Download go modules + +Development + fmt Run go fmt + generate Generate and embed go documentation into README.md + vet Run go vet + lint Run golangci-lint + +Testing & Benchmarking + test Run Go tests + bench Run Go benchmarks + ``` + +## Benchmarks + +> **Note:** These benchmarks were run on an Apple M2 Max CPU with 12 cores (8 performance and 4 efficiency) and 32 GB of memory, running macOS 14.6.1. + +*Your mileage may vary.* + +```sh +go test -bench=. +goos: darwin +goarch: arm64 +pkg: github.com/agentstation/publicid +BenchmarkNew-12 2012978 574.8 ns/op +BenchmarkNewWithAttempts-12 2091734 577.3 ns/op +BenchmarkLong-12 1966120 616.9 ns/op +BenchmarkLongWithAttempts-12 1952052 610.4 ns/op +BenchmarkValidate-12 100000000 10.73 ns/op +BenchmarkValidateLong-12 99347000 13.31 ns/op +PASS +ok github.com/agentstation/publicid 9.790s +``` diff --git a/devbox.json b/devbox.json new file mode 100644 index 0000000..e9eb8e5 --- /dev/null +++ b/devbox.json @@ -0,0 +1,14 @@ +{ + "packages": [ + "git@2.44.1", + "curl@8.6.0", + "go@1.22.5", + "golangci-lint@1.60.3", + "gomarkdoc@1.1.0" + ], + "shell": { + "init_hook": [ + "PS1=\"$(echo -e \"\\033[1;34m%~\\033[0m \\n\\033[0;32m%n@devbox\\033[0m ➜ \")\"" + ] + } +} \ No newline at end of file diff --git a/devbox.lock b/devbox.lock new file mode 100644 index 0000000..914e28e --- /dev/null +++ b/devbox.lock @@ -0,0 +1,345 @@ +{ + "lockfile_version": "1", + "packages": { + "curl@8.6.0": { + "last_modified": "2024-04-18T12:17:44Z", + "resolved": "github:NixOS/nixpkgs/d764f230634fa4f86dc8d01c6af9619c7cc5d225#curl", + "source": "devbox-search", + "version": "8.6.0", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "bin", + "path": "/nix/store/l4a7pz5vghwrv4c7dha2ir4n1j24vmql-curl-8.6.0-bin", + "default": true + }, + { + "name": "man", + "path": "/nix/store/dcz7331rwvhy3gccyvgpsg5k9lqgbpyr-curl-8.6.0-man", + "default": true + }, + { + "name": "dev", + "path": "/nix/store/4z628w0fb864xzd70pb9dxfj835i2w8z-curl-8.6.0-dev" + }, + { + "name": "devdoc", + "path": "/nix/store/6h25ydl4qgwbzqr8plz3nra2xasf5q7v-curl-8.6.0-devdoc" + }, + { + "name": "out", + "path": "/nix/store/r18dz7grrxzsa3lq8hhga034350psr2y-curl-8.6.0" + } + ], + "store_path": "/nix/store/l4a7pz5vghwrv4c7dha2ir4n1j24vmql-curl-8.6.0-bin" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "bin", + "path": "/nix/store/lpl81g9mqryx7nqamaxhb5pj7kqn5f3h-curl-8.6.0-bin", + "default": true + }, + { + "name": "man", + "path": "/nix/store/ci69kkdhwqbxi8ls1q58rs830712ny8m-curl-8.6.0-man", + "default": true + }, + { + "name": "debug", + "path": "/nix/store/yckyk19014y3mnkr7gdwy0xdpcf4zq7r-curl-8.6.0-debug" + }, + { + "name": "dev", + "path": "/nix/store/0qbz4lg1hx991lk9z8v08smgnzcvccgk-curl-8.6.0-dev" + }, + { + "name": "devdoc", + "path": "/nix/store/l8vm3slv06a1ygif4n3sw4449y9974pr-curl-8.6.0-devdoc" + }, + { + "name": "out", + "path": "/nix/store/r0ba9cj4vknh9lnbrz349sa7ghjilfp0-curl-8.6.0" + } + ], + "store_path": "/nix/store/lpl81g9mqryx7nqamaxhb5pj7kqn5f3h-curl-8.6.0-bin" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "bin", + "path": "/nix/store/h4y9nsc225qs267qyw6508624vac3i65-curl-8.6.0-bin", + "default": true + }, + { + "name": "man", + "path": "/nix/store/ihl29gx6wd4qy8wanyzlsw9z3bwxp2lk-curl-8.6.0-man", + "default": true + }, + { + "name": "out", + "path": "/nix/store/5pmn51h74sqs0habggbf1cjwqyqxqavb-curl-8.6.0" + }, + { + "name": "dev", + "path": "/nix/store/s1jg15l85g9rx5906q0w0i4ylr24wp37-curl-8.6.0-dev" + }, + { + "name": "devdoc", + "path": "/nix/store/rbmwpdr558ar8h6yfqxzf65sswyhc1bf-curl-8.6.0-devdoc" + } + ], + "store_path": "/nix/store/h4y9nsc225qs267qyw6508624vac3i65-curl-8.6.0-bin" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "bin", + "path": "/nix/store/zlhsbgcxv68rwl79pymhbn2af8yv2zq0-curl-8.6.0-bin", + "default": true + }, + { + "name": "man", + "path": "/nix/store/i7v2fxwc8bir31d51s9nidn5k6pz8vi5-curl-8.6.0-man", + "default": true + }, + { + "name": "out", + "path": "/nix/store/2v59zbb6i773c1b0mwwdqhw3nghfm6d9-curl-8.6.0" + }, + { + "name": "debug", + "path": "/nix/store/crfj3m5p3xcbb0qy04pr158jh4adrgi3-curl-8.6.0-debug" + }, + { + "name": "dev", + "path": "/nix/store/sj6sqg8243z11nrlpdgw51p1f39ld63w-curl-8.6.0-dev" + }, + { + "name": "devdoc", + "path": "/nix/store/vlk9bi6cclivap3sp1b84lbba01x9lgp-curl-8.6.0-devdoc" + } + ], + "store_path": "/nix/store/zlhsbgcxv68rwl79pymhbn2af8yv2zq0-curl-8.6.0-bin" + } + } + }, + "git@2.44.1": { + "last_modified": "2024-05-22T06:18:38Z", + "resolved": "github:NixOS/nixpkgs/3f316d2a50699a78afe5e77ca486ad553169061e#git", + "source": "devbox-search", + "version": "2.44.1", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/1y3m89x5sl3bwag9lk4fdbqmswzjp9is-git-2.44.1", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/hhwbgxadc1r7f2vf0f7mf4mlcy3xsxh9-git-2.44.1-doc" + } + ], + "store_path": "/nix/store/1y3m89x5sl3bwag9lk4fdbqmswzjp9is-git-2.44.1" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/y6si9acv2p32jjyavs5arj8lwk7qq945-git-2.44.1", + "default": true + }, + { + "name": "debug", + "path": "/nix/store/vjgwf4vr6dqclvb503fqir13yiqp7bf6-git-2.44.1-debug" + }, + { + "name": "doc", + "path": "/nix/store/1c5816l3n7dgbjkbf6fdlw0amq5rvazq-git-2.44.1-doc" + } + ], + "store_path": "/nix/store/y6si9acv2p32jjyavs5arj8lwk7qq945-git-2.44.1" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/rwkir7nma4hgyg8lydzjisbjgwr9a4xf-git-2.44.1", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/y4l5l7k5syy0s3baznixgxbnmn8aiyxq-git-2.44.1-doc" + } + ], + "store_path": "/nix/store/rwkir7nma4hgyg8lydzjisbjgwr9a4xf-git-2.44.1" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/jxjzgz1p0hdqsg8k17ivw5mgcd6ffx9c-git-2.44.1", + "default": true + }, + { + "name": "debug", + "path": "/nix/store/93xf345jmvrcz1wc3va981rp13rn4hyz-git-2.44.1-debug" + }, + { + "name": "doc", + "path": "/nix/store/rvgnmd4cwvmy3v77ig977ix40d2mipri-git-2.44.1-doc" + } + ], + "store_path": "/nix/store/jxjzgz1p0hdqsg8k17ivw5mgcd6ffx9c-git-2.44.1" + } + } + }, + "go@1.22.5": { + "last_modified": "2024-08-14T11:41:26Z", + "resolved": "github:NixOS/nixpkgs/0cb2fd7c59fed0cd82ef858cbcbdb552b9a33465#go", + "source": "devbox-search", + "version": "1.22.5", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/p2i1kd6n12qj3s8kx65l3199mmjcffwz-go-1.22.5", + "default": true + } + ], + "store_path": "/nix/store/p2i1kd6n12qj3s8kx65l3199mmjcffwz-go-1.22.5" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/xvwr7mp9yqafl8pclayhrhdcxkszaf6d-go-1.22.5", + "default": true + } + ], + "store_path": "/nix/store/xvwr7mp9yqafl8pclayhrhdcxkszaf6d-go-1.22.5" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/vkrmfnargqkzg5amlfl4yh8vgxja7pli-go-1.22.5", + "default": true + } + ], + "store_path": "/nix/store/vkrmfnargqkzg5amlfl4yh8vgxja7pli-go-1.22.5" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/4ay992wzksf59aapkkh5lflv4rkbmdjy-go-1.22.5", + "default": true + } + ], + "store_path": "/nix/store/4ay992wzksf59aapkkh5lflv4rkbmdjy-go-1.22.5" + } + } + }, + "golangci-lint@1.60.3": { + "last_modified": "2024-09-07T06:00:21Z", + "resolved": "github:NixOS/nixpkgs/29cca090417df03e6d1928d99f77e8e81c74c3fa#golangci-lint", + "source": "devbox-search", + "version": "1.60.3", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/byyqd75qaws3bp84lfnipwdz65y2sms8-golangci-lint-1.60.3", + "default": true + } + ], + "store_path": "/nix/store/byyqd75qaws3bp84lfnipwdz65y2sms8-golangci-lint-1.60.3" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/q76c0iw8rpf1n8v2kwl7shiiymvm2lkn-golangci-lint-1.60.3", + "default": true + } + ], + "store_path": "/nix/store/q76c0iw8rpf1n8v2kwl7shiiymvm2lkn-golangci-lint-1.60.3" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/4ihxpxi0q56329cnvikv15vcj4lsz17m-golangci-lint-1.60.3", + "default": true + } + ], + "store_path": "/nix/store/4ihxpxi0q56329cnvikv15vcj4lsz17m-golangci-lint-1.60.3" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/ilaacmahr3mfjh2hsfvcsyh7f2gsz0zj-golangci-lint-1.60.3", + "default": true + } + ], + "store_path": "/nix/store/ilaacmahr3mfjh2hsfvcsyh7f2gsz0zj-golangci-lint-1.60.3" + } + } + }, + "gomarkdoc@1.1.0": { + "last_modified": "2024-09-12T11:58:09Z", + "resolved": "github:NixOS/nixpkgs/280db3decab4cbeb22a4599bd472229ab74d25e1#gomarkdoc", + "source": "devbox-search", + "version": "1.1.0", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/gyjdfx1rvr251z7jahdjxks0flhja7i8-gomarkdoc-1.1.0", + "default": true + } + ], + "store_path": "/nix/store/gyjdfx1rvr251z7jahdjxks0flhja7i8-gomarkdoc-1.1.0" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/aiqp8zsj297b6wk4q4ba02czd5hcddkn-gomarkdoc-1.1.0", + "default": true + } + ], + "store_path": "/nix/store/aiqp8zsj297b6wk4q4ba02czd5hcddkn-gomarkdoc-1.1.0" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/apb1z5ncnzjirgc7x53ibdwcrv54hvmx-gomarkdoc-1.1.0", + "default": true + } + ], + "store_path": "/nix/store/apb1z5ncnzjirgc7x53ibdwcrv54hvmx-gomarkdoc-1.1.0" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/00h496m6d9qsb321s07ic1g3s39865si-gomarkdoc-1.1.0", + "default": true + } + ], + "store_path": "/nix/store/00h496m6d9qsb321s07ic1g3s39865si-gomarkdoc-1.1.0" + } + } + } + } +} diff --git a/generate.go b/generate.go new file mode 100644 index 0000000..ebd7afb --- /dev/null +++ b/generate.go @@ -0,0 +1,3 @@ +package publicid + +//go:generate gomarkdoc -o README.md -e . diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8bc459b --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/agentstation/publicid + +go 1.22.5 + +require github.com/matoous/go-nanoid/v2 v2.1.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f4601eb --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE= +github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/publicid.go b/publicid.go new file mode 100644 index 0000000..93fe307 --- /dev/null +++ b/publicid.go @@ -0,0 +1,91 @@ +package publicid + +import ( + "fmt" + + nanoid "github.com/matoous/go-nanoid/v2" +) + +const ( + alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + longLen = 12 + shortLen = 8 +) + +// Option is a function type for configuring ID generation. +type Option func(*config) + +// config holds the configuration for ID generation. +type config struct { + attempts int +} + +// Attempts returns an Option to set the number of attempts for ID generation. +func Attempts(n int) Option { + return func(c *config) { + c.attempts = n + } +} + +// New generates a unique nanoID with a length of 8 characters and the given options. +func New(opts ...Option) (string, error) { + return generateID(shortLen, opts...) +} + +// NewLong generates a unique nanoID with a length of 12 characters and the given options. +func NewLong(opts ...Option) (string, error) { + return generateID(longLen, opts...) +} + +// generateID is a helper function to generate IDs with the given length and options. +func generateID(length int, opts ...Option) (string, error) { + cfg := &config{attempts: 1} + for _, opt := range opts { + opt(cfg) + } + + var lastErr error + for i := 0; i < cfg.attempts; i++ { + id, err := nanoid.Generate(alphabet, length) + if err == nil { + return id, nil + } + lastErr = err + } + return "", fmt.Errorf("failed to generate ID after %d attempts: %w", cfg.attempts, lastErr) +} + +// Validate checks if a given field name's public ID value is valid according to +// the constraints defined by package publicid. +func Validate(id string) error { + return validate(id, shortLen) +} + +// validateLong checks if a given field name's public ID value is valid according to +// the constraints defined by package publicid. +func ValidateLong(fieldName, id string) error { + return validate(id, longLen) +} + +// validate checks if a given public ID value is valid. +func validate(id string, expectedLen int) error { + if id == "" { + return fmt.Errorf("public ID is empty") + } + if len(id) != expectedLen { + return fmt.Errorf("public ID has length %d, want %d", len(id), expectedLen) + } + for _, char := range id { + if !isValidChar(char) { + return fmt.Errorf("public ID contains invalid character: %c", char) + } + } + return nil +} + +// isValidChar checks if a given character is a valid public ID character. +func isValidChar(c rune) bool { + return (c >= '0' && c <= '9') || + (c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') +} diff --git a/publicid_benchmark_test.go b/publicid_benchmark_test.go new file mode 100644 index 0000000..1ca831c --- /dev/null +++ b/publicid_benchmark_test.go @@ -0,0 +1,85 @@ +package publicid_test + +import ( + "testing" + + "github.com/agentstation/publicid" +) + +func BenchmarkNew(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := publicid.New() + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkNewWithAttempts(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := publicid.New(publicid.Attempts(5)) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkLong(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := publicid.NewLong() + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkLongWithAttempts(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := publicid.NewLong(publicid.Attempts(5)) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkValidate(b *testing.B) { + id, err := publicid.New() + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + err := publicid.Validate(id) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkValidateLong(b *testing.B) { + id, err := publicid.NewLong() + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + err := publicid.ValidateLong("BenchmarkValidateLong", id) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkUniqueness(b *testing.B) { + ids := make(map[string]bool) + for i := 0; i < b.N; i++ { + id, err := publicid.New() + if err != nil { + b.Fatalf("Failed to generate ID: %v", err) + } + if ids[id] { + b.Fatalf("Duplicate ID generated: %s", id) + } + ids[id] = true + } +} diff --git a/publicid_test.go b/publicid_test.go new file mode 100644 index 0000000..6d2e96a --- /dev/null +++ b/publicid_test.go @@ -0,0 +1,106 @@ +//go:build !integration +// +build !integration + +package publicid + +import ( + "testing" +) + +func TestNew(t *testing.T) { + id, err := New() + if err != nil { + t.Errorf("New() returned an error: %v", err) + } + if len(id) != 8 { + t.Errorf("New() returned id with length %d, want 8", len(id)) + } + if err := Validate(id); err != nil { + t.Errorf("New() returned invalid id: %v", err) + } +} + +func TestNewWithAttempts(t *testing.T) { + id, err := New(Attempts(5)) + if err != nil { + t.Errorf("New(Attempts(5)) returned an error: %v", err) + } + if len(id) != 8 { + t.Errorf("New(Attempts(5)) returned id with length %d, want 8", len(id)) + } + if err := Validate(id); err != nil { + t.Errorf("New(Attempts(5)) returned invalid id: %v", err) + } +} + +func TestLong(t *testing.T) { + id, err := NewLong() + if err != nil { + t.Errorf("Long() returned an error: %v", err) + } + if len(id) != 12 { + t.Errorf("Long() returned id with length %d, want 12", len(id)) + } + if err := ValidateLong("TestLong", id); err != nil { + t.Errorf("Long() returned invalid id: %v", err) + } +} + +func TestLongWithAttempts(t *testing.T) { + id, err := NewLong(Attempts(5)) + if err != nil { + t.Errorf("Long(Attempts(5)) returned an error: %v", err) + } + if len(id) != 12 { + t.Errorf("Long(Attempts(5)) returned id with length %d, want 12", len(id)) + } + if err := ValidateLong("TestLongWithAttempts", id); err != nil { + t.Errorf("Long(Attempts(5)) returned invalid id: %v", err) + } +} + +func TestValidate(t *testing.T) { + testCases := []struct { + name string + id string + wantError bool + }{ + {"Valid ID", "abCD1234", false}, + {"Empty ID", "", true}, + {"Short ID", "abc123", true}, + {"Long ID", "abcDEF123456", true}, + {"Invalid char", "abCD12_4", true}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := Validate(tc.id) + if (err != nil) != tc.wantError { + t.Errorf("Validate() error = %v, wantError %v", err, tc.wantError) + } + }) + } +} + +func TestValidateLong(t *testing.T) { + testCases := []struct { + name string + id string + wantError bool + }{ + {"Valid ID", "abCD1234EFGH", false}, + {"Empty ID", "", true}, + {"Short ID", "abcDEF123", true}, + {"Long ID", "abcDEF123456789", true}, + {"Invalid char", "abCD1234EF_H", true}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := ValidateLong("TestValidateLong", tc.id) + if (err != nil) != tc.wantError { + t.Errorf("ValidateLong() error = %v, wantError %v", err, tc.wantError) + } + }) + } +}