diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 839a5c8..1e66466 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -51,6 +51,20 @@ updates: patterns: - "*" + - package-ecosystem: "gomod" + directory: "/id" + schedule: + interval: "daily" + commit-message: + prefix: "[id]" + include: "scope" + allow: + - dependency-type: all + groups: + main: + patterns: + - "*" + - package-ecosystem: "gomod" directory: "/tst" schedule: diff --git a/.github/workflows/sub_id.yml b/.github/workflows/sub_id.yml new file mode 100644 index 0000000..b40abba --- /dev/null +++ b/.github/workflows/sub_id.yml @@ -0,0 +1,26 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json +name: id + +on: + workflow_dispatch: {} + + push: + branches: [ main ] + + pull_request: + branches: [ main ] + paths: + - .golangci.yml + - tools/** + - .github/workflows/ci.yml + - .github/workflows/sub_id.yml + - id/** + +jobs: + + ci: + uses: ./.github/workflows/ci.yml + with: + mod_path: id + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/README.md b/README.md index e773704..8996176 100644 --- a/README.md +++ b/README.md @@ -4,39 +4,12 @@ https://docs.codecov.com/docs/status-badges --> -## http -[Readme](http/README.md)     -[![ci](https://github.com/ifnotnil/x/actions/workflows/sub_http.yml/badge.svg)](https://github.com/ifnotnil/x/actions/workflows/sub_http.yml) -[![Go Report Card](https://goreportcard.com/badge/github.com/ifnotnil/x/http)](https://goreportcard.com/report/github.com/ifnotnil/x/http) -[![PkgGoDev](https://pkg.go.dev/badge/github.com/ifnotnit/x/http)](https://pkg.go.dev/github.com/ifnotnil/x/http) -[![Version](https://img.shields.io/github/v/tag/ifnotnil/x?filter=http%2F*)](https://pkg.go.dev/github.com/ifnotnil/x/http?tab=versions) -[![codecov](https://codecov.io/gh/ifnotnil/x/graph/badge.svg?token=n0t9q5Y3Sf&component=http)](https://codecov.io/gh/ifnotnil/x) -Install: `go get -u github.com/ifnotnil/x/http` +| Module | Description | +| -------- | ------- | +| `http` | [![ci](https://github.com/ifnotnil/x/actions/workflows/sub_http.yml/badge.svg)](https://github.com/ifnotnil/x/actions/workflows/sub_http.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/ifnotnil/x/http)](https://goreportcard.com/report/github.com/ifnotnil/x/http) [![PkgGoDev](https://pkg.go.dev/badge/github.com/ifnotnit/x/http)](https://pkg.go.dev/github.com/ifnotnil/x/http) [![Version](https://img.shields.io/github/v/tag/ifnotnil/x?filter=http%2F*)](https://pkg.go.dev/github.com/ifnotnil/x/http?tab=versions) [![codecov](https://codecov.io/gh/ifnotnil/x/graph/badge.svg?token=n0t9q5Y3Sf&component=http)](https://codecov.io/gh/ifnotnil/x)
Install: `go get -u github.com/ifnotnil/x/http`

[Readme](http/README.md)
http middlewares and inbound/outbound loggers | +| `conf` | [![ci](https://github.com/ifnotnil/x/actions/workflows/sub_conf.yml/badge.svg)](https://github.com/ifnotnil/x/actions/workflows/sub_conf.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/ifnotnil/x/conf)](https://goreportcard.com/report/github.com/ifnotnil/x/conf) [![PkgGoDev](https://pkg.go.dev/badge/github.com/ifnotnit/x/conf)](https://pkg.go.dev/github.com/ifnotnil/x/conf) [![Version](https://img.shields.io/github/v/tag/ifnotnil/x?filter=conf%2F*)](https://pkg.go.dev/github.com/ifnotnil/x/http?conf=versions) [![codecov](https://codecov.io/gh/ifnotnil/x/graph/badge.svg?token=n0t9q5Y3Sf&component=conf)](https://codecov.io/gh/ifnotnil/x)
Install: `go get -u github.com/ifnotnil/x/conf`

[Readme](conf/README.md)
A [knadh/koanf](github.com/knadh/koanf) boilerplate with some helpers | +| `tst` | [![ci](https://github.com/ifnotnil/x/actions/workflows/sub_tst.yml/badge.svg)](https://github.com/ifnotnil/x/actions/workflows/sub_tst.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/ifnotnil/x/tst)](https://goreportcard.com/report/github.com/ifnotnil/x/tst) [![PkgGoDev](https://pkg.go.dev/badge/github.com/ifnotnit/x/tst)](https://pkg.go.dev/github.com/ifnotnil/x/tst) [![Version](https://img.shields.io/github/v/tag/ifnotnil/x?filter=tst%2F*)](https://pkg.go.dev/github.com/ifnotnil/x/tst?tab=versions) [![codecov](https://codecov.io/gh/ifnotnil/x/graph/badge.svg?token=n0t9q5Y3Sf&component=tst)](https://codecov.io/gh/ifnotnil/x)
Install: `go get -u github.com/ifnotnil/x/tst`

[Readme](tst/README.md)
A test helper package providing error assertion test functions. | +| `id` | [![ci](https://github.com/ifnotnil/x/actions/workflows/sub_id.yml/badge.svg)](https://github.com/ifnotnil/x/actions/workflows/sub_id.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/ifnotnil/x/id)](https://goreportcard.com/report/github.com/ifnotnil/x/id) [![PkgGoDev](https://pkg.go.dev/badge/github.com/ifnotnit/x/id)](https://pkg.go.dev/github.com/ifnotnil/x/id) [![Version](https://img.shields.io/github/v/tag/ifnotnil/x?filter=id%2F*)](https://pkg.go.dev/github.com/ifnotnil/x/id?tab=versions) [![codecov](https://codecov.io/gh/ifnotnil/x/graph/badge.svg?token=n0t9q5Y3Sf&component=id)](https://codecov.io/gh/ifnotnil/x)
Install: `go get -u github.com/ifnotnil/x/id`

[Readme](id/README.md)
URL Safe base64 and base 62 wrappers of uuid for text representation. | -## conf - -A [knadh/koanf](github.com/knadh/koanf) boilerplate with some helpers. - -[Readme](conf/README.md)     -[![ci](https://github.com/ifnotnil/x/actions/workflows/sub_conf.yml/badge.svg)](https://github.com/ifnotnil/x/actions/workflows/sub_conf.yml) -[![Go Report Card](https://goreportcard.com/badge/github.com/ifnotnil/x/conf)](https://goreportcard.com/report/github.com/ifnotnil/x/conf) -[![PkgGoDev](https://pkg.go.dev/badge/github.com/ifnotnit/x/conf)](https://pkg.go.dev/github.com/ifnotnil/x/conf) -[![Version](https://img.shields.io/github/v/tag/ifnotnil/x?filter=conf%2F*)](https://pkg.go.dev/github.com/ifnotnil/x/conf?tab=versions) -[![codecov](https://codecov.io/gh/ifnotnil/x/graph/badge.svg?token=n0t9q5Y3Sf&component=conf)](https://codecov.io/gh/ifnotnil/x) - -Install: `go get -u github.com/ifnotnil/x/conf` - -## tst - -A test helper package providing error assertion test functions. - -[Readme](tst/README.md)     -[![ci](https://github.com/ifnotnil/x/actions/workflows/sub_tst.yml/badge.svg)](https://github.com/ifnotnil/x/actions/workflows/sub_tst.yml) -[![Go Report Card](https://goreportcard.com/badge/github.com/ifnotnil/x/tst)](https://goreportcard.com/report/github.com/ifnotnil/x/tst) -[![PkgGoDev](https://pkg.go.dev/badge/github.com/ifnotnit/x/tst)](https://pkg.go.dev/github.com/ifnotnil/x/tst) -[![Version](https://img.shields.io/github/v/tag/ifnotnil/x?filter=tst%2F*)](https://pkg.go.dev/github.com/ifnotnil/x/tst?tab=versions) -[![codecov](https://codecov.io/gh/ifnotnil/x/graph/badge.svg?token=n0t9q5Y3Sf&component=tst)](https://codecov.io/gh/ifnotnil/x) - -Install: `go get -u github.com/ifnotnil/x/tst` diff --git a/codecov.yml b/codecov.yml index f6ba961..67f4cef 100644 --- a/codecov.yml +++ b/codecov.yml @@ -34,6 +34,10 @@ component_management: name: tst paths: - "tst/" + - component_id: id + name: id + paths: + - "id/" ignore: - "http/internal/testingx" # test helpers diff --git a/http/encoding/encoding.go b/http/encoding/encoding.go index 585fb13..139ee3e 100644 --- a/http/encoding/encoding.go +++ b/http/encoding/encoding.go @@ -1,7 +1,6 @@ package encoding import ( - "encoding/base64" "errors" "net/url" "strings" @@ -16,16 +15,6 @@ import ( "golang.org/x/text/encoding/unicode" ) -// URLSafeBase64 returns a [base64.Encoding] based on [base64.URLEncoding] replacing the default padding character ('=') padding character to a url safe one ('~'). -// In URL parameters, the following characters are considered safe and do not need encoding [rfc3986](https://www.rfc-editor.org/rfc/rfc3986.html#section-3.1): -// Alphabetic characters: A-Z, a-z -// Digits: 0-9 -// Hyphen: - -// Underscore: _ -// Period: . -// Tilde: ~ -var URLSafeBase64 = base64.URLEncoding.WithPadding('~') - // RFC5987ExtendedNotationParameterValue decodes RFC 5987 encoded filenames expecting the extended notation // (charset "'" [ language ] "'" value-chars) // example: UTF-8'en'file%20name.jpg diff --git a/id/.mockery.yml b/id/.mockery.yml new file mode 100644 index 0000000..8163587 --- /dev/null +++ b/id/.mockery.yml @@ -0,0 +1,23 @@ +# https://vektra.github.io/mockery/v3.5/configuration/ + +log-level: info +formatter: goimports +force-file-write: true +require-template-schema-exists: true + +all: true +recursive: false +dir: '{{.InterfaceDir}}' +filename: mocks_test.go +pkgname: '{{.SrcPackageName}}' +structname: '{{.Mock}}{{.InterfaceName}}' + +# https://vektra.github.io/mockery/v3.5/template/ +template: testify +template-schema: '{{.Template}}.schema.json' + +packages: + github.com/ifnotnil/x/id: + config: + all: true + recursive: true diff --git a/id/Makefile b/id/Makefile new file mode 100644 index 0000000..b4b668b --- /dev/null +++ b/id/Makefile @@ -0,0 +1,8 @@ +SHELL := /usr/bin/env bash + +REPO_ROOT = $(shell cd .. && pwd) + +include $(REPO_ROOT)/scripts/go.mk +include $(REPO_ROOT)/tools/tools.mk +include $(REPO_ROOT)/scripts/lib.mk + diff --git a/id/README.md b/id/README.md new file mode 100644 index 0000000..03de4df --- /dev/null +++ b/id/README.md @@ -0,0 +1,10 @@ +# id +[![ci](https://github.com/ifnotnil/x/actions/workflows/sub_id.yml/badge.svg)](https://github.com/ifnotnil/x/actions/workflows/sub_id.yml) +[![Go Report Card](https://goreportcard.com/badge/github.com/ifnotnil/x/id)](https://goreportcard.com/report/github.com/ifnotnil/x/id) +[![PkgGoDev](https://pkg.go.dev/badge/github.com/ifnotnit/x/id)](https://pkg.go.dev/github.com/ifnotnil/x/id) +[![Version](https://img.shields.io/github/v/tag/ifnotnil/x?filter=id%2F*)](https://pkg.go.dev/github.com/ifnotnil/x/id?tab=versions) +[![codecov](https://codecov.io/gh/ifnotnil/x/graph/badge.svg?token=n0t9q5Y3Sf&component=id)](https://codecov.io/gh/ifnotnil/x) + +URL Safe base64 and base 62 wrappers of uuid for text representation. + +The wrappers work with generics of `~[16]byte` so both `gofrs` and `google` uuid can be wrapped. diff --git a/id/base62.go b/id/base62.go new file mode 100644 index 0000000..ef4a952 --- /dev/null +++ b/id/base62.go @@ -0,0 +1,55 @@ +package id + +import ( + "fmt" + "math" + "math/big" +) + +type Base62 struct{} + +func (e Base62) appendEncode(dst, src []byte) []byte { + if len(src) == 0 { + return nil + } + + num := big.Int{} + num.SetBytes(src) + return num.Append(dst, 62) +} + +func (e Base62) encodedLen(n int) int { + return base62EncodedLen(n) +} + +func (e Base62) decode(dst, src []byte) (n int, err error) { + if len(src) == 0 { + return 0, nil + } + + num := big.Int{} + nn, ok := num.SetString(string(src), 62) + if !ok { + return 0, fmt.Errorf("base62 error while parsing") + } + + bb := nn.Bytes() + // if len(bb) != uuidSize { + // return 0, fmt.Errorf("base62 error while parsing") + // } + + return copy(dst, bb), nil +} + +// lg262 : log2(62) ≈ 5.954196310386875 +const lg262 = 5.954196310386875 + +func base62EncodedLen(n int) int { + return int(math.Ceil(float64(n) * 8 / lg262)) +} + +func base62DecodedLen(m int) int { + return int(math.Floor(float64(m) * lg262 / 8)) +} + +var _ encoding = Base62{} diff --git a/id/base64.go b/id/base64.go new file mode 100644 index 0000000..1fcf1f6 --- /dev/null +++ b/id/base64.go @@ -0,0 +1,93 @@ +package id + +import ( + "encoding/base64" +) + +// In URL parameters, the following characters are considered safe and do not need encoding [rfc3986](https://www.rfc-editor.org/rfc/rfc3986.html#section-3.1): +// Alphabetic characters: A-Z, a-z +// Digits: 0-9 +// Hyphen: - +// Underscore: _ +// Period: . +// Tilde: ~ + +// stdBase64 is a [base64.Encoding] based on [base64.URLEncoding] without padding character. +// alphabet of base64: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_ +var ( + stdBase64 = base64.URLEncoding.WithPadding(base64.NoPadding) + stdBase64WithPadding = base64.URLEncoding.WithPadding('~') +) + +type Base64 struct{} + +func (b Base64) encode(dst, src []byte) { + stdBase64.Encode(dst, src) +} + +func (b Base64) appendEncode(dst, src []byte) []byte { + return stdBase64.AppendEncode(dst, src) +} + +func (b Base64) encodeToString(src []byte) string { + return stdBase64.EncodeToString(src) +} + +func (b Base64) encodedLen(n int) int { + return stdBase64.EncodedLen(n) +} + +func (b Base64) appendDecode(dst, src []byte) ([]byte, error) { + return stdBase64.AppendDecode(dst, src) +} + +func (b Base64) decodeString(s string) ([]byte, error) { + return stdBase64.DecodeString(s) +} + +func (b Base64) decode(dst, src []byte) (n int, err error) { + return stdBase64.Decode(dst, src) +} + +func (b Base64) decodedLen(n int) int { + return stdBase64.DecodedLen(n) +} + +type Base64WithPadding struct{} + +func (b Base64WithPadding) encode(dst, src []byte) { + stdBase64WithPadding.Encode(dst, src) +} + +func (b Base64WithPadding) appendEncode(dst, src []byte) []byte { + return stdBase64WithPadding.AppendEncode(dst, src) +} + +func (b Base64WithPadding) encodeToString(src []byte) string { + return stdBase64WithPadding.EncodeToString(src) +} + +func (b Base64WithPadding) encodedLen(n int) int { + return stdBase64WithPadding.EncodedLen(n) +} + +func (b Base64WithPadding) appendDecode(dst, src []byte) ([]byte, error) { + return stdBase64WithPadding.AppendDecode(dst, src) +} + +func (b Base64WithPadding) decodeString(s string) ([]byte, error) { + return stdBase64WithPadding.DecodeString(s) +} + +func (b Base64WithPadding) decode(dst, src []byte) (n int, err error) { + return stdBase64WithPadding.Decode(dst, src) +} + +func (b Base64WithPadding) decodedLen(n int) int { + return stdBase64WithPadding.DecodedLen(n) +} + +var ( + _ encoding = Base64{} + _ encoding = Base64WithPadding{} +) diff --git a/id/go.mod b/id/go.mod new file mode 100644 index 0000000..24b7595 --- /dev/null +++ b/id/go.mod @@ -0,0 +1,15 @@ +module github.com/ifnotnil/x/id + +go 1.24.0 + +require ( + github.com/ifnotnil/x/tst v0.0.2 + github.com/stretchr/testify v1.11.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/id/go.sum b/id/go.sum new file mode 100644 index 0000000..eeaccad --- /dev/null +++ b/id/go.sum @@ -0,0 +1,14 @@ +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/ifnotnil/x/tst v0.0.2 h1:6ydceMwj3uiKFu1B+TTQJcGd1KZtDOAdyy95kTgtCe4= +github.com/ifnotnil/x/tst v0.0.2/go.mod h1:TFSDsUOkXhDw6k2+vxuypmPXhJzuQ3U+qWHFc4KiMEo= +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/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/id/mocks_test.go b/id/mocks_test.go new file mode 100644 index 0000000..8fb0a3e --- /dev/null +++ b/id/mocks_test.go @@ -0,0 +1,212 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package id + +import ( + mock "github.com/stretchr/testify/mock" +) + +// newMockencoding creates a new instance of mockencoding. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newMockencoding(t interface { + mock.TestingT + Cleanup(func()) +}) *mockencoding { + mock := &mockencoding{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// mockencoding is an autogenerated mock type for the encoding type +type mockencoding struct { + mock.Mock +} + +type mockencoding_Expecter struct { + mock *mock.Mock +} + +func (_m *mockencoding) EXPECT() *mockencoding_Expecter { + return &mockencoding_Expecter{mock: &_m.Mock} +} + +// appendEncode provides a mock function for the type mockencoding +func (_mock *mockencoding) appendEncode(dst []byte, src []byte) []byte { + ret := _mock.Called(dst, src) + + if len(ret) == 0 { + panic("no return value specified for appendEncode") + } + + var r0 []byte + if returnFunc, ok := ret.Get(0).(func([]byte, []byte) []byte); ok { + r0 = returnFunc(dst, src) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + return r0 +} + +// mockencoding_appendEncode_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'appendEncode' +type mockencoding_appendEncode_Call struct { + *mock.Call +} + +// appendEncode is a helper method to define mock.On call +// - dst []byte +// - src []byte +func (_e *mockencoding_Expecter) appendEncode(dst interface{}, src interface{}) *mockencoding_appendEncode_Call { + return &mockencoding_appendEncode_Call{Call: _e.mock.On("appendEncode", dst, src)} +} + +func (_c *mockencoding_appendEncode_Call) Run(run func(dst []byte, src []byte)) *mockencoding_appendEncode_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 []byte + if args[0] != nil { + arg0 = args[0].([]byte) + } + var arg1 []byte + if args[1] != nil { + arg1 = args[1].([]byte) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *mockencoding_appendEncode_Call) Return(bytes []byte) *mockencoding_appendEncode_Call { + _c.Call.Return(bytes) + return _c +} + +func (_c *mockencoding_appendEncode_Call) RunAndReturn(run func(dst []byte, src []byte) []byte) *mockencoding_appendEncode_Call { + _c.Call.Return(run) + return _c +} + +// decode provides a mock function for the type mockencoding +func (_mock *mockencoding) decode(dst []byte, src []byte) (int, error) { + ret := _mock.Called(dst, src) + + if len(ret) == 0 { + panic("no return value specified for decode") + } + + var r0 int + var r1 error + if returnFunc, ok := ret.Get(0).(func([]byte, []byte) (int, error)); ok { + return returnFunc(dst, src) + } + if returnFunc, ok := ret.Get(0).(func([]byte, []byte) int); ok { + r0 = returnFunc(dst, src) + } else { + r0 = ret.Get(0).(int) + } + if returnFunc, ok := ret.Get(1).(func([]byte, []byte) error); ok { + r1 = returnFunc(dst, src) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// mockencoding_decode_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'decode' +type mockencoding_decode_Call struct { + *mock.Call +} + +// decode is a helper method to define mock.On call +// - dst []byte +// - src []byte +func (_e *mockencoding_Expecter) decode(dst interface{}, src interface{}) *mockencoding_decode_Call { + return &mockencoding_decode_Call{Call: _e.mock.On("decode", dst, src)} +} + +func (_c *mockencoding_decode_Call) Run(run func(dst []byte, src []byte)) *mockencoding_decode_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 []byte + if args[0] != nil { + arg0 = args[0].([]byte) + } + var arg1 []byte + if args[1] != nil { + arg1 = args[1].([]byte) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *mockencoding_decode_Call) Return(n int, err error) *mockencoding_decode_Call { + _c.Call.Return(n, err) + return _c +} + +func (_c *mockencoding_decode_Call) RunAndReturn(run func(dst []byte, src []byte) (int, error)) *mockencoding_decode_Call { + _c.Call.Return(run) + return _c +} + +// encodedLen provides a mock function for the type mockencoding +func (_mock *mockencoding) encodedLen(n int) int { + ret := _mock.Called(n) + + if len(ret) == 0 { + panic("no return value specified for encodedLen") + } + + var r0 int + if returnFunc, ok := ret.Get(0).(func(int) int); ok { + r0 = returnFunc(n) + } else { + r0 = ret.Get(0).(int) + } + return r0 +} + +// mockencoding_encodedLen_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'encodedLen' +type mockencoding_encodedLen_Call struct { + *mock.Call +} + +// encodedLen is a helper method to define mock.On call +// - n int +func (_e *mockencoding_Expecter) encodedLen(n interface{}) *mockencoding_encodedLen_Call { + return &mockencoding_encodedLen_Call{Call: _e.mock.On("encodedLen", n)} +} + +func (_c *mockencoding_encodedLen_Call) Run(run func(n int)) *mockencoding_encodedLen_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 int + if args[0] != nil { + arg0 = args[0].(int) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *mockencoding_encodedLen_Call) Return(n1 int) *mockencoding_encodedLen_Call { + _c.Call.Return(n1) + return _c +} + +func (_c *mockencoding_encodedLen_Call) RunAndReturn(run func(n int) int) *mockencoding_encodedLen_Call { + _c.Call.Return(run) + return _c +} diff --git a/id/uuid.go b/id/uuid.go new file mode 100644 index 0000000..967723f --- /dev/null +++ b/id/uuid.go @@ -0,0 +1,90 @@ +package id + +import ( + eng "encoding" + "encoding/json" + "errors" +) + +const uuidSize = 16 + +type encoding interface { + // encode(dst, src []byte) + appendEncode(dst, src []byte) []byte + // encodeToString(src []byte) string + encodedLen(n int) int + // appendDecode(dst, src []byte) ([]byte, error) + // decodeString(s string) ([]byte, error) + decode(dst, src []byte) (n int, err error) + // decodedLen(n int) int +} + +type ID[UUID ~[uuidSize]byte, Enc encoding] struct { + Value UUID +} + +func (u ID[UUID, Enc]) IsZero() bool { + return u.Value == zeroUUID +} + +func (u ID[UUID, Enc]) MarshalJSON() ([]byte, error) { + var enc Enc + ln := enc.encodedLen(uuidSize) + b := make([]byte, 0, ln+2) + b = append(b, '"') + + b = enc.appendEncode(b, u.Value[:]) + + b = append(b, '"') + + return b, nil +} + +func (u ID[UUID, Enc]) AppendText(b []byte) ([]byte, error) { + var enc Enc + return enc.appendEncode(b, u.Value[:]), nil +} + +func (u ID[UUID, Enc]) MarshalText() ([]byte, error) { + return u.AppendText(nil) +} + +func (u *ID[UUID, Enc]) UnmarshalJSON(b []byte) error { + var s string + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + return u.UnmarshalText([]byte(s)) +} + +func (u *ID[UUID, Enc]) UnmarshalText(text []byte) error { + var enc Enc + dec := [uuidSize]byte{} + + n, err := enc.decode(dec[:], text) + if err != nil { + return err + } + + if n != uuidSize { + return ErrMalformedUUID + } + + u.Value = dec + + return nil +} + +var ( + _ json.Marshaler = (*ID[[uuidSize]byte, Base64])(nil) + _ eng.TextAppender = (*ID[[uuidSize]byte, Base64])(nil) + _ eng.TextMarshaler = (*ID[[uuidSize]byte, Base64])(nil) + _ json.Unmarshaler = (*ID[[uuidSize]byte, Base64])(nil) + _ eng.TextUnmarshaler = (*ID[[uuidSize]byte, Base64])(nil) +) + +var ErrMalformedUUID = errors.New("malformed uuid") + +var zeroUUID = [uuidSize]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} diff --git a/id/uuid_test.go b/id/uuid_test.go new file mode 100644 index 0000000..fdbccfe --- /dev/null +++ b/id/uuid_test.go @@ -0,0 +1,199 @@ +package id + +import ( + "encoding/json" + "testing" + + "github.com/ifnotnil/x/tst" + "github.com/stretchr/testify/assert" +) + +type id = [uuidSize]byte + +type unmarshalTest struct { + name string + input string + destination any + expected any + errorAsserter tst.ErrorAssertionFunc +} + +func (tc unmarshalTest) Test(t *testing.T) { + gotErr := json.Unmarshal([]byte(tc.input), tc.destination) + tc.errorAsserter(t, gotErr) + assert.Equal(t, tc.expected, tc.destination) +} + +func TestJSONUnmarshal(t *testing.T) { + type Foo[Enc encoding] struct { + ID ID[id, Enc] `json:"id"` + } + + type FooPointer[Enc encoding] struct { + ID *ID[id, Enc] `json:"id"` + } + + uuid := id{0x01, 0x9a, 0x26, 0x89, 0x44, 0x4a, 0x7c, 0x5e, 0x8c, 0x61, 0x07, 0x03, 0xbe, 0x31, 0x4c, 0xfc} + + unmarshalTests := []unmarshalTest{ + { + name: "Foo[Base64]", + input: `{"id":"AZomiURKfF6MYQcDvjFM_A"}`, + destination: &Foo[Base64]{ID: ID[id, Base64]{Value: zeroUUID}}, + expected: &Foo[Base64]{ID: ID[id, Base64]{Value: uuid}}, + errorAsserter: tst.NoError(), + }, + { + name: "FooPointer[Base64]", + input: `{"id":"AZomiURKfF6MYQcDvjFM_A"}`, + destination: &FooPointer[Base64]{ID: nil}, + expected: &FooPointer[Base64]{ID: &ID[id, Base64]{Value: uuid}}, + errorAsserter: tst.NoError(), + }, + { + name: "Foo[Base64WithPadding]", + input: `{"id":"AZomiURKfF6MYQcDvjFM_A~~"}`, + destination: &Foo[Base64WithPadding]{ID: ID[id, Base64WithPadding]{Value: zeroUUID}}, + expected: &Foo[Base64WithPadding]{ID: ID[id, Base64WithPadding]{Value: uuid}}, + errorAsserter: tst.NoError(), + }, + { + name: "FooPointer[Base64WithPadding]", + input: `{"id":"AZomiURKfF6MYQcDvjFM_A~~"}`, + destination: &FooPointer[Base64WithPadding]{ID: nil}, + expected: &FooPointer[Base64WithPadding]{ID: &ID[id, Base64WithPadding]{Value: uuid}}, + errorAsserter: tst.NoError(), + }, + { + name: "Foo[Base62]", + input: `{"id":"31reJxR0z0LERESS86rDe"}`, + destination: &Foo[Base62]{ID: ID[id, Base62]{Value: zeroUUID}}, + expected: &Foo[Base62]{ID: ID[id, Base62]{Value: uuid}}, + errorAsserter: tst.NoError(), + }, + { + name: "FooPointer[Base62]", + input: `{"id":"31reJxR0z0LERESS86rDe"}`, + destination: &FooPointer[Base62]{ID: nil}, + expected: &FooPointer[Base62]{ID: &ID[id, Base62]{Value: uuid}}, + errorAsserter: tst.NoError(), + }, + } + + for _, tc := range unmarshalTests { + t.Run(tc.name, tc.Test) + } +} + +type marshalTestCase struct { + name string + input any + expectedJSON string + errorAsserter tst.ErrorAssertionFunc +} + +func (tc marshalTestCase) Test(t *testing.T) { + got, gotErr := json.Marshal(tc.input) + tc.errorAsserter(t, gotErr) + assert.Equal(t, tc.expectedJSON, string(got)) +} + +func TestJSONMarshal(t *testing.T) { + type Foo[enc encoding] struct { + ID ID[id, enc] `json:"id"` + } + + type FooPointer[enc encoding] struct { + ID *ID[id, enc] `json:"id"` + } + + type FooOZ[enc encoding] struct { + ID ID[id, enc] `json:"id,omitzero"` + } + + type FooPointerOZ[enc encoding] struct { + ID *ID[id, enc] `json:"id,omitzero"` + } + + const jsBase64 = `{"id":"AZomiURKfF6MYQcDvjFM_A"}` + uuid := id{0x01, 0x9a, 0x26, 0x89, 0x44, 0x4a, 0x7c, 0x5e, 0x8c, 0x61, 0x07, 0x03, 0xbe, 0x31, 0x4c, 0xfc} + + marshalTests := []marshalTestCase{ + { + name: "FooOZ[Base64] zero", + input: FooOZ[Base64]{ID: ID[id, Base64]{Value: zeroUUID}}, + expectedJSON: `{}`, + errorAsserter: tst.NoError(), + }, + { + name: "*FooOZ[Base64] zero", + input: &FooOZ[Base64]{ID: ID[id, Base64]{Value: zeroUUID}}, + expectedJSON: `{}`, + errorAsserter: tst.NoError(), + }, + { + name: "FooPointerOZ[Base64] zero", + input: FooPointerOZ[Base64]{ID: &ID[id, Base64]{Value: zeroUUID}}, + expectedJSON: `{}`, + errorAsserter: tst.NoError(), + }, + { + name: "*FooPointerOZ[Base64] zero", + input: &FooPointerOZ[Base64]{ID: &ID[id, Base64]{Value: zeroUUID}}, + expectedJSON: `{}`, + errorAsserter: tst.NoError(), + }, + { + name: "FooPointerOZ[Base64] nil", + input: FooPointerOZ[Base64]{ID: nil}, + expectedJSON: `{}`, + errorAsserter: tst.NoError(), + }, + { + name: "*FooPointerOZ[Base64] nil", + input: &FooPointerOZ[Base64]{ID: nil}, + expectedJSON: `{}`, + errorAsserter: tst.NoError(), + }, + { + name: "Foo[Base64] uuid", + input: Foo[Base64]{ID: ID[id, Base64]{Value: uuid}}, + expectedJSON: jsBase64, + errorAsserter: tst.NoError(), + }, + { + name: "Foo[Base62] uuid", + input: Foo[Base62]{ID: ID[id, Base62]{Value: uuid}}, + expectedJSON: `{"id":"31reJxR0z0LERESS86rDe"}`, + errorAsserter: tst.NoError(), + }, + { + name: "*Foo[Base64] uuid", + input: &Foo[Base64]{ID: ID[id, Base64]{Value: uuid}}, + expectedJSON: jsBase64, + errorAsserter: tst.NoError(), + }, + { + name: "FooPointerOZ[Base64] uuid", + input: FooPointerOZ[Base64]{ID: &ID[id, Base64]{Value: uuid}}, + expectedJSON: jsBase64, + errorAsserter: tst.NoError(), + }, + { + name: "*FooPointerOZ[Base64] uuid", + input: &FooPointerOZ[Base64]{ID: &ID[id, Base64]{Value: uuid}}, + expectedJSON: jsBase64, + errorAsserter: tst.NoError(), + }, + { + name: "Foo[Base64WithPadding] uuid", + input: Foo[Base64WithPadding]{ID: ID[id, Base64WithPadding]{Value: uuid}}, + expectedJSON: `{"id":"AZomiURKfF6MYQcDvjFM_A~~"}`, + errorAsserter: tst.NoError(), + }, + } + + for _, tc := range marshalTests { + t.Run(tc.name, tc.Test) + } +} diff --git a/tools/gen.go b/tools/gen.go new file mode 100644 index 0000000..244eaac --- /dev/null +++ b/tools/gen.go @@ -0,0 +1,5 @@ +//go:build tools + +package tools + +