mirror of
https://github.com/jimeh/rands.git
synced 2026-02-19 11:26:38 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
312e856234 | ||
|
f4dccc6e46
|
|||
|
ec94efe49e
|
|||
| fe4308607c | |||
| e87d9c4726 | |||
| 127ebbaa03 | |||
|
|
41227bd53d | ||
| 16bd3ea3b9 | |||
| 8da5e1ef80 | |||
|
1b0bb32a3e
|
3
.github/.release-please-manifest.json
vendored
Normal file
3
.github/.release-please-manifest.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
".": "0.4.0"
|
||||||
|
}
|
||||||
15
.github/release-please-config.json
vendored
Normal file
15
.github/release-please-config.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"bootstrap-sha": "660fc4d179bfe22826230988407fff810e3b8568",
|
||||||
|
"packages": {
|
||||||
|
".": {
|
||||||
|
"release-type": "go",
|
||||||
|
"changelog-path": "CHANGELOG.md",
|
||||||
|
"bump-minor-pre-major": true,
|
||||||
|
"bump-patch-for-minor-pre-major": true,
|
||||||
|
"always-update": true,
|
||||||
|
"draft": false,
|
||||||
|
"prerelease": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
|
||||||
|
}
|
||||||
106
.github/workflows/ci.yml
vendored
106
.github/workflows/ci.yml
vendored
@@ -7,11 +7,14 @@ jobs:
|
|||||||
name: Lint
|
name: Lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: golangci-lint
|
- uses: actions/setup-go@v5
|
||||||
uses: golangci/golangci-lint-action@v2
|
|
||||||
with:
|
with:
|
||||||
version: v1.43
|
go-version-file: "go.mod"
|
||||||
|
- name: golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v6
|
||||||
|
with:
|
||||||
|
version: v1.64
|
||||||
env:
|
env:
|
||||||
VERBOSE: "true"
|
VERBOSE: "true"
|
||||||
|
|
||||||
@@ -19,34 +22,22 @@ jobs:
|
|||||||
name: Tidy
|
name: Tidy
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.15
|
go-version-file: "go.mod"
|
||||||
- uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-
|
|
||||||
- name: Check if mods are tidy
|
- name: Check if mods are tidy
|
||||||
run: make check-tidy
|
run: make check-tidy
|
||||||
|
|
||||||
benchmark:
|
benchmark:
|
||||||
name: Benchmarks
|
name: Benchmarks
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.ref != 'refs/heads/master'
|
if: github.ref != 'refs/heads/main'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.15
|
go-version-file: "go.mod"
|
||||||
- uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-
|
|
||||||
- name: Run benchmarks
|
- name: Run benchmarks
|
||||||
run: make bench | tee output.raw
|
run: make bench | tee output.raw
|
||||||
- name: Fix benchmark names
|
- name: Fix benchmark names
|
||||||
@@ -67,18 +58,12 @@ jobs:
|
|||||||
name: Coverage
|
name: Coverage
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.15
|
go-version-file: "go.mod"
|
||||||
- uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-
|
|
||||||
- name: Publish coverage
|
- name: Publish coverage
|
||||||
uses: paambaati/codeclimate-action@v2.7.4
|
uses: paambaati/codeclimate-action@v9
|
||||||
env:
|
env:
|
||||||
VERBOSE: "true"
|
VERBOSE: "true"
|
||||||
GOMAXPROCS: 4
|
GOMAXPROCS: 4
|
||||||
@@ -92,17 +77,22 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
name: Test
|
name: Test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version:
|
||||||
|
- "1.17"
|
||||||
|
- "1.18"
|
||||||
|
- "1.19"
|
||||||
|
- "1.20"
|
||||||
|
- "1.21"
|
||||||
|
- "1.22"
|
||||||
|
- "1.23"
|
||||||
|
- "1.24"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.15
|
go-version: ${{ matrix.go-version }}
|
||||||
- uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: make test
|
run: make test
|
||||||
env:
|
env:
|
||||||
@@ -111,18 +101,15 @@ jobs:
|
|||||||
benchmark-store:
|
benchmark-store:
|
||||||
name: Store benchmarks
|
name: Store benchmarks
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.ref == 'refs/heads/master'
|
if: github.ref == 'refs/heads/main'
|
||||||
|
permissions:
|
||||||
|
deployments: write
|
||||||
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.15
|
go-version-file: "go.mod"
|
||||||
- uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-
|
|
||||||
- name: Run benchmarks
|
- name: Run benchmarks
|
||||||
run: make bench | tee output.raw
|
run: make bench | tee output.raw
|
||||||
- name: Fix benchmark names
|
- name: Fix benchmark names
|
||||||
@@ -134,6 +121,19 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
tool: "go"
|
tool: "go"
|
||||||
output-file-path: output.txt
|
output-file-path: output.txt
|
||||||
github-token: ${{ secrets.GH_PUSH_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
comment-on-alert: true
|
comment-on-alert: true
|
||||||
auto-push: true
|
auto-push: true
|
||||||
|
|
||||||
|
release-please:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
|
outputs:
|
||||||
|
release_created: ${{ steps.release-please.outputs.release_created }}
|
||||||
|
version: ${{ steps.release-please.outputs.version }}
|
||||||
|
steps:
|
||||||
|
- uses: jimeh/release-please-manifest-action@v2
|
||||||
|
id: release-please
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.RELEASE_BOT_APP_ID }}
|
||||||
|
private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
|
||||||
|
|||||||
@@ -4,18 +4,13 @@ linters-settings:
|
|||||||
statements: 150
|
statements: 150
|
||||||
gocyclo:
|
gocyclo:
|
||||||
min-complexity: 20
|
min-complexity: 20
|
||||||
golint:
|
|
||||||
min-confidence: 0
|
|
||||||
govet:
|
govet:
|
||||||
check-shadowing: true
|
|
||||||
enable-all: true
|
enable-all: true
|
||||||
disable:
|
disable:
|
||||||
- fieldalignment
|
- fieldalignment
|
||||||
lll:
|
lll:
|
||||||
line-length: 80
|
line-length: 80
|
||||||
tab-width: 4
|
tab-width: 4
|
||||||
maligned:
|
|
||||||
suggest-new: true
|
|
||||||
misspell:
|
misspell:
|
||||||
locale: US
|
locale: US
|
||||||
|
|
||||||
@@ -24,13 +19,11 @@ linters:
|
|||||||
enable:
|
enable:
|
||||||
- asciicheck
|
- asciicheck
|
||||||
- bodyclose
|
- bodyclose
|
||||||
- deadcode
|
- copyloopvar
|
||||||
- depguard
|
|
||||||
- durationcheck
|
- durationcheck
|
||||||
- errcheck
|
- errcheck
|
||||||
- errorlint
|
- errorlint
|
||||||
- exhaustive
|
- exhaustive
|
||||||
- exportloopref
|
|
||||||
- funlen
|
- funlen
|
||||||
- gochecknoinits
|
- gochecknoinits
|
||||||
- goconst
|
- goconst
|
||||||
@@ -58,12 +51,10 @@ linters:
|
|||||||
- rowserrcheck
|
- rowserrcheck
|
||||||
- sqlclosecheck
|
- sqlclosecheck
|
||||||
- staticcheck
|
- staticcheck
|
||||||
- structcheck
|
|
||||||
- typecheck
|
- typecheck
|
||||||
- unconvert
|
- unconvert
|
||||||
- unparam
|
- unparam
|
||||||
- unused
|
- unused
|
||||||
- varcheck
|
|
||||||
- wastedassign
|
- wastedassign
|
||||||
- whitespace
|
- whitespace
|
||||||
|
|
||||||
|
|||||||
12
CHANGELOG.md
12
CHANGELOG.md
@@ -2,6 +2,18 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||||
|
|
||||||
|
## [0.4.0](https://github.com/jimeh/rands/compare/v0.3.0...v0.4.0) (2025-03-07)
|
||||||
|
|
||||||
|
|
||||||
|
### ⚠ BREAKING CHANGES
|
||||||
|
|
||||||
|
* **deps:** Minimum Go version changed from 1.15 to 1.17.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **deps:** upgrade to Go 1.17 and golanci-lint 1.64 ([#5](https://github.com/jimeh/rands/issues/5)) ([16bd3ea](https://github.com/jimeh/rands/commit/16bd3ea3b9483f5510c0e0fa35e832e881840b3a))
|
||||||
|
* **strings/uuidv7:** add UUIDv7 generation ([#10](https://github.com/jimeh/rands/issues/10)) ([fe43086](https://github.com/jimeh/rands/commit/fe4308607cc8d454255908dc44e64462759e303d))
|
||||||
|
|
||||||
## [0.3.0](https://github.com/jimeh/rands/compare/v0.2.0...v0.3.0) (2021-12-17)
|
## [0.3.0](https://github.com/jimeh/rands/compare/v0.2.0...v0.3.0) (2021-12-17)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
35
Makefile
35
Makefile
@@ -34,23 +34,19 @@ SHELL := env \
|
|||||||
# Tools
|
# Tools
|
||||||
#
|
#
|
||||||
|
|
||||||
TOOLS += $(TOOLDIR)/gobin
|
|
||||||
$(TOOLDIR)/gobin:
|
|
||||||
GO111MODULE=off go get -u github.com/myitcv/gobin
|
|
||||||
|
|
||||||
# external tool
|
# external tool
|
||||||
define tool # 1: binary-name, 2: go-import-path
|
define tool # 1: binary-name, 2: go-import-path
|
||||||
TOOLS += $(TOOLDIR)/$(1)
|
TOOLS += $(TOOLDIR)/$(1)
|
||||||
|
|
||||||
$(TOOLDIR)/$(1): $(TOOLDIR)/gobin Makefile
|
$(TOOLDIR)/$(1): Makefile
|
||||||
gobin $(V) "$(2)"
|
GOBIN="$(CURDIR)/$(TOOLDIR)" go install "$(2)"
|
||||||
endef
|
endef
|
||||||
|
|
||||||
$(eval $(call tool,godoc,golang.org/x/tools/cmd/godoc))
|
$(eval $(call tool,godoc,golang.org/x/tools/cmd/godoc@latest))
|
||||||
$(eval $(call tool,gofumpt,mvdan.cc/gofumpt))
|
$(eval $(call tool,gofumpt,mvdan.cc/gofumpt@latest))
|
||||||
$(eval $(call tool,goimports,golang.org/x/tools/cmd/goimports))
|
$(eval $(call tool,goimports,golang.org/x/tools/cmd/goimports@latest))
|
||||||
$(eval $(call tool,golangci-lint,github.com/golangci/golangci-lint/cmd/golangci-lint@v1.43))
|
$(eval $(call tool,golangci-lint,github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64))
|
||||||
$(eval $(call tool,gomod,github.com/Helcaraxan/gomod))
|
$(eval $(call tool,gomod,github.com/Helcaraxan/gomod@latest))
|
||||||
|
|
||||||
.PHONY: tools
|
.PHONY: tools
|
||||||
tools: $(TOOLS)
|
tools: $(TOOLS)
|
||||||
@@ -158,20 +154,3 @@ check-tidy:
|
|||||||
docs: $(TOOLDIR)/godoc
|
docs: $(TOOLDIR)/godoc
|
||||||
$(info serviing docs on http://127.0.0.1:6060/pkg/$(GOMODNAME)/)
|
$(info serviing docs on http://127.0.0.1:6060/pkg/$(GOMODNAME)/)
|
||||||
@godoc -http=127.0.0.1:6060
|
@godoc -http=127.0.0.1:6060
|
||||||
|
|
||||||
#
|
|
||||||
# Release
|
|
||||||
#
|
|
||||||
|
|
||||||
.PHONY: new-version
|
|
||||||
new-version: check-npx
|
|
||||||
npx standard-version
|
|
||||||
|
|
||||||
.PHONY: next-version
|
|
||||||
next-version: check-npx
|
|
||||||
npx standard-version --dry-run
|
|
||||||
|
|
||||||
.PHONY: check-npx
|
|
||||||
check-npx:
|
|
||||||
$(if $(shell which npx),,\
|
|
||||||
$(error No npx found in PATH, please install NodeJS))
|
|
||||||
|
|||||||
36
README.md
36
README.md
@@ -11,29 +11,13 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://pkg.go.dev/github.com/jimeh/rands">
|
<a href="https://pkg.go.dev/github.com/jimeh/rands"><img src="https://img.shields.io/badge/%E2%80%8B-reference-387b97.svg?logo=go&logoColor=white" alt="Go Reference"></a>
|
||||||
<img src="https://img.shields.io/badge/%E2%80%8B-reference-387b97.svg?logo=go&logoColor=white"
|
<a href="https://github.com/jimeh/rands/releases"><img src="https://img.shields.io/github/v/tag/jimeh/rands?label=release" alt="GitHub tag (latest SemVer)"></a>
|
||||||
alt="Go Reference">
|
<a href="https://github.com/jimeh/rands/actions"><img src="https://img.shields.io/github/actions/workflow/status/jimeh/rands/ci.yml?branch=main&logo=github" alt="Actions Status"></a>
|
||||||
</a>
|
<a href="https://codeclimate.com/github/jimeh/rands"><img src="https://img.shields.io/codeclimate/coverage/jimeh/rands.svg?logo=code%20climate" alt="Coverage"></a>
|
||||||
<a href="https://github.com/jimeh/rands/releases">
|
<a href="https://github.com/jimeh/rands/issues"><img src="https://img.shields.io/github/issues-raw/jimeh/rands.svg?style=flat&logo=github&logoColor=white" alt="GitHub issues"></a>
|
||||||
<img src="https://img.shields.io/github/v/tag/jimeh/rands?label=release" alt="GitHub tag (latest SemVer)">
|
<a href="https://github.com/jimeh/rands/pulls"><img src="https://img.shields.io/github/issues-pr-raw/jimeh/rands.svg?style=flat&logo=github&logoColor=white" alt="GitHub pull requests"></a>
|
||||||
</a>
|
<a href="https://github.com/jimeh/rands/blob/master/LICENSE"><img src="https://img.shields.io/github/license/jimeh/rands.svg?style=flat" alt="License Status"></a>
|
||||||
<a href="https://github.com/jimeh/rands/actions">
|
|
||||||
<img src="https://img.shields.io/github/workflow/status/jimeh/rands/CI.svg?logo=github" alt="Actions Status">
|
|
||||||
</a>
|
|
||||||
<a href="https://codeclimate.com/github/jimeh/rands">
|
|
||||||
<img src="https://img.shields.io/codeclimate/coverage/jimeh/rands.svg?logo=code%20climate" alt="Coverage">
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/jimeh/rands/issues">
|
|
||||||
<img src="https://img.shields.io/github/issues-raw/jimeh/rands.svg?style=flat&logo=github&logoColor=white"
|
|
||||||
alt="GitHub issues">
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/jimeh/rands/pulls">
|
|
||||||
<img src="https://img.shields.io/github/issues-pr-raw/jimeh/rands.svg?style=flat&logo=github&logoColor=white" alt="GitHub pull requests">
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/jimeh/rands/blob/master/LICENSE">
|
|
||||||
<img src="https://img.shields.io/github/license/jimeh/rands.svg?style=flat" alt="License Status">
|
|
||||||
</a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## [`rands`](https://pkg.go.dev/github.com/jimeh/rands) package
|
## [`rands`](https://pkg.go.dev/github.com/jimeh/rands) package
|
||||||
@@ -69,6 +53,7 @@ s, err := rands.UnicodeString(16, []rune("九七二人入八力十下三千上
|
|||||||
|
|
||||||
s, err := rands.DNSLabel(16) // => z0ij9o8qkbs0ru-h
|
s, err := rands.DNSLabel(16) // => z0ij9o8qkbs0ru-h
|
||||||
s, err := rands.UUID() // => a62b8712-f238-43ba-a47e-333f5fffe785
|
s, err := rands.UUID() // => a62b8712-f238-43ba-a47e-333f5fffe785
|
||||||
|
s, err := rands.UUIDv7() // => 01954a31-867f-7ffb-876e-b818f960ec3b
|
||||||
|
|
||||||
n, err := rands.Int(2147483647) // => 1334400235
|
n, err := rands.Int(2147483647) // => 1334400235
|
||||||
n, err := rands.Int64(int64(9223372036854775807)) // => 8256935979116161233
|
n, err := rands.Int64(int64(9223372036854775807)) // => 8256935979116161233
|
||||||
@@ -76,7 +61,7 @@ n, err := rands.Int64(int64(9223372036854775807)) // => 8256935979116161233
|
|||||||
b, err := rands.Bytes(8) // => [0 220 137 243 135 204 34 63]
|
b, err := rands.Bytes(8) // => [0 220 137 243 135 204 34 63]
|
||||||
```
|
```
|
||||||
|
|
||||||
## [`randsmust`](https://pkg.go.dev/github.com/jimeh/rands/must) package
|
## [`randsmust`](https://pkg.go.dev/github.com/jimeh/rands/randsmust) package
|
||||||
|
|
||||||
`randsmust` is specifically intended as an alternative to `rands` for use in
|
`randsmust` is specifically intended as an alternative to `rands` for use in
|
||||||
tests. All functions return a single value, and panic in the event of an error.
|
tests. All functions return a single value, and panic in the event of an error.
|
||||||
@@ -111,6 +96,7 @@ s := randsmust.UnicodeString(16, []rune("九七二人入八力十下三千上口
|
|||||||
|
|
||||||
s := randsmust.DNSLabel(16) // => pu31o0gqyk76x35f
|
s := randsmust.DNSLabel(16) // => pu31o0gqyk76x35f
|
||||||
s := randsmust.UUID() // => d616c873-f3dd-4690-bcd6-ed307eec1105
|
s := randsmust.UUID() // => d616c873-f3dd-4690-bcd6-ed307eec1105
|
||||||
|
s := randsmust.UUIDv7() // => 01954a30-add2-7590-8238-6cf6b2790c1e
|
||||||
|
|
||||||
n := randsmust.Int(2147483647) // => 1293388115
|
n := randsmust.Int(2147483647) // => 1293388115
|
||||||
n := randsmust.Int64(int64(9223372036854775807)) // => 6168113630900161239
|
n := randsmust.Int64(int64(9223372036854775807)) // => 6168113630900161239
|
||||||
@@ -123,7 +109,7 @@ b := randsmust.Bytes(8) // => [205 128 54 95 0 95 53 51]
|
|||||||
Please see the Go Reference for documentation and examples:
|
Please see the Go Reference for documentation and examples:
|
||||||
|
|
||||||
- [`rands`](https://pkg.go.dev/github.com/jimeh/rands)
|
- [`rands`](https://pkg.go.dev/github.com/jimeh/rands)
|
||||||
- [`randsmust`](https://pkg.go.dev/github.com/jimeh/rands/must)
|
- [`randsmust`](https://pkg.go.dev/github.com/jimeh/rands/randsmust)
|
||||||
|
|
||||||
## Benchmarks
|
## Benchmarks
|
||||||
|
|
||||||
|
|||||||
10
go.mod
10
go.mod
@@ -1,5 +1,11 @@
|
|||||||
module github.com/jimeh/rands
|
module github.com/jimeh/rands
|
||||||
|
|
||||||
go 1.15
|
go 1.17
|
||||||
|
|
||||||
require github.com/stretchr/testify v1.7.0
|
require github.com/stretchr/testify v1.10.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
|
|||||||
16
go.sum
16
go.sum
@@ -1,11 +1,19 @@
|
|||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
16
ints.go
16
ints.go
@@ -8,13 +8,13 @@ import (
|
|||||||
|
|
||||||
var ErrInvalidMaxInt = fmt.Errorf("%w: max cannot be less than 1", Err)
|
var ErrInvalidMaxInt = fmt.Errorf("%w: max cannot be less than 1", Err)
|
||||||
|
|
||||||
// Int generates a random int ranging between 0 and max.
|
// Int generates a random int ranging between 0 and nMax.
|
||||||
func Int(max int) (int, error) {
|
func Int(nMax int) (int, error) {
|
||||||
if max < 1 {
|
if nMax < 1 {
|
||||||
return 0, ErrInvalidMaxInt
|
return 0, ErrInvalidMaxInt
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
|
r, err := rand.Int(rand.Reader, big.NewInt(int64(nMax)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -22,13 +22,13 @@ func Int(max int) (int, error) {
|
|||||||
return int(r.Int64()), nil
|
return int(r.Int64()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Int64 generates a random int64 ranging between 0 and max.
|
// Int64 generates a random int64 ranging between 0 and nMax.
|
||||||
func Int64(max int64) (int64, error) {
|
func Int64(nMax int64) (int64, error) {
|
||||||
if max < 1 {
|
if nMax < 1 {
|
||||||
return 0, ErrInvalidMaxInt
|
return 0, ErrInvalidMaxInt
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := rand.Int(rand.Reader, big.NewInt(max))
|
r, err := rand.Int(rand.Reader, big.NewInt(nMax))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ package randsmust
|
|||||||
|
|
||||||
import "github.com/jimeh/rands"
|
import "github.com/jimeh/rands"
|
||||||
|
|
||||||
// Int generates a random int ranging between 0 and max.
|
// Int generates a random int ranging between 0 and nMax.
|
||||||
func Int(max int) int {
|
func Int(nMax int) int {
|
||||||
r, err := rands.Int(max)
|
r, err := rands.Int(nMax)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -12,9 +12,9 @@ func Int(max int) int {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
// Int64 generates a random int64 ranging between 0 and max.
|
// Int64 generates a random int64 ranging between 0 and nMax.
|
||||||
func Int64(max int64) int64 {
|
func Int64(nMax int64) int64 {
|
||||||
r, err := rands.Int64(max)
|
r, err := rands.Int64(nMax)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,12 +165,12 @@ func UnicodeString(n int, alphabet []rune) string {
|
|||||||
//
|
//
|
||||||
// In summary, the generated random string will:
|
// In summary, the generated random string will:
|
||||||
//
|
//
|
||||||
// - be between 1 and 63 characters in length, other n values returns a error
|
// - be between 1 and 63 characters in length, other n values returns a error
|
||||||
// - first character will be one of a-z
|
// - first character will be one of a-z
|
||||||
// - last character will be one of a-z or 0-9
|
// - last character will be one of a-z or 0-9
|
||||||
// - in-between first and last characters consist of a-z, 0-9, or "-"
|
// - in-between first and last characters consist of a-z, 0-9, or "-"
|
||||||
// - potentially contain two or more consecutive "-", except the 3rd and 4th
|
// - potentially contain two or more consecutive "-", except the 3rd and 4th
|
||||||
// characters, as that would violate RFC 5891, section 4.2.3.1.
|
// characters, as that would violate RFC 5891, section 4.2.3.1.
|
||||||
func DNSLabel(n int) string {
|
func DNSLabel(n int) string {
|
||||||
r, err := rands.DNSLabel(n)
|
r, err := rands.DNSLabel(n)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -180,7 +180,7 @@ func DNSLabel(n int) string {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
// UUID returns a random UUID v4 in string format as defined by RFC 4122,
|
// UUIDv4 returns a random UUID v4 in string format as defined by RFC 4122,
|
||||||
// section 4.4.
|
// section 4.4.
|
||||||
func UUID() string {
|
func UUID() string {
|
||||||
r, err := rands.UUID()
|
r, err := rands.UUID()
|
||||||
@@ -190,3 +190,24 @@ func UUID() string {
|
|||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UUIDv7 returns a time-ordered UUID v7 in string format.
|
||||||
|
//
|
||||||
|
// The UUID v7 format uses a timestamp with millisecond precision in the most
|
||||||
|
// significant bits, followed by random data. This provides both uniqueness and
|
||||||
|
// chronological ordering, making it ideal for database primary keys and
|
||||||
|
// situations where sorting by creation time is desired.
|
||||||
|
//
|
||||||
|
// References:
|
||||||
|
// - https://uuid7.com/
|
||||||
|
// - https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-7
|
||||||
|
//
|
||||||
|
//nolint:lll
|
||||||
|
func UUIDv7() string {
|
||||||
|
r, err := rands.UUIDv7()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|||||||
@@ -75,3 +75,8 @@ func ExampleUUID() {
|
|||||||
s := randsmust.UUID()
|
s := randsmust.UUID()
|
||||||
fmt.Println(s) // => 5baa35a6-9a46-49b4-91d0-9530173e118d
|
fmt.Println(s) // => 5baa35a6-9a46-49b4-91d0-9530173e118d
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ExampleUUIDv7() {
|
||||||
|
s := randsmust.UUIDv7()
|
||||||
|
fmt.Println(s) // => 01954a3a-a06f-7186-8774-51a770503eb2
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/jimeh/rands"
|
"github.com/jimeh/rands"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -487,19 +488,82 @@ func TestUUID(t *testing.T) {
|
|||||||
`^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$`,
|
`^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$`,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
|
||||||
for i := 0; i < 10000; i++ {
|
for i := 0; i < 10000; i++ {
|
||||||
got := UUID()
|
got := UUID()
|
||||||
require.Regexp(t, m, got)
|
require.Regexp(t, m, got)
|
||||||
|
|
||||||
|
if _, ok := seen[got]; ok {
|
||||||
|
require.FailNow(t, "duplicate UUID")
|
||||||
|
}
|
||||||
|
|
||||||
|
seen[got] = struct{}{}
|
||||||
|
|
||||||
raw := strings.ReplaceAll(got, "-", "")
|
raw := strings.ReplaceAll(got, "-", "")
|
||||||
b := make([]byte, 16)
|
b := make([]byte, 16)
|
||||||
_, err := hex.Decode(b, []byte(raw))
|
_, err := hex.Decode(b, []byte(raw))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Equal(t, 4, int(b[6]>>4), "version is not 4")
|
require.Equal(t, 4, int(b[6]>>4), "version is not 4")
|
||||||
require.Equal(t, byte(0x80), b[8]&0xc0,
|
require.Equal(t, byte(0x80), b[8]&0xc0, "variant is not RFC 4122")
|
||||||
"variant is not RFC 4122",
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUUIDv7(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
m := regexp.MustCompile(
|
||||||
|
`^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$`,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Store timestamps to verify they're increasing
|
||||||
|
var lastTimestampBytes int64
|
||||||
|
var lastUUID string
|
||||||
|
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
for i := 0; i < 10000; i++ {
|
||||||
|
got := UUIDv7()
|
||||||
|
require.Regexp(t, m, got)
|
||||||
|
|
||||||
|
if _, ok := seen[got]; ok {
|
||||||
|
require.FailNow(t, "duplicate UUID")
|
||||||
|
}
|
||||||
|
|
||||||
|
seen[got] = struct{}{}
|
||||||
|
|
||||||
|
raw := strings.ReplaceAll(got, "-", "")
|
||||||
|
b := make([]byte, 16)
|
||||||
|
_, err := hex.Decode(b, []byte(raw))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Check version is 7
|
||||||
|
require.Equal(t, 7, int(b[6]>>4), "version is not 7")
|
||||||
|
|
||||||
|
// Check variant is RFC 4122
|
||||||
|
require.Equal(t, byte(0x80), b[8]&0xc0, "variant is not RFC 4122")
|
||||||
|
|
||||||
|
// Extract timestamp bytes
|
||||||
|
timestampBytes := int64(b[0])<<40 | int64(b[1])<<32 | int64(b[2])<<24 |
|
||||||
|
int64(b[3])<<16 | int64(b[4])<<8 | int64(b[5])
|
||||||
|
|
||||||
|
// Verify timestamp is within 10 seconds of current time. This is a
|
||||||
|
// sanity check to ensure the UUID is not too far off from the current
|
||||||
|
// time, while allowing tests to pass on super slow machines.
|
||||||
|
tsTime := time.UnixMilli(timestampBytes)
|
||||||
|
require.WithinDuration(t, time.Now(), tsTime, 10*time.Second,
|
||||||
|
"timestamp is not within 10 seconds of current time",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// After the first UUID, verify that UUIDs are monotonically increasing
|
||||||
|
if i > 0 && timestampBytes < lastTimestampBytes {
|
||||||
|
require.FailNow(t, "UUIDs are not monotonically increasing",
|
||||||
|
"current: %s (ts: %d), previous: %s (ts: %d)",
|
||||||
|
got, timestampBytes, lastUUID, lastTimestampBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
lastTimestampBytes = timestampBytes
|
||||||
|
lastUUID = got
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
51
strings.go
51
strings.go
@@ -7,6 +7,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/jimeh/rands/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -18,7 +20,6 @@ const (
|
|||||||
alphabeticChars = upperChars + lowerChars
|
alphabeticChars = upperChars + lowerChars
|
||||||
alphanumericChars = alphabeticChars + numericChars
|
alphanumericChars = alphabeticChars + numericChars
|
||||||
dnsLabelChars = lowerNumericChars + "-"
|
dnsLabelChars = lowerNumericChars + "-"
|
||||||
uuidHyphen = byte('-')
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -171,12 +172,12 @@ func UnicodeString(n int, alphabet []rune) (string, error) {
|
|||||||
//
|
//
|
||||||
// In summary, the generated random string will:
|
// In summary, the generated random string will:
|
||||||
//
|
//
|
||||||
// - be between 1 and 63 characters in length, other n values returns a error
|
// - be between 1 and 63 characters in length, other n values returns a error
|
||||||
// - first character will be one of a-z
|
// - first character will be one of a-z
|
||||||
// - last character will be one of a-z or 0-9
|
// - last character will be one of a-z or 0-9
|
||||||
// - in-between first and last characters consist of a-z, 0-9, or "-"
|
// - in-between first and last characters consist of a-z, 0-9, or "-"
|
||||||
// - potentially contain two or more consecutive "-", except the 3rd and 4th
|
// - potentially contain two or more consecutive "-", except the 3rd and 4th
|
||||||
// characters, as that would violate RFC 5891, section 4.2.3.1.
|
// characters, as that would violate RFC 5891, section 4.2.3.1.
|
||||||
func DNSLabel(n int) (string, error) {
|
func DNSLabel(n int) (string, error) {
|
||||||
switch {
|
switch {
|
||||||
case n < 1 || n > 63:
|
case n < 1 || n > 63:
|
||||||
@@ -237,27 +238,33 @@ func DNSLabel(n int) (string, error) {
|
|||||||
// UUID returns a random UUID v4 in string format as defined by RFC 4122,
|
// UUID returns a random UUID v4 in string format as defined by RFC 4122,
|
||||||
// section 4.4.
|
// section 4.4.
|
||||||
func UUID() (string, error) {
|
func UUID() (string, error) {
|
||||||
b, err := Bytes(16)
|
uuid, err := uuid.NewRandom()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
b[6] = (b[6] & 0x0f) | 0x40 // Version: 4 (random)
|
return uuid.String(), nil
|
||||||
b[8] = (b[8] & 0x3f) | 0x80 // Variant: RFC 4122
|
}
|
||||||
|
|
||||||
// Construct a UUID v4 string according to RFC 4122 specifications.
|
// UUIDv7 returns a time-ordered UUID v7 in string format.
|
||||||
dst := make([]byte, 36)
|
//
|
||||||
hex.Encode(dst[0:8], b[0:4]) // time-low
|
// The UUID v7 format uses a timestamp with millisecond precision in the most
|
||||||
dst[8] = uuidHyphen
|
// significant bits, followed by random data. This provides both uniqueness and
|
||||||
hex.Encode(dst[9:13], b[4:6]) // time-mid
|
// chronological ordering, making it ideal for database primary keys and
|
||||||
dst[13] = uuidHyphen
|
// situations where sorting by creation time is desired.
|
||||||
hex.Encode(dst[14:18], b[6:8]) // time-high-and-version
|
//
|
||||||
dst[18] = uuidHyphen
|
// References:
|
||||||
hex.Encode(dst[19:23], b[8:10]) // clock-seq-and-reserved, clock-seq-low
|
// - https://uuid7.com/
|
||||||
dst[23] = uuidHyphen
|
// - https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-7
|
||||||
hex.Encode(dst[24:], b[10:]) // node
|
//
|
||||||
|
//nolint:lll
|
||||||
|
func UUIDv7() (string, error) {
|
||||||
|
uuid, err := uuid.NewV7()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
return string(dst), nil
|
return uuid.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func isASCII(s string) bool {
|
func isASCII(s string) bool {
|
||||||
|
|||||||
@@ -132,3 +132,12 @@ func ExampleUUID() {
|
|||||||
|
|
||||||
fmt.Println(s) // => 6a1c4f65-d5d6-4a28-aa51-eaa94fa7ad4a
|
fmt.Println(s) // => 6a1c4f65-d5d6-4a28-aa51-eaa94fa7ad4a
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ExampleUUIDv7() {
|
||||||
|
s, err := rands.UUIDv7()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(s) // => 01954a3a-a06f-7848-b836-bced92ae5a1a
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -612,20 +613,26 @@ func TestUUID(t *testing.T) {
|
|||||||
`^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$`,
|
`^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$`,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
|
||||||
for i := 0; i < 10000; i++ {
|
for i := 0; i < 10000; i++ {
|
||||||
got, err := UUID()
|
got, err := UUID()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Regexp(t, m, got)
|
require.Regexp(t, m, got)
|
||||||
|
|
||||||
|
if _, ok := seen[got]; ok {
|
||||||
|
require.FailNow(t, "duplicate UUID")
|
||||||
|
}
|
||||||
|
|
||||||
|
seen[got] = struct{}{}
|
||||||
|
|
||||||
raw := strings.ReplaceAll(got, "-", "")
|
raw := strings.ReplaceAll(got, "-", "")
|
||||||
b := make([]byte, 16)
|
b := make([]byte, 16)
|
||||||
_, err = hex.Decode(b, []byte(raw))
|
_, err = hex.Decode(b, []byte(raw))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Equal(t, 4, int(b[6]>>4), "version is not 4")
|
require.Equal(t, 4, int(b[6]>>4), "version is not 4")
|
||||||
require.Equal(t, byte(0x80), b[8]&0xc0,
|
require.Equal(t, byte(0x80), b[8]&0xc0, "variant is not RFC 4122")
|
||||||
"variant is not RFC 4122",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -635,6 +642,70 @@ func BenchmarkUUID(b *testing.B) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUUIDv7(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
m := regexp.MustCompile(
|
||||||
|
`^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$`,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Store timestamps to verify they're increasing
|
||||||
|
var lastTimestampBytes int64
|
||||||
|
var lastUUID string
|
||||||
|
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
for i := 0; i < 10000; i++ {
|
||||||
|
got, err := UUIDv7()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Regexp(t, m, got)
|
||||||
|
|
||||||
|
if _, ok := seen[got]; ok {
|
||||||
|
require.FailNow(t, "duplicate UUID")
|
||||||
|
}
|
||||||
|
|
||||||
|
seen[got] = struct{}{}
|
||||||
|
|
||||||
|
raw := strings.ReplaceAll(got, "-", "")
|
||||||
|
b := make([]byte, 16)
|
||||||
|
_, err = hex.Decode(b, []byte(raw))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Check version is 7
|
||||||
|
require.Equal(t, 7, int(b[6]>>4), "version is not 7")
|
||||||
|
|
||||||
|
// Check variant is RFC 4122
|
||||||
|
require.Equal(t, byte(0x80), b[8]&0xc0, "variant is not RFC 4122")
|
||||||
|
|
||||||
|
// Extract timestamp bytes
|
||||||
|
timestampBytes := int64(b[0])<<40 | int64(b[1])<<32 |
|
||||||
|
int64(b[2])<<24 | int64(b[3])<<16 | int64(b[4])<<8 | int64(b[5])
|
||||||
|
|
||||||
|
// Verify timestamp is within 10 seconds of current time. This is a
|
||||||
|
// sanity check to ensure the UUID is not too far off from the current
|
||||||
|
// time, while allowing tests to pass on super slow machines.
|
||||||
|
tsTime := time.UnixMilli(timestampBytes)
|
||||||
|
require.WithinDuration(t, time.Now(), tsTime, 10*time.Second,
|
||||||
|
"timestamp is not within 10 seconds of current time",
|
||||||
|
)
|
||||||
|
|
||||||
|
// After the first UUID, verify that UUIDs are monotonically increasing
|
||||||
|
if i > 0 && timestampBytes < lastTimestampBytes {
|
||||||
|
require.FailNow(t, "UUIDs are not monotonically increasing",
|
||||||
|
"current: %s (ts: %d), previous: %s (ts: %d)",
|
||||||
|
got, timestampBytes, lastUUID, lastTimestampBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
lastTimestampBytes = timestampBytes
|
||||||
|
lastUUID = got
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkUUIDv7(b *testing.B) {
|
||||||
|
for n := 0; n < b.N; n++ {
|
||||||
|
_, _ = UUIDv7()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Helpers
|
// Helpers
|
||||||
//
|
//
|
||||||
|
|||||||
24
uuid/random.go
Normal file
24
uuid/random.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package uuid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewRandom returns a random UUID v4 in string format as defined by RFC 4122,
|
||||||
|
// section 4.4.
|
||||||
|
func NewRandom() (UUID, error) {
|
||||||
|
var u UUID
|
||||||
|
|
||||||
|
// Fill the entire UUID with random bytes.
|
||||||
|
_, err := rand.Read(u[:])
|
||||||
|
if err != nil {
|
||||||
|
// This should never happen.
|
||||||
|
return u, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the version and variant bits.
|
||||||
|
u[6] = (u[6] & 0x0f) | 0x40 // Version: 4 (random)
|
||||||
|
u[8] = (u[8] & 0x3f) | 0x80 // Variant: RFC 4122
|
||||||
|
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
39
uuid/random_test.go
Normal file
39
uuid/random_test.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package uuid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewRandom(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
m := regexp.MustCompile(
|
||||||
|
`^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$`,
|
||||||
|
)
|
||||||
|
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
|
||||||
|
for i := 0; i < 10000; i++ {
|
||||||
|
got, err := NewRandom()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Regexp(t, m, got.String())
|
||||||
|
|
||||||
|
if _, ok := seen[got.String()]; ok {
|
||||||
|
require.FailNow(t, "duplicate UUID")
|
||||||
|
}
|
||||||
|
|
||||||
|
seen[got.String()] = struct{}{}
|
||||||
|
|
||||||
|
require.Equal(t, 4, int(got[6]>>4), "version is not 4")
|
||||||
|
require.Equal(t, byte(0x80), got[8]&0xc0, "variant is not RFC 4122")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkNewRandom(b *testing.B) {
|
||||||
|
for n := 0; n < b.N; n++ {
|
||||||
|
_, _ = NewRandom()
|
||||||
|
}
|
||||||
|
}
|
||||||
101
uuid/uuid.go
Normal file
101
uuid/uuid.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
// Package uuid provides a UUID type and associated utilities.
|
||||||
|
package uuid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Err = errors.New("uuid")
|
||||||
|
ErrInvalidLength = fmt.Errorf("%w: invalid length", Err)
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
hyphen = '-'
|
||||||
|
)
|
||||||
|
|
||||||
|
// UUID represents a Universally Unique Identifier (UUID).
|
||||||
|
// It is implemented as a 16-byte array.
|
||||||
|
type UUID [16]byte
|
||||||
|
|
||||||
|
// String returns the string representation of the UUID,
|
||||||
|
// formatted according to RFC 4122 (8-4-4-4-12 hex digits separated by hyphens).
|
||||||
|
func (u UUID) String() string {
|
||||||
|
dst := make([]byte, 36)
|
||||||
|
|
||||||
|
hex.Encode(dst[0:8], u[0:4])
|
||||||
|
dst[8] = hyphen
|
||||||
|
hex.Encode(dst[9:13], u[4:6])
|
||||||
|
dst[13] = hyphen
|
||||||
|
hex.Encode(dst[14:18], u[6:8])
|
||||||
|
dst[18] = hyphen
|
||||||
|
hex.Encode(dst[19:23], u[8:10])
|
||||||
|
dst[23] = hyphen
|
||||||
|
hex.Encode(dst[24:], u[10:])
|
||||||
|
|
||||||
|
return string(dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromBytes creates a UUID from a byte slice.
|
||||||
|
//
|
||||||
|
// If the slice isn't exactly 16 bytes, it returns an empty UUID.
|
||||||
|
func FromBytes(b []byte) (UUID, error) {
|
||||||
|
var u UUID
|
||||||
|
if len(b) != 16 {
|
||||||
|
return u, ErrInvalidLength
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(u[:], b)
|
||||||
|
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromString creates a UUID from a string.
|
||||||
|
//
|
||||||
|
// If the string isn't exactly 36 characters, it returns an empty UUID.
|
||||||
|
func FromString(s string) (UUID, error) {
|
||||||
|
if len(s) != 36 {
|
||||||
|
return UUID{}, ErrInvalidLength
|
||||||
|
}
|
||||||
|
|
||||||
|
raw := strings.ReplaceAll(s, "-", "")
|
||||||
|
|
||||||
|
u := UUID{}
|
||||||
|
_, err := hex.Decode(u[:], []byte(raw))
|
||||||
|
if err != nil {
|
||||||
|
return UUID{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time returns the timestamp of the UUID if it's a version 7 (time-ordered)
|
||||||
|
// UUID. Otherwise, it returns the zero time.
|
||||||
|
func (u UUID) Time() (t time.Time, ok bool) {
|
||||||
|
if u.Version() != 7 {
|
||||||
|
return time.Time{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the timestamp from the UUID.
|
||||||
|
// For UUIDv7, only the first 6 bytes contain the timestamp in milliseconds
|
||||||
|
timestamp := uint64(u[0])<<40 | uint64(u[1])<<32 | uint64(u[2])<<24 |
|
||||||
|
uint64(u[3])<<16 | uint64(u[4])<<8 | uint64(u[5])
|
||||||
|
|
||||||
|
if timestamp > math.MaxInt64 {
|
||||||
|
// This shouldn't happen until year 292,272,993.
|
||||||
|
return time.Time{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.UnixMilli(int64(timestamp)), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version returns the version of the UUID.
|
||||||
|
func (u UUID) Version() int {
|
||||||
|
// The version is stored in the 4 most significant bits of byte 6
|
||||||
|
return int(u[6] >> 4)
|
||||||
|
}
|
||||||
475
uuid/uuid_test.go
Normal file
475
uuid/uuid_test.go
Normal file
@@ -0,0 +1,475 @@
|
|||||||
|
package uuid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUUID_String(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
uuid UUID
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Zero UUID",
|
||||||
|
uuid: UUID{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||||
|
expected: "00000000-0000-0000-0000-000000000000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Random UUID",
|
||||||
|
uuid: UUID{
|
||||||
|
0x12, 0x34, 0x56, 0x78,
|
||||||
|
0x9a, 0xbc,
|
||||||
|
0xde, 0xf0,
|
||||||
|
0x12, 0x34,
|
||||||
|
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
||||||
|
},
|
||||||
|
expected: "12345678-9abc-def0-1234-56789abcdef0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UUID with max values",
|
||||||
|
uuid: UUID{
|
||||||
|
0xff, 0xff, 0xff, 0xff,
|
||||||
|
0xff, 0xff,
|
||||||
|
0xff, 0xff,
|
||||||
|
0xff, 0xff,
|
||||||
|
0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||||
|
},
|
||||||
|
expected: "ffffffff-ffff-ffff-ffff-ffffffffffff",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := tt.uuid.String()
|
||||||
|
assert.Equal(t, tt.expected, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkUUID_String(b *testing.B) {
|
||||||
|
u := UUID{
|
||||||
|
0x12, 0x34, 0x56, 0x78,
|
||||||
|
0x9a, 0xbc,
|
||||||
|
0xde, 0xf0,
|
||||||
|
0x12, 0x34,
|
||||||
|
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = u.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFromBytes(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
bytes []byte
|
||||||
|
want UUID
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid 16 bytes",
|
||||||
|
bytes: []byte{
|
||||||
|
0x12,
|
||||||
|
0x34,
|
||||||
|
0x56,
|
||||||
|
0x78,
|
||||||
|
0x9a,
|
||||||
|
0xbc,
|
||||||
|
0xde,
|
||||||
|
0xf0,
|
||||||
|
0x12,
|
||||||
|
0x34,
|
||||||
|
0x56,
|
||||||
|
0x78,
|
||||||
|
0x9a,
|
||||||
|
0xbc,
|
||||||
|
0xde,
|
||||||
|
0xf0,
|
||||||
|
},
|
||||||
|
want: UUID{
|
||||||
|
0x12, 0x34, 0x56, 0x78,
|
||||||
|
0x9a, 0xbc,
|
||||||
|
0xde, 0xf0,
|
||||||
|
0x12, 0x34,
|
||||||
|
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
||||||
|
},
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty bytes",
|
||||||
|
bytes: []byte{},
|
||||||
|
want: UUID{},
|
||||||
|
wantErr: ErrInvalidLength,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Too few bytes",
|
||||||
|
bytes: []byte{0x12, 0x34, 0x56, 0x78, 0x9a},
|
||||||
|
want: UUID{},
|
||||||
|
wantErr: ErrInvalidLength,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Too many bytes",
|
||||||
|
bytes: []byte{
|
||||||
|
0x12,
|
||||||
|
0x34,
|
||||||
|
0x56,
|
||||||
|
0x78,
|
||||||
|
0x9a,
|
||||||
|
0xbc,
|
||||||
|
0xde,
|
||||||
|
0xf0,
|
||||||
|
0x12,
|
||||||
|
0x34,
|
||||||
|
0x56,
|
||||||
|
0x78,
|
||||||
|
0x9a,
|
||||||
|
0xbc,
|
||||||
|
0xde,
|
||||||
|
0xf0,
|
||||||
|
0xab,
|
||||||
|
},
|
||||||
|
want: UUID{},
|
||||||
|
wantErr: ErrInvalidLength,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := FromBytes(tt.bytes)
|
||||||
|
|
||||||
|
if tt.wantErr != nil {
|
||||||
|
assert.EqualError(t, err, tt.wantErr.Error())
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkFromBytes(b *testing.B) {
|
||||||
|
bytes := []byte{
|
||||||
|
0x12, 0x34, 0x56, 0x78,
|
||||||
|
0x9a, 0xbc,
|
||||||
|
0xde, 0xf0,
|
||||||
|
0x12, 0x34,
|
||||||
|
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, _ = FromBytes(bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFromString(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
str string
|
||||||
|
want UUID
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid UUID string",
|
||||||
|
str: "12345678-9abc-def0-1234-56789abcdef0",
|
||||||
|
want: UUID{
|
||||||
|
0x12, 0x34, 0x56, 0x78,
|
||||||
|
0x9a, 0xbc,
|
||||||
|
0xde, 0xf0,
|
||||||
|
0x12, 0x34,
|
||||||
|
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
||||||
|
},
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty string",
|
||||||
|
str: "",
|
||||||
|
want: UUID{},
|
||||||
|
wantErr: ErrInvalidLength,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Too short string",
|
||||||
|
str: "12345678-9abc-def0-1234-56789abcde",
|
||||||
|
want: UUID{},
|
||||||
|
wantErr: ErrInvalidLength,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Too long string",
|
||||||
|
str: "12345678-9abc-def0-1234-56789abcdef0a",
|
||||||
|
want: UUID{},
|
||||||
|
wantErr: ErrInvalidLength,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid characters",
|
||||||
|
str: "12345678-9abc-defg-1234-56789abcdef0",
|
||||||
|
want: UUID{},
|
||||||
|
wantErr: errors.New("encoding/hex: invalid byte: U+0067 'g'"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Zero UUID",
|
||||||
|
str: "00000000-0000-0000-0000-000000000000",
|
||||||
|
want: UUID{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Max value UUID",
|
||||||
|
str: "ffffffff-ffff-ffff-ffff-ffffffffffff",
|
||||||
|
want: UUID{
|
||||||
|
0xff, 0xff, 0xff, 0xff,
|
||||||
|
0xff, 0xff,
|
||||||
|
0xff, 0xff,
|
||||||
|
0xff, 0xff,
|
||||||
|
0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||||
|
},
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := FromString(tt.str)
|
||||||
|
|
||||||
|
if tt.wantErr != nil {
|
||||||
|
assert.EqualError(t, err, tt.wantErr.Error())
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkFromString(b *testing.B) {
|
||||||
|
uuidStr := "12345678-9abc-def0-1234-56789abcdef0"
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, _ = FromString(uuidStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUUID_Time(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Define a reference time for testing.
|
||||||
|
refTime := time.Date(2023, 5, 15, 12, 0, 0, 0, time.UTC)
|
||||||
|
// The timestamp in milliseconds (Unix timestamp * 1000 in 6 bytes).
|
||||||
|
timestampMillis := refTime.UnixMilli()
|
||||||
|
|
||||||
|
// Create bytes for the timestamp (first 6 bytes of UUID).
|
||||||
|
timestampBytes := []byte{
|
||||||
|
byte(timestampMillis >> 40),
|
||||||
|
byte(timestampMillis >> 32),
|
||||||
|
byte(timestampMillis >> 24),
|
||||||
|
byte(timestampMillis >> 16),
|
||||||
|
byte(timestampMillis >> 8),
|
||||||
|
byte(timestampMillis),
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
uuid UUID
|
||||||
|
wantTime time.Time
|
||||||
|
wantOk bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Version 7 UUID",
|
||||||
|
uuid: func() UUID {
|
||||||
|
var u UUID
|
||||||
|
// Set first 6 bytes to timestamp.
|
||||||
|
copy(u[:6], timestampBytes)
|
||||||
|
// Set version to 7 (0111 as the high nibble of byte 6).
|
||||||
|
u[6] = (u[6] & 0x0F) | 0x70
|
||||||
|
// Set variant to RFC 4122 (10xx as the high bits of byte 8).
|
||||||
|
u[8] = (u[8] & 0x3F) | 0x80
|
||||||
|
|
||||||
|
return u
|
||||||
|
}(),
|
||||||
|
wantTime: refTime,
|
||||||
|
wantOk: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Version 4 UUID (not time-based)",
|
||||||
|
uuid: func() UUID {
|
||||||
|
var u UUID
|
||||||
|
// Set first 6 bytes to same timestamp to verify it's ignored.
|
||||||
|
copy(u[:6], timestampBytes)
|
||||||
|
// Set version to 4 (0100 as the high nibble of byte 6).
|
||||||
|
u[6] = (u[6] & 0x0F) | 0x40
|
||||||
|
|
||||||
|
return u
|
||||||
|
}(),
|
||||||
|
wantTime: time.Time{}, // Zero time for non-V7 UUIDs
|
||||||
|
wantOk: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Zero UUID",
|
||||||
|
uuid: UUID{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||||
|
wantTime: time.Time{}, // Zero time for version 0
|
||||||
|
wantOk: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
gotTime, gotOk := tt.uuid.Time()
|
||||||
|
|
||||||
|
assert.Equal(t, tt.wantOk, gotOk)
|
||||||
|
|
||||||
|
if tt.wantTime.IsZero() {
|
||||||
|
assert.True(t, gotTime.IsZero())
|
||||||
|
} else {
|
||||||
|
// Compare time at millisecond precision.
|
||||||
|
assert.Equal(t, tt.wantTime.UnixMilli(), gotTime.UnixMilli())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkUUID_Time(b *testing.B) {
|
||||||
|
uuid, err := NewV7()
|
||||||
|
require.NoError(b, err)
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, _ = uuid.Time()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUUID_Version(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
uuid UUID
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Version 0 (invalid/nil UUID)",
|
||||||
|
uuid: UUID{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Version 1 (time-based)",
|
||||||
|
uuid: UUID{
|
||||||
|
0x12, 0x34, 0x56, 0x78,
|
||||||
|
0x9a, 0xbc,
|
||||||
|
0x10, 0xf0, // 0x10 = version 1 in top nibble
|
||||||
|
0x12, 0x34,
|
||||||
|
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
||||||
|
},
|
||||||
|
want: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Version 2 (DCE Security)",
|
||||||
|
uuid: UUID{
|
||||||
|
0x12, 0x34, 0x56, 0x78,
|
||||||
|
0x9a, 0xbc,
|
||||||
|
0x20, 0xf0, // 0x20 = version 2 in top nibble
|
||||||
|
0x12, 0x34,
|
||||||
|
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
||||||
|
},
|
||||||
|
want: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Version 3 (name-based, MD5)",
|
||||||
|
uuid: UUID{
|
||||||
|
0x12, 0x34, 0x56, 0x78,
|
||||||
|
0x9a, 0xbc,
|
||||||
|
0x30, 0xf0, // 0x30 = version 3 in top nibble
|
||||||
|
0x12, 0x34,
|
||||||
|
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
||||||
|
},
|
||||||
|
want: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Version 4 (random)",
|
||||||
|
uuid: UUID{
|
||||||
|
0x12, 0x34, 0x56, 0x78,
|
||||||
|
0x9a, 0xbc,
|
||||||
|
0x40, 0xf0, // 0x40 = version 4 in top nibble
|
||||||
|
0x12, 0x34,
|
||||||
|
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
||||||
|
},
|
||||||
|
want: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Version 5 (name-based, SHA-1)",
|
||||||
|
uuid: UUID{
|
||||||
|
0x12, 0x34, 0x56, 0x78,
|
||||||
|
0x9a, 0xbc,
|
||||||
|
0x50, 0xf0, // 0x50 = version 5 in top nibble
|
||||||
|
0x12, 0x34,
|
||||||
|
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
||||||
|
},
|
||||||
|
want: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Version 7 (time-ordered)",
|
||||||
|
uuid: UUID{
|
||||||
|
0x12, 0x34, 0x56, 0x78,
|
||||||
|
0x9a, 0xbc,
|
||||||
|
0x70, 0xf0, // 0x70 = version 7 in top nibble
|
||||||
|
0x12, 0x34,
|
||||||
|
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
||||||
|
},
|
||||||
|
want: 7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Version 8 (custom)",
|
||||||
|
uuid: UUID{
|
||||||
|
0x12, 0x34, 0x56, 0x78,
|
||||||
|
0x9a, 0xbc,
|
||||||
|
0x80, 0xf0, // 0x80 = version 8 in top nibble
|
||||||
|
0x12, 0x34,
|
||||||
|
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
||||||
|
},
|
||||||
|
want: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Version 15 (theoretical max)",
|
||||||
|
uuid: UUID{
|
||||||
|
0x12, 0x34, 0x56, 0x78,
|
||||||
|
0x9a, 0xbc,
|
||||||
|
0xf0, 0xf0, // 0xf0 = version 15 in top nibble
|
||||||
|
0x12, 0x34,
|
||||||
|
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
||||||
|
},
|
||||||
|
want: 15,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := tt.uuid.Version()
|
||||||
|
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkUUID_Version(b *testing.B) {
|
||||||
|
uuid, err := NewV7()
|
||||||
|
require.NoError(b, err)
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = uuid.Version()
|
||||||
|
}
|
||||||
|
}
|
||||||
56
uuid/version_7.go
Normal file
56
uuid/version_7.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package uuid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewV7 returns a time-ordered UUID v7 in string format.
|
||||||
|
//
|
||||||
|
// The UUID v7 format uses a timestamp with millisecond precision in the most
|
||||||
|
// significant bits, followed by random data. This provides both uniqueness and
|
||||||
|
// chronological ordering, making it ideal for database primary keys and
|
||||||
|
// situations where sorting by creation time is desired.
|
||||||
|
//
|
||||||
|
// References:
|
||||||
|
// - https://uuid7.com/
|
||||||
|
// - https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-7
|
||||||
|
//
|
||||||
|
//nolint:lll
|
||||||
|
func NewV7() (UUID, error) {
|
||||||
|
var u UUID
|
||||||
|
|
||||||
|
// Write the timestamp to the first 6 bytes of the UUID.
|
||||||
|
timestamp := time.Now().UnixMilli()
|
||||||
|
u[0] = byte(timestamp >> 40)
|
||||||
|
u[1] = byte(timestamp >> 32)
|
||||||
|
u[2] = byte(timestamp >> 24)
|
||||||
|
u[3] = byte(timestamp >> 16)
|
||||||
|
u[4] = byte(timestamp >> 8)
|
||||||
|
u[5] = byte(timestamp)
|
||||||
|
|
||||||
|
// Fill the remaining bytes with random data.
|
||||||
|
_, err := rand.Read(u[6:])
|
||||||
|
if err != nil {
|
||||||
|
// This should never happen.
|
||||||
|
return u, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the version and variant bits.
|
||||||
|
u[6] = (u[6] & 0x0f) | 0x70 // Version: 7 (time-ordered)
|
||||||
|
u[8] = (u[8] & 0x3f) | 0x80 // Variant: RFC 4122
|
||||||
|
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// V7Time returns the time of a UUIDv7 string.
|
||||||
|
//
|
||||||
|
// If the UUID is not a valid UUIDv7, it returns a zero time and false.
|
||||||
|
func V7Time(s string) (t time.Time, ok bool) {
|
||||||
|
u, err := FromString(s)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.Time()
|
||||||
|
}
|
||||||
168
uuid/version_7_test.go
Normal file
168
uuid/version_7_test.go
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
package uuid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUUIDv7(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
m := regexp.MustCompile(
|
||||||
|
`^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$`,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Store timestamps to verify they're increasing.
|
||||||
|
var lastTimestampBytes int64
|
||||||
|
var lastUUID string
|
||||||
|
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
for i := 0; i < 10000; i++ {
|
||||||
|
got, err := NewV7()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Regexp(t, m, got.String())
|
||||||
|
|
||||||
|
if _, ok := seen[got.String()]; ok {
|
||||||
|
require.FailNow(t, "duplicate UUID")
|
||||||
|
}
|
||||||
|
|
||||||
|
seen[got.String()] = struct{}{}
|
||||||
|
|
||||||
|
// Check version is 7.
|
||||||
|
require.Equal(t, 7, int(got[6]>>4), "version is not 7")
|
||||||
|
|
||||||
|
// Check variant is RFC 4122.
|
||||||
|
require.Equal(t, byte(0x80), got[8]&0xc0, "variant is not RFC 4122")
|
||||||
|
|
||||||
|
// Extract timestamp bytes.
|
||||||
|
timestampBytes := int64(got[0])<<40 | int64(got[1])<<32 |
|
||||||
|
int64(got[2])<<24 | int64(got[3])<<16 | int64(got[4])<<8 |
|
||||||
|
int64(got[5])
|
||||||
|
|
||||||
|
// Verify timestamp is within 10 seconds of current time. This is a
|
||||||
|
// sanity check to ensure the UUID is not too far off from the current
|
||||||
|
// time, while allowing tests to pass on super slow machines.
|
||||||
|
tsTime := time.UnixMilli(timestampBytes)
|
||||||
|
require.WithinDuration(t, time.Now(), tsTime, 10*time.Second,
|
||||||
|
"timestamp is not within 10 seconds of current time",
|
||||||
|
)
|
||||||
|
|
||||||
|
// After the first UUID, verify that UUIDs are monotonically increasing
|
||||||
|
if i > 0 && timestampBytes < lastTimestampBytes {
|
||||||
|
require.FailNow(t, "UUIDs are not monotonically increasing",
|
||||||
|
"current: %s (ts: %d), previous: %s (ts: %d)",
|
||||||
|
got, timestampBytes, lastUUID, lastTimestampBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
lastTimestampBytes = timestampBytes
|
||||||
|
lastUUID = got.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkNewV7(b *testing.B) {
|
||||||
|
for n := 0; n < b.N; n++ {
|
||||||
|
_, _ = NewV7()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestV7Time(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Define a reference time for testing.
|
||||||
|
refTime := time.Date(2023, 5, 15, 12, 0, 0, 0, time.UTC)
|
||||||
|
// The timestamp in milliseconds.
|
||||||
|
timestampMillis := refTime.UnixMilli()
|
||||||
|
|
||||||
|
// Create bytes for the timestamp (first 6 bytes of UUID).
|
||||||
|
timestampBytes := []byte{
|
||||||
|
byte(timestampMillis >> 40),
|
||||||
|
byte(timestampMillis >> 32),
|
||||||
|
byte(timestampMillis >> 24),
|
||||||
|
byte(timestampMillis >> 16),
|
||||||
|
byte(timestampMillis >> 8),
|
||||||
|
byte(timestampMillis),
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
uuidStr string
|
||||||
|
wantTime time.Time
|
||||||
|
wantOk bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Version 7 UUID",
|
||||||
|
uuidStr: func() string {
|
||||||
|
var u UUID
|
||||||
|
// Set first 6 bytes to timestamp.
|
||||||
|
copy(u[:6], timestampBytes)
|
||||||
|
// Set version to 7 (0111 as the high nibble of byte 6).
|
||||||
|
u[6] = (u[6] & 0x0F) | 0x70
|
||||||
|
// Set variant to RFC 4122 (10xx as the high bits of byte 8).
|
||||||
|
u[8] = (u[8] & 0x3F) | 0x80
|
||||||
|
|
||||||
|
return u.String()
|
||||||
|
}(),
|
||||||
|
wantTime: refTime,
|
||||||
|
wantOk: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Version 4 UUID (not time-based)",
|
||||||
|
uuidStr: func() string {
|
||||||
|
var u UUID
|
||||||
|
// Set first 6 bytes to same timestamp to verify it's ignored.
|
||||||
|
copy(u[:6], timestampBytes)
|
||||||
|
// Set version to 4 (0100 as the high nibble of byte 6).
|
||||||
|
u[6] = (u[6] & 0x0F) | 0x40
|
||||||
|
// Set variant to RFC 4122 (10xx as the high bits of byte 8).
|
||||||
|
u[8] = (u[8] & 0x3F) | 0x80
|
||||||
|
|
||||||
|
return u.String()
|
||||||
|
}(),
|
||||||
|
wantTime: time.Time{}, // Zero time for non-V7 UUIDs
|
||||||
|
wantOk: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Zero UUID",
|
||||||
|
uuidStr: "00000000-0000-0000-0000-000000000000",
|
||||||
|
wantTime: time.Time{}, // Zero time for version 0
|
||||||
|
wantOk: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid UUID string",
|
||||||
|
uuidStr: "not-a-valid-uuid",
|
||||||
|
wantTime: time.Time{},
|
||||||
|
wantOk: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
gotTime, gotOk := V7Time(tt.uuidStr)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.wantOk, gotOk)
|
||||||
|
|
||||||
|
if tt.wantTime.IsZero() {
|
||||||
|
assert.True(t, gotTime.IsZero())
|
||||||
|
} else {
|
||||||
|
// Compare time at millisecond precision.
|
||||||
|
assert.Equal(t, tt.wantTime.UnixMilli(), gotTime.UnixMilli())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkV7Time(b *testing.B) {
|
||||||
|
u, err := NewV7()
|
||||||
|
require.NoError(b, err)
|
||||||
|
|
||||||
|
s := u.String()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for n := 0; n < b.N; n++ {
|
||||||
|
_, _ = V7Time(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user