From ab4cdb437721e9acf03c97e27653d06cc6400826 Mon Sep 17 00:00:00 2001 From: Kostas Stamatakis Date: Wed, 29 Oct 2025 12:36:52 +0200 Subject: [PATCH 01/15] Add `id` module --- .github/dependabot.yml | 14 ++++ .github/workflows/sub_id.yml | 26 ++++++++ http/encoding/encoding.go | 11 ---- id/.mockery.yml | 23 +++++++ id/Makefile | 8 +++ id/README.md | 7 ++ id/go.mod | 14 ++++ id/go.sum | 14 ++++ id/uuid.go | 77 ++++++++++++++++++++++ id/uuid_test.go | 121 +++++++++++++++++++++++++++++++++++ 10 files changed, 304 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/sub_id.yml create mode 100644 id/.mockery.yml create mode 100644 id/Makefile create mode 100644 id/README.md create mode 100644 id/go.mod create mode 100644 id/go.sum create mode 100644 id/uuid.go create mode 100644 id/uuid_test.go 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/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..2a92899 --- /dev/null +++ b/id/README.md @@ -0,0 +1,7 @@ +# 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) + diff --git a/id/go.mod b/id/go.mod new file mode 100644 index 0000000..590029b --- /dev/null +++ b/id/go.mod @@ -0,0 +1,14 @@ +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 + 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/uuid.go b/id/uuid.go new file mode 100644 index 0000000..a6d3d67 --- /dev/null +++ b/id/uuid.go @@ -0,0 +1,77 @@ +package id + +import ( + "encoding/base64" + "encoding/json" + "errors" +) + +// 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: ~ + +// Base64 is a [base64.Encoding] based on [base64.URLEncoding] without padding character. +// alphabet of base64: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_ +var ( + Base64 = base64.URLEncoding.WithPadding(base64.NoPadding) + Base64WithPadding = base64.URLEncoding.WithPadding('~') +) + +var ( + base64UUIDEncodedLen = Base64.EncodedLen(uuidSize) + base64UUIDEncodedLenJSON = base64UUIDEncodedLen + 2 + zeroUUID = [uuidSize]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} +) + +const uuidSize = 16 + +type Base64UUID[U ~[uuidSize]byte] struct { + Value U +} + +func (u Base64UUID[U]) IsZero() bool { + return u.Value == zeroUUID +} + +func (u Base64UUID[U]) MarshalJSON() ([]byte, error) { + b := make([]byte, 1, base64UUIDEncodedLenJSON) + b[0] = '"' + sub := b[1:][:base64UUIDEncodedLen] + Base64.Encode(sub, u.Value[:]) + b = b[0 : base64UUIDEncodedLenJSON-1] + b = append(b, '"') + + return b, nil +} + +func (u *Base64UUID[U]) UnmarshalJSON(b []byte) error { + var s string + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + decBytes, err := Base64.DecodeString(s) + if err != nil { + return err + } + + if len(decBytes) != uuidSize { + return ErrMalformedUUID + } + + copy(u.Value[:], decBytes) + + return nil +} + +var ErrMalformedUUID = errors.New("malformed uuid") + +var ( + _ json.Marshaler = (*Base64UUID[[uuidSize]byte])(nil) + _ json.Unmarshaler = (*Base64UUID[[uuidSize]byte])(nil) +) diff --git a/id/uuid_test.go b/id/uuid_test.go new file mode 100644 index 0000000..d1e8a3c --- /dev/null +++ b/id/uuid_test.go @@ -0,0 +1,121 @@ +package id + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/ifnotnil/x/tst" + "github.com/stretchr/testify/assert" +) + +type id = [uuidSize]byte + +func TestUUID_JSON(t *testing.T) { + type Foo struct { + ID Base64UUID[id] `json:"id"` + } + + type FooPointer struct { + ID *Base64UUID[id] `json:"id"` + } + + type FooOZ struct { + ID Base64UUID[id] `json:"id,omitzero"` + } + + type FooPointerOZ struct { + ID *Base64UUID[id] `json:"id,omitzero"` + } + + const js = `{"id":"AZomiURKfF6MYQcDvjFM_A"}` + uuid := id{0x01, 0x9a, 0x26, 0x89, 0x44, 0x4a, 0x7c, 0x5e, 0x8c, 0x61, 0x07, 0x03, 0xbe, 0x31, 0x4c, 0xfc} + + unmarshalTests := []struct { + input string + destination any + expected any + errorAsserter tst.ErrorAssertionFunc + }{ + { + input: js, + destination: &Foo{ID: Base64UUID[id]{Value: zeroUUID}}, + expected: &Foo{ID: Base64UUID[id]{Value: uuid}}, + errorAsserter: tst.NoError(), + }, + { + input: js, + destination: &FooPointer{ID: nil}, + expected: &FooPointer{ID: &Base64UUID[id]{Value: uuid}}, + errorAsserter: tst.NoError(), + }, + } + + for i, tc := range unmarshalTests { + t.Run(fmt.Sprintf("unmarshal_%d", i), func(t *testing.T) { + gotErr := json.Unmarshal([]byte(tc.input), tc.destination) + tc.errorAsserter(t, gotErr) + assert.Equal(t, tc.expected, tc.destination) + }) + } + + marshalTests := []struct { + input any + expectedJSON string + errorAsserter tst.ErrorAssertionFunc + }{ + 0: { + input: Foo{ID: Base64UUID[id]{Value: uuid}}, + expectedJSON: js, + errorAsserter: tst.NoError(), + }, + 1: { + input: &Foo{ID: Base64UUID[id]{Value: uuid}}, + expectedJSON: js, + errorAsserter: tst.NoError(), + }, + 2: { + input: FooPointer{ID: &Base64UUID[id]{Value: uuid}}, + expectedJSON: js, + errorAsserter: tst.NoError(), + }, + 3: { + input: &FooPointer{ID: &Base64UUID[id]{Value: uuid}}, + expectedJSON: js, + errorAsserter: tst.NoError(), + }, + 4: { + input: FooOZ{ID: Base64UUID[id]{Value: zeroUUID}}, + expectedJSON: `{}`, + errorAsserter: tst.NoError(), + }, + 5: { + input: &FooOZ{ID: Base64UUID[id]{Value: zeroUUID}}, + expectedJSON: `{}`, + errorAsserter: tst.NoError(), + }, + 6: { + input: FooPointerOZ{ID: &Base64UUID[id]{Value: zeroUUID}}, + expectedJSON: `{}`, + errorAsserter: tst.NoError(), + }, + 7: { + input: &FooPointerOZ{ID: &Base64UUID[id]{Value: zeroUUID}}, + expectedJSON: `{}`, + errorAsserter: tst.NoError(), + }, + 8: { + input: &FooPointerOZ{ID: nil}, + expectedJSON: `{}`, + errorAsserter: tst.NoError(), + }, + } + + for i, tc := range marshalTests { + t.Run(fmt.Sprintf("marshal_%d", i), func(t *testing.T) { + got, gotErr := json.Marshal(tc.input) + tc.errorAsserter(t, gotErr) + assert.Equal(t, tc.expectedJSON, string(got)) + }) + } +} From dcecb7e55c8fc7047f95067b9a91c30978a38f3d Mon Sep 17 00:00:00 2001 From: Kostas Stamatakis Date: Wed, 29 Oct 2025 12:50:00 +0200 Subject: [PATCH 02/15] refactor main readme --- README.md | 37 +++++-------------------------------- 1 file changed, 5 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index e773704..81fd797 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=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=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=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=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. | -## 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` From b2c6d7d8ad2b5c2b11a5a43ca2a857a1cefed3dd Mon Sep 17 00:00:00 2001 From: Kostas Stamatakis Date: Wed, 29 Oct 2025 12:50:40 +0200 Subject: [PATCH 03/15] refactor main readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 81fd797..70367b5 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ https://docs.codecov.com/docs/status-badges | 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=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=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=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=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. | +| `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=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=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=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=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. | From 3f6dc1d09813e0814022b6bdfb9acc9586cc3648 Mon Sep 17 00:00:00 2001 From: Kostas Stamatakis Date: Wed, 29 Oct 2025 12:51:38 +0200 Subject: [PATCH 04/15] refactor main readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 70367b5..44c1a00 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ https://docs.codecov.com/docs/status-badges | 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=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=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=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=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. | +| `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?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`

[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/http?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. | From 08b3d23e0b582861d81adabaf33fc55059fbb2e2 Mon Sep 17 00:00:00 2001 From: Kostas Stamatakis Date: Wed, 29 Oct 2025 13:14:05 +0200 Subject: [PATCH 05/15] config --- README.md | 4 ++-- codecov.yml | 4 ++++ tools/gen.go | 5 +++++ 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 tools/gen.go diff --git a/README.md b/README.md index 44c1a00..5d432a6 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ https://docs.codecov.com/docs/status-badges | 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?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`

[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/http?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. | +| `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. | 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/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 + + From 911853ea9805bd68bc2d78beeee7899ec6c63fc5 Mon Sep 17 00:00:00 2001 From: Kostas Stamatakis Date: Wed, 29 Oct 2025 13:16:11 +0200 Subject: [PATCH 06/15] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5d432a6..8996176 100644 --- a/README.md +++ b/README.md @@ -11,5 +11,5 @@ https://docs.codecov.com/docs/status-badges | `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. | From 544ab953468e3ec1ef368575ec6ac20db6ea09ca Mon Sep 17 00:00:00 2001 From: Kostas Stamatakis Date: Wed, 29 Oct 2025 13:18:28 +0200 Subject: [PATCH 07/15] readme --- id/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/id/README.md b/id/README.md index 2a92899..03de4df 100644 --- a/id/README.md +++ b/id/README.md @@ -5,3 +5,6 @@ [![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. From 0a19d5c1787b220a4e459861caa37c984e1dfe95 Mon Sep 17 00:00:00 2001 From: Kostas Stamatakis Date: Wed, 29 Oct 2025 13:32:02 +0200 Subject: [PATCH 08/15] add encoding.Text* api --- id/uuid.go | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/id/uuid.go b/id/uuid.go index a6d3d67..015117f 100644 --- a/id/uuid.go +++ b/id/uuid.go @@ -1,9 +1,11 @@ package id import ( + "encoding" "encoding/base64" "encoding/json" "errors" + "slices" ) // 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): @@ -55,16 +57,33 @@ func (u *Base64UUID[U]) UnmarshalJSON(b []byte) error { return err } - decBytes, err := Base64.DecodeString(s) + return u.UnmarshalText([]byte(s)) +} + +func (u Base64UUID[U]) AppendText(b []byte) ([]byte, error) { + b = slices.Grow(b, base64UUIDEncodedLen) + b = b[:len(b)+base64UUIDEncodedLen] + Base64.Encode(b, u.Value[:]) + return b, nil +} + +func (u Base64UUID[U]) MarshalText() ([]byte, error) { + return u.AppendText(nil) +} + +func (u *Base64UUID[U]) UnmarshalText(text []byte) error { + dec := [uuidSize]byte{} + + n, err := Base64.Decode(dec[:], text) if err != nil { return err } - if len(decBytes) != uuidSize { + if n != uuidSize { return ErrMalformedUUID } - copy(u.Value[:], decBytes) + u.Value = dec return nil } @@ -72,6 +91,9 @@ func (u *Base64UUID[U]) UnmarshalJSON(b []byte) error { var ErrMalformedUUID = errors.New("malformed uuid") var ( - _ json.Marshaler = (*Base64UUID[[uuidSize]byte])(nil) - _ json.Unmarshaler = (*Base64UUID[[uuidSize]byte])(nil) + _ json.Marshaler = (*Base64UUID[[uuidSize]byte])(nil) + _ json.Unmarshaler = (*Base64UUID[[uuidSize]byte])(nil) + _ encoding.TextAppender = (*Base64UUID[[uuidSize]byte])(nil) + _ encoding.TextMarshaler = (*Base64UUID[[uuidSize]byte])(nil) + _ encoding.TextUnmarshaler = (*Base64UUID[[uuidSize]byte])(nil) ) From 3f5394b2c478d50e419493dc7135a1c79aa97b3b Mon Sep 17 00:00:00 2001 From: Kostas Stamatakis Date: Wed, 29 Oct 2025 13:45:52 +0200 Subject: [PATCH 09/15] reuse --- id/uuid.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/id/uuid.go b/id/uuid.go index 015117f..b8dd4c2 100644 --- a/id/uuid.go +++ b/id/uuid.go @@ -42,9 +42,11 @@ func (u Base64UUID[U]) IsZero() bool { func (u Base64UUID[U]) MarshalJSON() ([]byte, error) { b := make([]byte, 1, base64UUIDEncodedLenJSON) b[0] = '"' - sub := b[1:][:base64UUIDEncodedLen] - Base64.Encode(sub, u.Value[:]) - b = b[0 : base64UUIDEncodedLenJSON-1] + if sub, err := u.AppendText(b[1:]); err != nil { + return nil, err + } else { + b = b[:len(sub)+1] + } b = append(b, '"') return b, nil From b7a8cc9045fc5ee22c00ac4b44671a69b73cc0ae Mon Sep 17 00:00:00 2001 From: Kostas Stamatakis Date: Wed, 29 Oct 2025 13:47:20 +0200 Subject: [PATCH 10/15] comments --- id/uuid.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/id/uuid.go b/id/uuid.go index b8dd4c2..18a4797 100644 --- a/id/uuid.go +++ b/id/uuid.go @@ -45,7 +45,7 @@ func (u Base64UUID[U]) MarshalJSON() ([]byte, error) { if sub, err := u.AppendText(b[1:]); err != nil { return nil, err } else { - b = b[:len(sub)+1] + b = b[:len(sub)+1] // the appended text plus the " character. } b = append(b, '"') @@ -64,7 +64,7 @@ func (u *Base64UUID[U]) UnmarshalJSON(b []byte) error { func (u Base64UUID[U]) AppendText(b []byte) ([]byte, error) { b = slices.Grow(b, base64UUIDEncodedLen) - b = b[:len(b)+base64UUIDEncodedLen] + b = b[:len(b)+base64UUIDEncodedLen] // safe since we already grew the buffer. Base64.Encode(b, u.Value[:]) return b, nil } From 39ce262ca47271970fa97219db747538208f06bf Mon Sep 17 00:00:00 2001 From: Kostas Stamatakis Date: Tue, 11 Nov 2025 22:56:20 +0200 Subject: [PATCH 11/15] dynamic encoding --- id/base62.go | 39 +++++++++++++++ id/base64.go | 93 +++++++++++++++++++++++++++++++++++ id/base64_test.go | 121 ++++++++++++++++++++++++++++++++++++++++++++++ id/encodings.go | 1 + id/uuid.go | 99 ++++++++++++++++++------------------- id/uuid_test.go | 120 --------------------------------------------- 6 files changed, 300 insertions(+), 173 deletions(-) create mode 100644 id/base62.go create mode 100644 id/base64.go create mode 100644 id/base64_test.go create mode 100644 id/encodings.go diff --git a/id/base62.go b/id/base62.go new file mode 100644 index 0000000..fd377fc --- /dev/null +++ b/id/base62.go @@ -0,0 +1,39 @@ +package id + +import ( + "fmt" + "math" + "math/big" +) + +type Base62Encoding struct{} + +func (e *Base62Encoding) AppendEncode(dst, src []byte) []byte { + if len(src) == 0 { + return nil + } + + num := big.Int{} + num.SetBytes(src) + return num.Append(dst, 62) +} + +func base62Decode(b []byte) ([]byte, error) { + num := big.Int{} + n, ok := num.SetString(string(b), 62) + if !ok { + return nil, fmt.Errorf("tbd") + } + + return n.Bytes(), nil +} + +func base62EncodedLen(n int) int { + // log2(62) ≈ 5.954196310386875 + return int(math.Ceil(float64(n) * 8 / 5.954196310386875)) +} + +func base62DecodedLen(m int) int { + // log2(62) ≈ 5.954196310386875 + return int(math.Floor(float64(m) * 5.954196310386875 / 8)) +} diff --git a/id/base64.go b/id/base64.go new file mode 100644 index 0000000..13b6938 --- /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/base64_test.go b/id/base64_test.go new file mode 100644 index 0000000..fd7353a --- /dev/null +++ b/id/base64_test.go @@ -0,0 +1,121 @@ +package id + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/ifnotnil/x/tst" + "github.com/stretchr/testify/assert" +) + +type id = [uuidSize]byte + +func TestUUID_JSON(t *testing.T) { + type Foo struct { + ID ID[id, Base64] `json:"id"` + } + + type FooPointer struct { + ID *ID[id, Base64] `json:"id"` + } + + type FooOZ struct { + ID ID[id, Base64] `json:"id,omitzero"` + } + + type FooPointerOZ struct { + ID *ID[id, Base64] `json:"id,omitzero"` + } + + const js = `{"id":"AZomiURKfF6MYQcDvjFM_A"}` + uuid := id{0x01, 0x9a, 0x26, 0x89, 0x44, 0x4a, 0x7c, 0x5e, 0x8c, 0x61, 0x07, 0x03, 0xbe, 0x31, 0x4c, 0xfc} + + unmarshalTests := []struct { + input string + destination any + expected any + errorAsserter tst.ErrorAssertionFunc + }{ + { + input: js, + destination: &Foo{ID: ID[id, Base64]{Value: zeroUUID}}, + expected: &Foo{ID: ID[id, Base64]{Value: uuid}}, + errorAsserter: tst.NoError(), + }, + { + input: js, + destination: &FooPointer{ID: nil}, + expected: &FooPointer{ID: &ID[id, Base64]{Value: uuid}}, + errorAsserter: tst.NoError(), + }, + } + + for i, tc := range unmarshalTests { + t.Run(fmt.Sprintf("unmarshal_%d", i), func(t *testing.T) { + gotErr := json.Unmarshal([]byte(tc.input), tc.destination) + tc.errorAsserter(t, gotErr) + assert.Equal(t, tc.expected, tc.destination) + }) + } + + marshalTests := []struct { + input any + expectedJSON string + errorAsserter tst.ErrorAssertionFunc + }{ + 0: { + input: Foo{ID: ID[id, Base64]{Value: uuid}}, + expectedJSON: js, + errorAsserter: tst.NoError(), + }, + 1: { + input: &Foo{ID: ID[id, Base64]{Value: uuid}}, + expectedJSON: js, + errorAsserter: tst.NoError(), + }, + 2: { + input: FooPointer{ID: &ID[id, Base64]{Value: uuid}}, + expectedJSON: js, + errorAsserter: tst.NoError(), + }, + 3: { + input: &FooPointer{ID: &ID[id, Base64]{Value: uuid}}, + expectedJSON: js, + errorAsserter: tst.NoError(), + }, + 4: { + input: FooOZ{ID: ID[id, Base64]{Value: zeroUUID}}, + expectedJSON: `{}`, + errorAsserter: tst.NoError(), + }, + 5: { + input: &FooOZ{ID: ID[id, Base64]{Value: zeroUUID}}, + expectedJSON: `{}`, + errorAsserter: tst.NoError(), + }, + 6: { + input: FooPointerOZ{ID: &ID[id, Base64]{Value: zeroUUID}}, + expectedJSON: `{}`, + errorAsserter: tst.NoError(), + }, + 7: { + input: &FooPointerOZ{ID: &ID[id, Base64]{Value: zeroUUID}}, + expectedJSON: `{}`, + errorAsserter: tst.NoError(), + }, + 8: { + input: &FooPointerOZ{ID: nil}, + expectedJSON: `{}`, + errorAsserter: tst.NoError(), + }, + } + + for i, tc := range marshalTests { + t.Run(fmt.Sprintf("marshal_%d", i), func(t *testing.T) { + got, gotErr := json.Marshal(tc.input) + tc.errorAsserter(t, gotErr) + assert.Equal(t, tc.expectedJSON, string(got)) + }) + } +} diff --git a/id/encodings.go b/id/encodings.go new file mode 100644 index 0000000..51e9eb9 --- /dev/null +++ b/id/encodings.go @@ -0,0 +1 @@ +package id diff --git a/id/uuid.go b/id/uuid.go index 18a4797..e37e283 100644 --- a/id/uuid.go +++ b/id/uuid.go @@ -2,57 +2,58 @@ package id import ( "encoding" - "encoding/base64" "encoding/json" "errors" "slices" ) -// 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: ~ - -// Base64 is a [base64.Encoding] based on [base64.URLEncoding] without padding character. -// alphabet of base64: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_ -var ( - Base64 = base64.URLEncoding.WithPadding(base64.NoPadding) - Base64WithPadding = base64.URLEncoding.WithPadding('~') -) - -var ( - base64UUIDEncodedLen = Base64.EncodedLen(uuidSize) - base64UUIDEncodedLenJSON = base64UUIDEncodedLen + 2 - zeroUUID = [uuidSize]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} -) - const uuidSize = 16 -type Base64UUID[U ~[uuidSize]byte] struct { - Value U +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 } -func (u Base64UUID[U]) IsZero() bool { +type ID[UUID ~[uuidSize]byte, Enc Encoding] struct { + Value UUID +} + +func (u ID[UUID, Enc]) IsZero() bool { return u.Value == zeroUUID } -func (u Base64UUID[U]) MarshalJSON() ([]byte, error) { - b := make([]byte, 1, base64UUIDEncodedLenJSON) +func (u ID[UUID, Enc]) MarshalJSON() ([]byte, error) { + var enc Enc + ln := enc.encodedLen(uuidSize) + b := make([]byte, 1, ln+2) b[0] = '"' - if sub, err := u.AppendText(b[1:]); err != nil { - return nil, err - } else { - b = b[:len(sub)+1] // the appended text plus the " character. - } + + b = enc.appendEncode(b, u.Value[:]) + b = append(b, '"') return b, nil } -func (u *Base64UUID[U]) UnmarshalJSON(b []byte) error { +func (u ID[UUID, Enc]) AppendText(b []byte) ([]byte, error) { + var enc Enc + ln := enc.encodedLen(uuidSize) + + b = slices.Grow(b, ln) + 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 { @@ -62,21 +63,11 @@ func (u *Base64UUID[U]) UnmarshalJSON(b []byte) error { return u.UnmarshalText([]byte(s)) } -func (u Base64UUID[U]) AppendText(b []byte) ([]byte, error) { - b = slices.Grow(b, base64UUIDEncodedLen) - b = b[:len(b)+base64UUIDEncodedLen] // safe since we already grew the buffer. - Base64.Encode(b, u.Value[:]) - return b, nil -} - -func (u Base64UUID[U]) MarshalText() ([]byte, error) { - return u.AppendText(nil) -} - -func (u *Base64UUID[U]) UnmarshalText(text []byte) error { +func (u *ID[UUID, Enc]) UnmarshalText(text []byte) error { + var enc Enc dec := [uuidSize]byte{} - n, err := Base64.Decode(dec[:], text) + n, err := enc.decode(dec[:], text) if err != nil { return err } @@ -90,12 +81,14 @@ func (u *Base64UUID[U]) UnmarshalText(text []byte) error { return nil } -var ErrMalformedUUID = errors.New("malformed uuid") - var ( - _ json.Marshaler = (*Base64UUID[[uuidSize]byte])(nil) - _ json.Unmarshaler = (*Base64UUID[[uuidSize]byte])(nil) - _ encoding.TextAppender = (*Base64UUID[[uuidSize]byte])(nil) - _ encoding.TextMarshaler = (*Base64UUID[[uuidSize]byte])(nil) - _ encoding.TextUnmarshaler = (*Base64UUID[[uuidSize]byte])(nil) + _ json.Marshaler = (*ID[[uuidSize]byte, Base64])(nil) + _ json.Unmarshaler = (*ID[[uuidSize]byte, Base64])(nil) + _ encoding.TextAppender = (*ID[[uuidSize]byte, Base64])(nil) + _ encoding.TextMarshaler = (*ID[[uuidSize]byte, Base64])(nil) + _ encoding.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 index d1e8a3c..51e9eb9 100644 --- a/id/uuid_test.go +++ b/id/uuid_test.go @@ -1,121 +1 @@ package id - -import ( - "encoding/json" - "fmt" - "testing" - - "github.com/ifnotnil/x/tst" - "github.com/stretchr/testify/assert" -) - -type id = [uuidSize]byte - -func TestUUID_JSON(t *testing.T) { - type Foo struct { - ID Base64UUID[id] `json:"id"` - } - - type FooPointer struct { - ID *Base64UUID[id] `json:"id"` - } - - type FooOZ struct { - ID Base64UUID[id] `json:"id,omitzero"` - } - - type FooPointerOZ struct { - ID *Base64UUID[id] `json:"id,omitzero"` - } - - const js = `{"id":"AZomiURKfF6MYQcDvjFM_A"}` - uuid := id{0x01, 0x9a, 0x26, 0x89, 0x44, 0x4a, 0x7c, 0x5e, 0x8c, 0x61, 0x07, 0x03, 0xbe, 0x31, 0x4c, 0xfc} - - unmarshalTests := []struct { - input string - destination any - expected any - errorAsserter tst.ErrorAssertionFunc - }{ - { - input: js, - destination: &Foo{ID: Base64UUID[id]{Value: zeroUUID}}, - expected: &Foo{ID: Base64UUID[id]{Value: uuid}}, - errorAsserter: tst.NoError(), - }, - { - input: js, - destination: &FooPointer{ID: nil}, - expected: &FooPointer{ID: &Base64UUID[id]{Value: uuid}}, - errorAsserter: tst.NoError(), - }, - } - - for i, tc := range unmarshalTests { - t.Run(fmt.Sprintf("unmarshal_%d", i), func(t *testing.T) { - gotErr := json.Unmarshal([]byte(tc.input), tc.destination) - tc.errorAsserter(t, gotErr) - assert.Equal(t, tc.expected, tc.destination) - }) - } - - marshalTests := []struct { - input any - expectedJSON string - errorAsserter tst.ErrorAssertionFunc - }{ - 0: { - input: Foo{ID: Base64UUID[id]{Value: uuid}}, - expectedJSON: js, - errorAsserter: tst.NoError(), - }, - 1: { - input: &Foo{ID: Base64UUID[id]{Value: uuid}}, - expectedJSON: js, - errorAsserter: tst.NoError(), - }, - 2: { - input: FooPointer{ID: &Base64UUID[id]{Value: uuid}}, - expectedJSON: js, - errorAsserter: tst.NoError(), - }, - 3: { - input: &FooPointer{ID: &Base64UUID[id]{Value: uuid}}, - expectedJSON: js, - errorAsserter: tst.NoError(), - }, - 4: { - input: FooOZ{ID: Base64UUID[id]{Value: zeroUUID}}, - expectedJSON: `{}`, - errorAsserter: tst.NoError(), - }, - 5: { - input: &FooOZ{ID: Base64UUID[id]{Value: zeroUUID}}, - expectedJSON: `{}`, - errorAsserter: tst.NoError(), - }, - 6: { - input: FooPointerOZ{ID: &Base64UUID[id]{Value: zeroUUID}}, - expectedJSON: `{}`, - errorAsserter: tst.NoError(), - }, - 7: { - input: &FooPointerOZ{ID: &Base64UUID[id]{Value: zeroUUID}}, - expectedJSON: `{}`, - errorAsserter: tst.NoError(), - }, - 8: { - input: &FooPointerOZ{ID: nil}, - expectedJSON: `{}`, - errorAsserter: tst.NoError(), - }, - } - - for i, tc := range marshalTests { - t.Run(fmt.Sprintf("marshal_%d", i), func(t *testing.T) { - got, gotErr := json.Marshal(tc.input) - tc.errorAsserter(t, gotErr) - assert.Equal(t, tc.expectedJSON, string(got)) - }) - } -} From 7d9439c63cf11553baf1470d0b53a52fe16ea7c8 Mon Sep 17 00:00:00 2001 From: Kostas Stamatakis Date: Wed, 19 Nov 2025 21:27:30 +0200 Subject: [PATCH 12/15] move tests --- id/base64_test.go | 121 ----------------------------------------- id/uuid.go | 10 ++-- id/uuid_test.go | 133 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 128 deletions(-) delete mode 100644 id/base64_test.go diff --git a/id/base64_test.go b/id/base64_test.go deleted file mode 100644 index fd7353a..0000000 --- a/id/base64_test.go +++ /dev/null @@ -1,121 +0,0 @@ -package id - -import ( - "encoding/json" - "fmt" - "testing" - - "github.com/ifnotnil/x/tst" - "github.com/stretchr/testify/assert" -) - -type id = [uuidSize]byte - -func TestUUID_JSON(t *testing.T) { - type Foo struct { - ID ID[id, Base64] `json:"id"` - } - - type FooPointer struct { - ID *ID[id, Base64] `json:"id"` - } - - type FooOZ struct { - ID ID[id, Base64] `json:"id,omitzero"` - } - - type FooPointerOZ struct { - ID *ID[id, Base64] `json:"id,omitzero"` - } - - const js = `{"id":"AZomiURKfF6MYQcDvjFM_A"}` - uuid := id{0x01, 0x9a, 0x26, 0x89, 0x44, 0x4a, 0x7c, 0x5e, 0x8c, 0x61, 0x07, 0x03, 0xbe, 0x31, 0x4c, 0xfc} - - unmarshalTests := []struct { - input string - destination any - expected any - errorAsserter tst.ErrorAssertionFunc - }{ - { - input: js, - destination: &Foo{ID: ID[id, Base64]{Value: zeroUUID}}, - expected: &Foo{ID: ID[id, Base64]{Value: uuid}}, - errorAsserter: tst.NoError(), - }, - { - input: js, - destination: &FooPointer{ID: nil}, - expected: &FooPointer{ID: &ID[id, Base64]{Value: uuid}}, - errorAsserter: tst.NoError(), - }, - } - - for i, tc := range unmarshalTests { - t.Run(fmt.Sprintf("unmarshal_%d", i), func(t *testing.T) { - gotErr := json.Unmarshal([]byte(tc.input), tc.destination) - tc.errorAsserter(t, gotErr) - assert.Equal(t, tc.expected, tc.destination) - }) - } - - marshalTests := []struct { - input any - expectedJSON string - errorAsserter tst.ErrorAssertionFunc - }{ - 0: { - input: Foo{ID: ID[id, Base64]{Value: uuid}}, - expectedJSON: js, - errorAsserter: tst.NoError(), - }, - 1: { - input: &Foo{ID: ID[id, Base64]{Value: uuid}}, - expectedJSON: js, - errorAsserter: tst.NoError(), - }, - 2: { - input: FooPointer{ID: &ID[id, Base64]{Value: uuid}}, - expectedJSON: js, - errorAsserter: tst.NoError(), - }, - 3: { - input: &FooPointer{ID: &ID[id, Base64]{Value: uuid}}, - expectedJSON: js, - errorAsserter: tst.NoError(), - }, - 4: { - input: FooOZ{ID: ID[id, Base64]{Value: zeroUUID}}, - expectedJSON: `{}`, - errorAsserter: tst.NoError(), - }, - 5: { - input: &FooOZ{ID: ID[id, Base64]{Value: zeroUUID}}, - expectedJSON: `{}`, - errorAsserter: tst.NoError(), - }, - 6: { - input: FooPointerOZ{ID: &ID[id, Base64]{Value: zeroUUID}}, - expectedJSON: `{}`, - errorAsserter: tst.NoError(), - }, - 7: { - input: &FooPointerOZ{ID: &ID[id, Base64]{Value: zeroUUID}}, - expectedJSON: `{}`, - errorAsserter: tst.NoError(), - }, - 8: { - input: &FooPointerOZ{ID: nil}, - expectedJSON: `{}`, - errorAsserter: tst.NoError(), - }, - } - - for i, tc := range marshalTests { - t.Run(fmt.Sprintf("marshal_%d", i), func(t *testing.T) { - got, gotErr := json.Marshal(tc.input) - tc.errorAsserter(t, gotErr) - assert.Equal(t, tc.expectedJSON, string(got)) - }) - } -} diff --git a/id/uuid.go b/id/uuid.go index e37e283..37576d3 100644 --- a/id/uuid.go +++ b/id/uuid.go @@ -4,13 +4,12 @@ import ( "encoding" "encoding/json" "errors" - "slices" ) const uuidSize = 16 type Encoding interface { - // encode(dst, src []byte) + encode(dst, src []byte) appendEncode(dst, src []byte) []byte // encodeToString(src []byte) string encodedLen(n int) int @@ -31,8 +30,8 @@ func (u ID[UUID, Enc]) IsZero() bool { func (u ID[UUID, Enc]) MarshalJSON() ([]byte, error) { var enc Enc ln := enc.encodedLen(uuidSize) - b := make([]byte, 1, ln+2) - b[0] = '"' + b := make([]byte, 0, ln+2) + b = append(b, '"') b = enc.appendEncode(b, u.Value[:]) @@ -43,9 +42,6 @@ func (u ID[UUID, Enc]) MarshalJSON() ([]byte, error) { func (u ID[UUID, Enc]) AppendText(b []byte) ([]byte, error) { var enc Enc - ln := enc.encodedLen(uuidSize) - - b = slices.Grow(b, ln) return enc.appendEncode(b, u.Value[:]), nil } diff --git a/id/uuid_test.go b/id/uuid_test.go index 51e9eb9..d48ad29 100644 --- a/id/uuid_test.go +++ b/id/uuid_test.go @@ -1 +1,134 @@ package id + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/ifnotnil/x/tst" + "github.com/stretchr/testify/assert" +) + +type id = [uuidSize]byte + +func TestJSONUnmarshal(t *testing.T) { + type Foo struct { + ID ID[id, Base64] `json:"id"` + } + + type FooPointer struct { + ID *ID[id, Base64] `json:"id"` + } + + const js = `{"id":"AZomiURKfF6MYQcDvjFM_A"}` + uuid := id{0x01, 0x9a, 0x26, 0x89, 0x44, 0x4a, 0x7c, 0x5e, 0x8c, 0x61, 0x07, 0x03, 0xbe, 0x31, 0x4c, 0xfc} + + unmarshalTests := []struct { + input string + destination any + expected any + errorAsserter tst.ErrorAssertionFunc + }{ + { + input: js, + destination: &Foo{ID: ID[id, Base64]{Value: zeroUUID}}, + expected: &Foo{ID: ID[id, Base64]{Value: uuid}}, + errorAsserter: tst.NoError(), + }, + { + input: js, + destination: &FooPointer{ID: nil}, + expected: &FooPointer{ID: &ID[id, Base64]{Value: uuid}}, + errorAsserter: tst.NoError(), + }, + } + + for i, tc := range unmarshalTests { + t.Run(fmt.Sprintf("unmarshal_%d", i), func(t *testing.T) { + gotErr := json.Unmarshal([]byte(tc.input), tc.destination) + tc.errorAsserter(t, gotErr) + assert.Equal(t, tc.expected, tc.destination) + }) + } +} + +func TestJSONMarshal(t *testing.T) { + type Foo struct { + ID ID[id, Base64] `json:"id"` + } + + type FooPointer struct { + ID *ID[id, Base64] `json:"id"` + } + + type FooOZ struct { + ID ID[id, Base64] `json:"id,omitzero"` + } + + type FooPointerOZ struct { + ID *ID[id, Base64] `json:"id,omitzero"` + } + + const js = `{"id":"AZomiURKfF6MYQcDvjFM_A"}` + uuid := id{0x01, 0x9a, 0x26, 0x89, 0x44, 0x4a, 0x7c, 0x5e, 0x8c, 0x61, 0x07, 0x03, 0xbe, 0x31, 0x4c, 0xfc} + + marshalTests := []struct { + input any + expectedJSON string + errorAsserter tst.ErrorAssertionFunc + }{ + 0: { + input: Foo{ID: ID[id, Base64]{Value: uuid}}, + expectedJSON: js, + errorAsserter: tst.NoError(), + }, + 1: { + input: &Foo{ID: ID[id, Base64]{Value: uuid}}, + expectedJSON: js, + errorAsserter: tst.NoError(), + }, + 2: { + input: FooPointer{ID: &ID[id, Base64]{Value: uuid}}, + expectedJSON: js, + errorAsserter: tst.NoError(), + }, + 3: { + input: &FooPointer{ID: &ID[id, Base64]{Value: uuid}}, + expectedJSON: js, + errorAsserter: tst.NoError(), + }, + 4: { + input: FooOZ{ID: ID[id, Base64]{Value: zeroUUID}}, + expectedJSON: `{}`, + errorAsserter: tst.NoError(), + }, + 5: { + input: &FooOZ{ID: ID[id, Base64]{Value: zeroUUID}}, + expectedJSON: `{}`, + errorAsserter: tst.NoError(), + }, + 6: { + input: FooPointerOZ{ID: &ID[id, Base64]{Value: zeroUUID}}, + expectedJSON: `{}`, + errorAsserter: tst.NoError(), + }, + 7: { + input: &FooPointerOZ{ID: &ID[id, Base64]{Value: zeroUUID}}, + expectedJSON: `{}`, + errorAsserter: tst.NoError(), + }, + 8: { + input: &FooPointerOZ{ID: nil}, + expectedJSON: `{}`, + errorAsserter: tst.NoError(), + }, + } + + for i, tc := range marshalTests { + t.Run(fmt.Sprintf("marshal_%d", i), func(t *testing.T) { + got, gotErr := json.Marshal(tc.input) + tc.errorAsserter(t, gotErr) + assert.Equal(t, tc.expectedJSON, string(got)) + }) + } +} From 35f29bd4315e9eccf0ee8fb36d27da9e26faf11c Mon Sep 17 00:00:00 2001 From: Kostas Stamatakis Date: Thu, 20 Nov 2025 19:11:39 +0200 Subject: [PATCH 13/15] tests --- id/base64.go | 4 +- id/encodings.go | 1 - id/uuid.go | 16 ++--- id/uuid_test.go | 183 ++++++++++++++++++++++++++++++------------------ 4 files changed, 124 insertions(+), 80 deletions(-) delete mode 100644 id/encodings.go diff --git a/id/base64.go b/id/base64.go index 13b6938..1fcf1f6 100644 --- a/id/base64.go +++ b/id/base64.go @@ -88,6 +88,6 @@ func (b Base64WithPadding) decodedLen(n int) int { } var ( - _ Encoding = Base64{} - _ Encoding = Base64WithPadding{} + _ encoding = Base64{} + _ encoding = Base64WithPadding{} ) diff --git a/id/encodings.go b/id/encodings.go deleted file mode 100644 index 51e9eb9..0000000 --- a/id/encodings.go +++ /dev/null @@ -1 +0,0 @@ -package id diff --git a/id/uuid.go b/id/uuid.go index 37576d3..5142485 100644 --- a/id/uuid.go +++ b/id/uuid.go @@ -1,14 +1,14 @@ package id import ( - "encoding" + eng "encoding" "encoding/json" "errors" ) const uuidSize = 16 -type Encoding interface { +type encoding interface { encode(dst, src []byte) appendEncode(dst, src []byte) []byte // encodeToString(src []byte) string @@ -19,7 +19,7 @@ type Encoding interface { decodedLen(n int) int } -type ID[UUID ~[uuidSize]byte, Enc Encoding] struct { +type ID[UUID ~[uuidSize]byte, Enc encoding] struct { Value UUID } @@ -78,11 +78,11 @@ func (u *ID[UUID, Enc]) UnmarshalText(text []byte) error { } var ( - _ json.Marshaler = (*ID[[uuidSize]byte, Base64])(nil) - _ json.Unmarshaler = (*ID[[uuidSize]byte, Base64])(nil) - _ encoding.TextAppender = (*ID[[uuidSize]byte, Base64])(nil) - _ encoding.TextMarshaler = (*ID[[uuidSize]byte, Base64])(nil) - _ encoding.TextUnmarshaler = (*ID[[uuidSize]byte, Base64])(nil) + _ 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") diff --git a/id/uuid_test.go b/id/uuid_test.go index d48ad29..ac5c8d5 100644 --- a/id/uuid_test.go +++ b/id/uuid_test.go @@ -2,7 +2,6 @@ package id import ( "encoding/json" - "fmt" "testing" "github.com/ifnotnil/x/tst" @@ -11,124 +10,170 @@ import ( 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 struct { - ID ID[id, Base64] `json:"id"` + type Foo[Enc encoding] struct { + ID ID[id, Enc] `json:"id"` } - type FooPointer struct { - ID *ID[id, Base64] `json:"id"` + type FooPointer[Enc encoding] struct { + ID *ID[id, Enc] `json:"id"` } - const js = `{"id":"AZomiURKfF6MYQcDvjFM_A"}` uuid := id{0x01, 0x9a, 0x26, 0x89, 0x44, 0x4a, 0x7c, 0x5e, 0x8c, 0x61, 0x07, 0x03, 0xbe, 0x31, 0x4c, 0xfc} - unmarshalTests := []struct { - input string - destination any - expected any - errorAsserter tst.ErrorAssertionFunc - }{ + 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(), + }, { - input: js, - destination: &Foo{ID: ID[id, Base64]{Value: zeroUUID}}, - expected: &Foo{ID: ID[id, Base64]{Value: uuid}}, + 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(), }, { - input: js, - destination: &FooPointer{ID: nil}, - expected: &FooPointer{ID: &ID[id, Base64]{Value: uuid}}, + name: "FooPointer[Base64WithPadding]", + input: `{"id":"AZomiURKfF6MYQcDvjFM_A~~"}`, + destination: &FooPointer[Base64WithPadding]{ID: nil}, + expected: &FooPointer[Base64WithPadding]{ID: &ID[id, Base64WithPadding]{Value: uuid}}, errorAsserter: tst.NoError(), }, } - for i, tc := range unmarshalTests { - t.Run(fmt.Sprintf("unmarshal_%d", i), func(t *testing.T) { - gotErr := json.Unmarshal([]byte(tc.input), tc.destination) - tc.errorAsserter(t, gotErr) - assert.Equal(t, tc.expected, tc.destination) - }) + 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 struct { - ID ID[id, Base64] `json:"id"` + type Foo[enc encoding] struct { + ID ID[id, enc] `json:"id"` } - type FooPointer struct { - ID *ID[id, Base64] `json:"id"` + type FooPointer[enc encoding] struct { + ID *ID[id, enc] `json:"id"` } - type FooOZ struct { - ID ID[id, Base64] `json:"id,omitzero"` + type FooOZ[enc encoding] struct { + ID ID[id, enc] `json:"id,omitzero"` } - type FooPointerOZ struct { - ID *ID[id, Base64] `json:"id,omitzero"` + type FooPointerOZ[enc encoding] struct { + ID *ID[id, enc] `json:"id,omitzero"` } - const js = `{"id":"AZomiURKfF6MYQcDvjFM_A"}` + const jsBase64 = `{"id":"AZomiURKfF6MYQcDvjFM_A"}` uuid := id{0x01, 0x9a, 0x26, 0x89, 0x44, 0x4a, 0x7c, 0x5e, 0x8c, 0x61, 0x07, 0x03, 0xbe, 0x31, 0x4c, 0xfc} - marshalTests := []struct { - input any - expectedJSON string - errorAsserter tst.ErrorAssertionFunc - }{ - 0: { - input: Foo{ID: ID[id, Base64]{Value: uuid}}, - expectedJSON: js, + marshalTests := []marshalTestCase{ + { + name: "FooOZ[Base64] zero", + input: FooOZ[Base64]{ID: ID[id, Base64]{Value: zeroUUID}}, + expectedJSON: `{}`, errorAsserter: tst.NoError(), }, - 1: { - input: &Foo{ID: ID[id, Base64]{Value: uuid}}, - expectedJSON: js, + { + name: "*FooOZ[Base64] zero", + input: &FooOZ[Base64]{ID: ID[id, Base64]{Value: zeroUUID}}, + expectedJSON: `{}`, errorAsserter: tst.NoError(), }, - 2: { - input: FooPointer{ID: &ID[id, Base64]{Value: uuid}}, - expectedJSON: js, + { + name: "FooPointerOZ[Base64] zero", + input: FooPointerOZ[Base64]{ID: &ID[id, Base64]{Value: zeroUUID}}, + expectedJSON: `{}`, errorAsserter: tst.NoError(), }, - 3: { - input: &FooPointer{ID: &ID[id, Base64]{Value: uuid}}, - expectedJSON: js, + { + name: "*FooPointerOZ[Base64] zero", + input: &FooPointerOZ[Base64]{ID: &ID[id, Base64]{Value: zeroUUID}}, + expectedJSON: `{}`, errorAsserter: tst.NoError(), }, - 4: { - input: FooOZ{ID: ID[id, Base64]{Value: zeroUUID}}, + { + name: "FooPointerOZ[Base64] nil", + input: FooPointerOZ[Base64]{ID: nil}, expectedJSON: `{}`, errorAsserter: tst.NoError(), }, - 5: { - input: &FooOZ{ID: ID[id, Base64]{Value: zeroUUID}}, + { + name: "*FooPointerOZ[Base64] nil", + input: &FooPointerOZ[Base64]{ID: nil}, expectedJSON: `{}`, errorAsserter: tst.NoError(), }, - 6: { - input: FooPointerOZ{ID: &ID[id, Base64]{Value: zeroUUID}}, - expectedJSON: `{}`, + { + name: "Foo[Base64] uuid", + input: Foo[Base64]{ID: ID[id, Base64]{Value: uuid}}, + expectedJSON: jsBase64, errorAsserter: tst.NoError(), }, - 7: { - input: &FooPointerOZ{ID: &ID[id, Base64]{Value: zeroUUID}}, - expectedJSON: `{}`, + { + name: "*Foo[Base64] uuid", + input: &Foo[Base64]{ID: ID[id, Base64]{Value: uuid}}, + expectedJSON: jsBase64, errorAsserter: tst.NoError(), }, - 8: { - input: &FooPointerOZ{ID: nil}, - expectedJSON: `{}`, + { + 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 i, tc := range marshalTests { - t.Run(fmt.Sprintf("marshal_%d", i), func(t *testing.T) { - got, gotErr := json.Marshal(tc.input) - tc.errorAsserter(t, gotErr) - assert.Equal(t, tc.expectedJSON, string(got)) - }) + for _, tc := range marshalTests { + t.Run(tc.name, tc.Test) } } From 377dc3e4ace3453affe6447dd2077f343bd823e5 Mon Sep 17 00:00:00 2001 From: Kostas Stamatakis Date: Sun, 23 Nov 2025 11:19:03 +0200 Subject: [PATCH 14/15] base 62 --- id/base62.go | 36 ++++++++++++++++++++++++++---------- id/uuid.go | 4 ++-- id/uuid_test.go | 20 ++++++++++++++++++++ 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/id/base62.go b/id/base62.go index fd377fc..ef4a952 100644 --- a/id/base62.go +++ b/id/base62.go @@ -6,9 +6,9 @@ import ( "math/big" ) -type Base62Encoding struct{} +type Base62 struct{} -func (e *Base62Encoding) AppendEncode(dst, src []byte) []byte { +func (e Base62) appendEncode(dst, src []byte) []byte { if len(src) == 0 { return nil } @@ -18,22 +18,38 @@ func (e *Base62Encoding) AppendEncode(dst, src []byte) []byte { return num.Append(dst, 62) } -func base62Decode(b []byte) ([]byte, error) { +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{} - n, ok := num.SetString(string(b), 62) + nn, ok := num.SetString(string(src), 62) if !ok { - return nil, fmt.Errorf("tbd") + return 0, fmt.Errorf("base62 error while parsing") } - return n.Bytes(), nil + 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 { - // log2(62) ≈ 5.954196310386875 - return int(math.Ceil(float64(n) * 8 / 5.954196310386875)) + return int(math.Ceil(float64(n) * 8 / lg262)) } func base62DecodedLen(m int) int { - // log2(62) ≈ 5.954196310386875 - return int(math.Floor(float64(m) * 5.954196310386875 / 8)) + return int(math.Floor(float64(m) * lg262 / 8)) } + +var _ encoding = Base62{} diff --git a/id/uuid.go b/id/uuid.go index 5142485..967723f 100644 --- a/id/uuid.go +++ b/id/uuid.go @@ -9,14 +9,14 @@ import ( const uuidSize = 16 type encoding interface { - encode(dst, src []byte) + // 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 + // decodedLen(n int) int } type ID[UUID ~[uuidSize]byte, Enc encoding] struct { diff --git a/id/uuid_test.go b/id/uuid_test.go index ac5c8d5..fdbccfe 100644 --- a/id/uuid_test.go +++ b/id/uuid_test.go @@ -64,6 +64,20 @@ func TestJSONUnmarshal(t *testing.T) { 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 { @@ -147,6 +161,12 @@ func TestJSONMarshal(t *testing.T) { 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}}, From 630e8a9088950bc5e72ca63d609f8c2bcb9f3bcc Mon Sep 17 00:00:00 2001 From: Kostas Stamatakis Date: Sun, 23 Nov 2025 12:10:15 +0200 Subject: [PATCH 15/15] fmt and gen --- id/go.mod | 1 + id/mocks_test.go | 212 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 213 insertions(+) create mode 100644 id/mocks_test.go diff --git a/id/go.mod b/id/go.mod index 590029b..24b7595 100644 --- a/id/go.mod +++ b/id/go.mod @@ -10,5 +10,6 @@ require ( 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/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 +}