Skip to content

Commit d65fcc5

Browse files
committed
Add id module
1 parent 2e10b2e commit d65fcc5

File tree

10 files changed

+304
-11
lines changed

10 files changed

+304
-11
lines changed

.github/dependabot.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,20 @@ updates:
5151
patterns:
5252
- "*"
5353

54+
- package-ecosystem: "gomod"
55+
directory: "/id"
56+
schedule:
57+
interval: "daily"
58+
commit-message:
59+
prefix: "[id]"
60+
include: "scope"
61+
allow:
62+
- dependency-type: all
63+
groups:
64+
main:
65+
patterns:
66+
- "*"
67+
5468
- package-ecosystem: "gomod"
5569
directory: "/tst"
5670
schedule:

.github/workflows/sub_id.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
2+
name: id
3+
4+
on:
5+
workflow_dispatch: {}
6+
7+
push:
8+
branches: [ main ]
9+
10+
pull_request:
11+
branches: [ main ]
12+
paths:
13+
- .golangci.yml
14+
- tools/**
15+
- .github/workflows/ci.yml
16+
- .github/workflows/sub_id.yml
17+
- id/**
18+
19+
jobs:
20+
21+
ci:
22+
uses: ./.github/workflows/ci.yml
23+
with:
24+
mod_path: id
25+
secrets:
26+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

http/encoding/encoding.go

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package encoding
22

33
import (
4-
"encoding/base64"
54
"errors"
65
"net/url"
76
"strings"
@@ -16,16 +15,6 @@ import (
1615
"golang.org/x/text/encoding/unicode"
1716
)
1817

19-
// URLSafeBase64 returns a [base64.Encoding] based on [base64.URLEncoding] replacing the default padding character ('=') padding character to a url safe one ('~').
20-
// 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):
21-
// Alphabetic characters: A-Z, a-z
22-
// Digits: 0-9
23-
// Hyphen: -
24-
// Underscore: _
25-
// Period: .
26-
// Tilde: ~
27-
var URLSafeBase64 = base64.URLEncoding.WithPadding('~')
28-
2918
// RFC5987ExtendedNotationParameterValue decodes RFC 5987 encoded filenames expecting the extended notation
3019
// (charset "'" [ language ] "'" value-chars)
3120
// example: UTF-8'en'file%20name.jpg

id/.mockery.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# https://vektra.github.io/mockery/v3.5/configuration/
2+
3+
log-level: info
4+
formatter: goimports
5+
force-file-write: true
6+
require-template-schema-exists: true
7+
8+
all: true
9+
recursive: false
10+
dir: '{{.InterfaceDir}}'
11+
filename: mocks_test.go
12+
pkgname: '{{.SrcPackageName}}'
13+
structname: '{{.Mock}}{{.InterfaceName}}'
14+
15+
# https://vektra.github.io/mockery/v3.5/template/
16+
template: testify
17+
template-schema: '{{.Template}}.schema.json'
18+
19+
packages:
20+
github.com/ifnotnil/x/id:
21+
config:
22+
all: true
23+
recursive: true

id/Makefile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
SHELL := /usr/bin/env bash
2+
3+
REPO_ROOT = $(shell cd .. && pwd)
4+
5+
include $(REPO_ROOT)/scripts/go.mk
6+
include $(REPO_ROOT)/tools/tools.mk
7+
include $(REPO_ROOT)/scripts/lib.mk
8+

id/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# id
2+
[![ci](https://github.com/ifnotnil/x/actions/workflows/sub_id.yml/badge.svg)](https://github.com/ifnotnil/x/actions/workflows/sub_id.yml)
3+
[![Go Report Card](https://goreportcard.com/badge/github.com/ifnotnil/x/id)](https://goreportcard.com/report/github.com/ifnotnil/x/id)
4+
[![PkgGoDev](https://pkg.go.dev/badge/github.com/ifnotnit/x/id)](https://pkg.go.dev/github.com/ifnotnil/x/id)
5+
[![Version](https://img.shields.io/github/v/tag/ifnotnil/x?filter=id%2F*)](https://pkg.go.dev/github.com/ifnotnil/x/id?tab=versions)
6+
[![codecov](https://codecov.io/gh/ifnotnil/x/graph/badge.svg?token=n0t9q5Y3Sf&component=id)](https://codecov.io/gh/ifnotnil/x)
7+

id/go.mod

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module github.com/ifnotnil/x/id
2+
3+
go 1.24.0
4+
5+
require (
6+
github.com/ifnotnil/x/tst v0.0.2
7+
github.com/stretchr/testify v1.11.1
8+
)
9+
10+
require (
11+
github.com/davecgh/go-spew v1.1.1 // indirect
12+
github.com/pmezard/go-difflib v1.0.0 // indirect
13+
gopkg.in/yaml.v3 v3.0.1 // indirect
14+
)

id/go.sum

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/ifnotnil/x/tst v0.0.2 h1:6ydceMwj3uiKFu1B+TTQJcGd1KZtDOAdyy95kTgtCe4=
4+
github.com/ifnotnil/x/tst v0.0.2/go.mod h1:TFSDsUOkXhDw6k2+vxuypmPXhJzuQ3U+qWHFc4KiMEo=
5+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
6+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
7+
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
8+
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
9+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
10+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
11+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
12+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
13+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
14+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

id/uuid.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package id
2+
3+
import (
4+
"encoding/base64"
5+
"encoding/json"
6+
"errors"
7+
)
8+
9+
// 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):
10+
// Alphabetic characters: A-Z, a-z
11+
// Digits: 0-9
12+
// Hyphen: -
13+
// Underscore: _
14+
// Period: .
15+
// Tilde: ~
16+
17+
// Base64 is a [base64.Encoding] based on [base64.URLEncoding] without padding character.
18+
// alphabet of base64: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_
19+
var (
20+
Base64 = base64.URLEncoding.WithPadding(base64.NoPadding)
21+
Base64WithPadding = base64.URLEncoding.WithPadding('~')
22+
)
23+
24+
var (
25+
base64UUIDEncodedLen = Base64.EncodedLen(uuidSize)
26+
base64UUIDEncodedLenJSON = base64UUIDEncodedLen + 2
27+
zeroUUID = [uuidSize]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
28+
)
29+
30+
const uuidSize = 16
31+
32+
type Base64UUID[U ~[uuidSize]byte] struct {
33+
Value U
34+
}
35+
36+
func (u Base64UUID[U]) IsZero() bool {
37+
return u.Value == zeroUUID
38+
}
39+
40+
func (u Base64UUID[U]) MarshalJSON() ([]byte, error) {
41+
b := make([]byte, 1, base64UUIDEncodedLenJSON)
42+
b[0] = '"'
43+
sub := b[1:][:base64UUIDEncodedLen]
44+
Base64.Encode(sub, u.Value[:])
45+
b = b[0 : base64UUIDEncodedLenJSON-1]
46+
b = append(b, '"')
47+
48+
return b, nil
49+
}
50+
51+
func (u *Base64UUID[U]) UnmarshalJSON(b []byte) error {
52+
var s string
53+
err := json.Unmarshal(b, &s)
54+
if err != nil {
55+
return err
56+
}
57+
58+
decBytes, err := Base64.DecodeString(s)
59+
if err != nil {
60+
return err
61+
}
62+
63+
if len(decBytes) != uuidSize {
64+
return ErrMalformedUUID
65+
}
66+
67+
copy(u.Value[:], decBytes)
68+
69+
return nil
70+
}
71+
72+
var ErrMalformedUUID = errors.New("malformed uuid")
73+
74+
var (
75+
_ json.Marshaler = (*Base64UUID[[uuidSize]byte])(nil)
76+
_ json.Unmarshaler = (*Base64UUID[[uuidSize]byte])(nil)
77+
)

id/uuid_test.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package id
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"testing"
7+
8+
"github.com/ifnotnil/x/tst"
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
type id = [uuidSize]byte
13+
14+
func TestUUID_JSON(t *testing.T) {
15+
type Foo struct {
16+
ID Base64UUID[id] `json:"id"`
17+
}
18+
19+
type FooPointer struct {
20+
ID *Base64UUID[id] `json:"id"`
21+
}
22+
23+
type FooOZ struct {
24+
ID Base64UUID[id] `json:"id,omitzero"`
25+
}
26+
27+
type FooPointerOZ struct {
28+
ID *Base64UUID[id] `json:"id,omitzero"`
29+
}
30+
31+
const js = `{"id":"AZomiURKfF6MYQcDvjFM_A"}`
32+
uuid := id{0x01, 0x9a, 0x26, 0x89, 0x44, 0x4a, 0x7c, 0x5e, 0x8c, 0x61, 0x07, 0x03, 0xbe, 0x31, 0x4c, 0xfc}
33+
34+
unmarshalTests := []struct {
35+
input string
36+
destination any
37+
expected any
38+
errorAsserter tst.ErrorAssertionFunc
39+
}{
40+
{
41+
input: js,
42+
destination: &Foo{ID: Base64UUID[id]{Value: zeroUUID}},
43+
expected: &Foo{ID: Base64UUID[id]{Value: uuid}},
44+
errorAsserter: tst.NoError(),
45+
},
46+
{
47+
input: js,
48+
destination: &FooPointer{ID: nil},
49+
expected: &FooPointer{ID: &Base64UUID[id]{Value: uuid}},
50+
errorAsserter: tst.NoError(),
51+
},
52+
}
53+
54+
for i, tc := range unmarshalTests {
55+
t.Run(fmt.Sprintf("unmarshal_%d", i), func(t *testing.T) {
56+
gotErr := json.Unmarshal([]byte(tc.input), tc.destination)
57+
tc.errorAsserter(t, gotErr)
58+
assert.Equal(t, tc.expected, tc.destination)
59+
})
60+
}
61+
62+
marshalTests := []struct {
63+
input any
64+
expectedJSON string
65+
errorAsserter tst.ErrorAssertionFunc
66+
}{
67+
0: {
68+
input: Foo{ID: Base64UUID[id]{Value: uuid}},
69+
expectedJSON: js,
70+
errorAsserter: tst.NoError(),
71+
},
72+
1: {
73+
input: &Foo{ID: Base64UUID[id]{Value: uuid}},
74+
expectedJSON: js,
75+
errorAsserter: tst.NoError(),
76+
},
77+
2: {
78+
input: FooPointer{ID: &Base64UUID[id]{Value: uuid}},
79+
expectedJSON: js,
80+
errorAsserter: tst.NoError(),
81+
},
82+
3: {
83+
input: &FooPointer{ID: &Base64UUID[id]{Value: uuid}},
84+
expectedJSON: js,
85+
errorAsserter: tst.NoError(),
86+
},
87+
4: {
88+
input: FooOZ{ID: Base64UUID[id]{Value: zeroUUID}},
89+
expectedJSON: `{}`,
90+
errorAsserter: tst.NoError(),
91+
},
92+
5: {
93+
input: &FooOZ{ID: Base64UUID[id]{Value: zeroUUID}},
94+
expectedJSON: `{}`,
95+
errorAsserter: tst.NoError(),
96+
},
97+
6: {
98+
input: FooPointerOZ{ID: &Base64UUID[id]{Value: zeroUUID}},
99+
expectedJSON: `{}`,
100+
errorAsserter: tst.NoError(),
101+
},
102+
7: {
103+
input: &FooPointerOZ{ID: &Base64UUID[id]{Value: zeroUUID}},
104+
expectedJSON: `{}`,
105+
errorAsserter: tst.NoError(),
106+
},
107+
8: {
108+
input: &FooPointerOZ{ID: nil},
109+
expectedJSON: `{}`,
110+
errorAsserter: tst.NoError(),
111+
},
112+
}
113+
114+
for i, tc := range marshalTests {
115+
t.Run(fmt.Sprintf("marshal_%d", i), func(t *testing.T) {
116+
got, gotErr := json.Marshal(tc.input)
117+
tc.errorAsserter(t, gotErr)
118+
assert.Equal(t, tc.expectedJSON, string(got))
119+
})
120+
}
121+
}

0 commit comments

Comments
 (0)