Skip to content

Commit 02d7b8d

Browse files
committed
feat(datauri): added data uri str fmt
1 parent 519c0f1 commit 02d7b8d

File tree

5 files changed

+184
-9
lines changed

5 files changed

+184
-9
lines changed

datauri/data_uri.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package datauri
2+
3+
import (
4+
"encoding"
5+
"encoding/base64"
6+
"errors"
7+
"mime"
8+
"net/url"
9+
"strings"
10+
)
11+
12+
var (
13+
ErrInvalidDataURI = errors.New("invalid data uri")
14+
)
15+
16+
func Parse(dataURI string) (*DataURI, error) {
17+
withoutPrefix := ""
18+
if strings.HasPrefix(dataURI, "data:") {
19+
withoutPrefix = dataURI[len("data:"):]
20+
} else {
21+
withoutPrefix = ";base64," + dataURI
22+
}
23+
24+
parts := strings.SplitN(withoutPrefix, ",", 2)
25+
if len(parts) != 2 {
26+
return nil, ErrInvalidDataURI
27+
}
28+
29+
meta, raw := parts[0], parts[1]
30+
isBase64 := strings.Contains(meta, ";base64")
31+
if isBase64 {
32+
meta = strings.Replace(meta, ";base64", "", 1)
33+
}
34+
35+
mediaType, params, err := mime.ParseMediaType(meta)
36+
if err != nil {
37+
mediaType = ""
38+
}
39+
40+
d := &DataURI{
41+
MediaType: mediaType,
42+
Params: params,
43+
}
44+
45+
if isBase64 {
46+
data, err := base64.StdEncoding.DecodeString(raw)
47+
if err != nil {
48+
return nil, errors.Join(ErrInvalidDataURI, err)
49+
}
50+
d.Data = data
51+
52+
return d, nil
53+
}
54+
55+
s, err := url.PathUnescape(raw)
56+
if err != nil {
57+
return nil, errors.Join(ErrInvalidDataURI, err)
58+
}
59+
60+
d.Data = []byte(s)
61+
62+
return d, nil
63+
}
64+
65+
type DataURI struct {
66+
MediaType string
67+
Params map[string]string
68+
Data []byte
69+
}
70+
71+
func (d DataURI) IsZero() bool {
72+
return len(d.Data) == 0
73+
}
74+
75+
func (DataURI) OpenAPISchemaFormat() string {
76+
return "data-uri"
77+
}
78+
79+
func (d *DataURI) Encoded(base64Encoded bool) string {
80+
s := &strings.Builder{}
81+
s.WriteString("data:")
82+
s.WriteString(strings.ReplaceAll(mime.FormatMediaType(d.MediaType, d.Params), "; ", ";"))
83+
84+
if base64Encoded {
85+
s.WriteString(";base64,")
86+
s.WriteString(base64.StdEncoding.EncodeToString(d.Data))
87+
} else {
88+
s.WriteString(",")
89+
s.WriteString(url.PathEscape(string(d.Data)))
90+
}
91+
return s.String()
92+
}
93+
94+
func (d DataURI) String() string {
95+
return d.Encoded(true)
96+
}
97+
98+
var _ encoding.TextMarshaler = (*DataURI)(nil)
99+
100+
func (d DataURI) MarshalText() ([]byte, error) {
101+
return []byte(d.Encoded(true)), nil
102+
}
103+
104+
var _ encoding.TextUnmarshaler = (*DataURI)(nil)
105+
106+
func (d *DataURI) UnmarshalText(text []byte) error {
107+
dd, err := Parse(string(text))
108+
if err != nil {
109+
return err
110+
}
111+
*d = *dd
112+
return nil
113+
}

datauri/data_uri_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package datauri
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/octohelm/x/testing/bdd"
8+
)
9+
10+
func TestDataURI(t *testing.T) {
11+
cases := []struct {
12+
URI string
13+
DataURI DataURI
14+
}{
15+
{
16+
URI: "data:,A%20brief%20note",
17+
DataURI: DataURI{
18+
Data: []byte("A brief note"),
19+
},
20+
},
21+
{
22+
URI: `data:text/plain;charset=utf-8;filename="file x",A%20brief%20note`,
23+
DataURI: DataURI{
24+
MediaType: "text/plain",
25+
Params: map[string]string{
26+
"charset": "utf-8",
27+
"filename": "file x",
28+
},
29+
Data: []byte("A brief note"),
30+
},
31+
},
32+
}
33+
34+
for _, c := range cases {
35+
bdd.FromT(t).Given(c.URI, func(b bdd.T) {
36+
b.When("parse", func(b bdd.T) {
37+
dataURI, err := Parse(c.URI)
38+
b.Then("success",
39+
bdd.NoError(err),
40+
bdd.Equal(c.DataURI, *dataURI),
41+
)
42+
})
43+
44+
b.When("encoded", func(b bdd.T) {
45+
uri := c.DataURI.Encoded(strings.Contains(c.URI, ";base64,"))
46+
47+
b.Then("success",
48+
bdd.Equal(c.URI, uri),
49+
)
50+
})
51+
})
52+
}
53+
54+
bdd.FromT(t).When("parse without data proto", func(b bdd.T) {
55+
dataURI, err := Parse("QSBicmllZiBub3Rl")
56+
57+
b.Then("success",
58+
bdd.NoError(err),
59+
bdd.Equal(string(dataURI.Data), `A brief note`),
60+
)
61+
})
62+
}

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
module github.com/octohelm/x
22

3-
go 1.25.1
3+
go 1.25.3
44

55
tool mvdan.cc/gofumpt
66

77
require (
8-
github.com/go-json-experiment/json v0.0.0-20250910080747-cc2cfa0554c3
8+
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e
99
github.com/google/go-cmp v0.7.0
1010
golang.org/x/sync v0.17.0
1111
golang.org/x/text v0.30.0
@@ -14,5 +14,5 @@ require (
1414

1515
require (
1616
golang.org/x/mod v0.29.0 // indirect
17-
mvdan.cc/gofumpt v0.9.1 // indirect
17+
mvdan.cc/gofumpt v0.9.2 // indirect
1818
)

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
github.com/go-json-experiment/json v0.0.0-20250910080747-cc2cfa0554c3 h1:02WINGfSX5w0Mn+F28UyRoSt9uvMhKguwWMlOAh6U/0=
2-
github.com/go-json-experiment/json v0.0.0-20250910080747-cc2cfa0554c3/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
1+
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU=
2+
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
33
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
44
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
55
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -20,5 +20,5 @@ golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
2020
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
2121
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
2222
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
23-
mvdan.cc/gofumpt v0.9.1 h1:p5YT2NfFWsYyTieYgwcQ8aKV3xRvFH4uuN/zB2gBbMQ=
24-
mvdan.cc/gofumpt v0.9.1/go.mod h1:3xYtNemnKiXaTh6R4VtlqDATFwBbdXI8lJvH/4qk7mw=
23+
mvdan.cc/gofumpt v0.9.2 h1:zsEMWL8SVKGHNztrx6uZrXdp7AX8r421Vvp23sz7ik4=
24+
mvdan.cc/gofumpt v0.9.2/go.mod h1:iB7Hn+ai8lPvofHd9ZFGVg2GOr8sBUw1QUWjNbmIL/s=

testing/internal/assert.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ func failureMessage[A any](actual A, m Matcher[A]) string {
3232
}
3333

3434
if m.Negative() {
35-
return fmt.Sprintf("should not %s, but got\n%s", m.Action(), maybeDiff(v, m))
35+
return fmt.Sprintf("should not %s, but got\n%v", m.Action(), maybeDiff(v, m))
3636
}
3737

38-
return fmt.Sprintf("should %s, but got\n%s", m.Action(), maybeDiff(v, m))
38+
return fmt.Sprintf("should %s, but got\n%v", m.Action(), maybeDiff(v, m))
3939
}
4040

4141
func maybeDiff(actual any, m any) any {

0 commit comments

Comments
 (0)