Skip to content

Commit 6a4df85

Browse files
authored
Add support for JSON marshalling (#21)
1 parent 2d88f33 commit 6a4df85

21 files changed

+1163
-7
lines changed

scripts/gen/main.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os"
77
"os/exec"
88
"path"
9+
"strconv"
910
"strings"
1011
"text/template"
1112
)
@@ -14,8 +15,6 @@ import (
1415
type templateContext struct {
1516
Indexes []int
1617
Len int
17-
TypeName string
18-
TypeDecl string
1918
GenericTypesForward string
2019
}
2120

@@ -24,7 +23,7 @@ const maxTupleLength = 9
2423

2524
var funcMap = template.FuncMap{
2625
"quote": func(value interface{}) string {
27-
return fmt.Sprintf("%q", fmt.Sprint(value))
26+
return strconv.Quote(fmt.Sprint(value))
2827
},
2928
"inc": func(value int) int {
3029
return value + 1
@@ -43,6 +42,14 @@ var funcMap = template.FuncMap{
4342
},
4443
"genericTypesDecl": genTypesDecl,
4544
"genericTypesDeclGenericConstraint": genTypesDeclGenericConstraint,
45+
"buildSingleTypedOverload": func(indexes []int, typ string) string {
46+
typesArray := make([]string, 0, len(indexes))
47+
for range indexes {
48+
typesArray = append(typesArray, typ)
49+
}
50+
51+
return fmt.Sprintf("T%d[%s]", len(indexes), strings.Join(typesArray, ", "))
52+
},
4653
}
4754

4855
//go:embed tuple.tpl

scripts/gen/tuple.tpl

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

33
import (
4+
"encoding/json"
45
"fmt"
6+
57
"golang.org/x/exp/constraints"
68
)
79

@@ -229,3 +231,24 @@ func GreaterOrEqual{{.Len}}[{{genericTypesDecl .Indexes "constraints.Ordered"}}]
229231
func GreaterOrEqual{{.Len}}C[{{genericTypesDeclGenericConstraint .Indexes "Comparable"}}](host, guest T{{.Len}}[{{.GenericTypesForward}}]) bool {
230232
return Compare{{.Len}}C(host, guest).GE()
231233
}
234+
235+
// MarshalJSON marshals the tuple into a JSON array.
236+
func (t {{$typeRef}}) MarshalJSON() ([]byte, error) {
237+
return json.Marshal(t.Slice())
238+
}
239+
240+
// MarshalJSON unmarshals the tuple from a JSON array.
241+
func (t *{{$typeRef}}) UnmarshalJSON(data []byte) error {
242+
var slice []any
243+
if err := json.Unmarshal(data, &slice); err != nil {
244+
return err
245+
}
246+
247+
unmarshalled, err := FromSlice{{.Len}}[{{.GenericTypesForward}}](slice)
248+
if err != nil {
249+
return err
250+
}
251+
252+
*t = unmarshalled
253+
return nil
254+
}

scripts/gen/tuple_test.tpl

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

33
import (
4+
"encoding/json"
45
"testing"
56

67
"github.com/stretchr/testify/require"
@@ -9,10 +10,11 @@ import (
910
{{/* These variables can be used when the context of dot changes. */}}
1011
{{$indexes := .Indexes}}
1112
{{$len := .Len}}
13+
{{$stringOverload := buildSingleTypedOverload $indexes "string"}}
1214

1315
func TestT{{.Len}}_New(t *testing.T) {
1416
tup := New{{.Len}}({{range .Indexes}}{{. | quote}},{{end}})
15-
require.Equal(t, T{{.Len}}[{{range $i, $index := .Indexes}}{{if gt $i 0}}, {{end}}string{{end}}]{
17+
require.Equal(t, {{$stringOverload}}{
1618
{{range .Indexes -}}
1719
V{{.}}: {{. | quote}},
1820
{{end}}
@@ -227,7 +229,7 @@ func TestT{{.Len}}_String(t *testing.T) {
227229

228230
func TestT{{.Len}}_GoString(t *testing.T) {
229231
tup := New{{.Len}}({{range .Indexes}}{{. | quote}},{{end}})
230-
require.Equal(t, `tuple.T{{.Len}}[{{range $i, $index := .Indexes}}{{if gt $i 0}}, {{end}}string{{end}}]{
232+
require.Equal(t, `tuple.{{$stringOverload}}{
231233
{{- range $i, $index := .Indexes -}}
232234
{{- if gt $i 0}}, {{end -}}
233235
V{{$index}}: {{$index | quote}}
@@ -281,7 +283,7 @@ func TestT{{.Len}}_FromArrayX(t *testing.T) {
281283

282284
for _, tt := range tests {
283285
t.Run(tt.name, func(t *testing.T) {
284-
do := func () T{{.Len}}[{{range $i, $index := .Indexes}}{{if gt $i 0}}, {{end}}string{{end}}] {
286+
do := func () {{$stringOverload}} {
285287
return FromArray{{.Len}}X[{{range $i, $index := .Indexes}}{{if gt $i 0}}, {{end}}string{{end}}](tt.array)
286288
}
287289

@@ -393,7 +395,7 @@ func TestT{{.Len}}_FromSliceX(t *testing.T) {
393395

394396
for _, tt := range tests {
395397
t.Run(tt.name, func(t *testing.T) {
396-
do := func () T{{.Len}}[{{range $i, $index := .Indexes}}{{if gt $i 0}}, {{end}}string{{end}}] {
398+
do := func () {{$stringOverload}} {
397399
return FromSlice{{.Len}}X[{{range $i, $index := .Indexes}}{{if gt $i 0}}, {{end}}string{{end}}](tt.slice)
398400
}
399401

@@ -472,3 +474,96 @@ func TestT{{.Len}}_FromSlice(t *testing.T) {
472474
})
473475
}
474476
}
477+
478+
func TestT{{.Len}}_MarshalJSON(t *testing.T) {
479+
tup := New{{.Len}}({{range .Indexes}}{{. | quote}},{{end}})
480+
481+
got, err := json.Marshal(tup)
482+
require.NoError(t, err)
483+
require.Equal(t, got, []byte(`[{{range .Indexes}}{{if ne . 1}},{{end}}{{. | quote}}{{end}}]`))
484+
}
485+
486+
func TestT{{.Len}}_UnmarshalJSON(t *testing.T) {
487+
tests := []struct{
488+
name string
489+
data []byte
490+
want {{$stringOverload}}
491+
wantErr bool
492+
}{
493+
{
494+
name: "nil data",
495+
data: nil,
496+
wantErr: true,
497+
},
498+
{
499+
name: "empty data",
500+
data: []byte{},
501+
wantErr: true,
502+
},
503+
{
504+
name: "string data",
505+
data: []byte(`"hi"`),
506+
wantErr: true,
507+
},
508+
{
509+
name: "empty json array",
510+
data: []byte(`[]`),
511+
wantErr: true,
512+
},
513+
{
514+
name: "longer json array",
515+
data: []byte(`["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]`),
516+
wantErr: true,
517+
},
518+
{
519+
name: "json array of invalid types",
520+
data: []byte(`[{{range $.Indexes}}{{if ne . 1}},{{end}}{{.}}{{end}}]`),
521+
wantErr: true,
522+
},
523+
{{if gt .Len 1 -}}
524+
{
525+
name: "json array with 1 invalid type",
526+
data: []byte(`[{{range $.Indexes}}{{if ne . 1}},{{end}}{{if eq . 1}}{{.}}{{else}}{{. | quote}}{{end}}{{end}}]`),
527+
wantErr: true,
528+
},
529+
{{- end}}
530+
{
531+
name: "json array of valid types",
532+
data: []byte(`[{{range $.Indexes}}{{if ne . 1}},{{end}}{{. | quote}}{{end}}]`),
533+
want: New{{.Len}}({{range .Indexes}}{{. | quote}},{{end}}),
534+
wantErr: false,
535+
},
536+
{
537+
name: "json object of valid types",
538+
data: []byte(`{{"{"}}{{range $.Indexes}}{{if ne . 1}},{{end}}"V{{.}}": {{. | quote}}{{end}}{{"}"}}`),
539+
wantErr: true,
540+
},
541+
}
542+
543+
for _, tt := range tests {
544+
t.Run(tt.name, func(t *testing.T) {
545+
var got {{$stringOverload}}
546+
err := json.Unmarshal(tt.data, &got)
547+
if tt.wantErr {
548+
require.Error(t, err)
549+
return
550+
}
551+
552+
require.NoError(t, err)
553+
require.Equal(t, tt.want, got)
554+
})
555+
}
556+
}
557+
558+
func TestT{{.Len}}_Marshal_Unmarshal(t *testing.T) {
559+
tup := New{{.Len}}({{range .Indexes}}{{. | quote}},{{end}})
560+
561+
marshalled, err := json.Marshal(tup)
562+
require.NoError(t, err)
563+
564+
var unmarshalled {{$stringOverload}}
565+
err = json.Unmarshal(marshalled, &unmarshalled)
566+
567+
require.NoError(t, err)
568+
require.Equal(t, tup, unmarshalled)
569+
}

tuple1.go

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

33
import (
4+
"encoding/json"
45
"fmt"
6+
57
"golang.org/x/exp/constraints"
68
)
79

@@ -194,3 +196,24 @@ func GreaterOrEqual1[Ty1 constraints.Ordered](host, guest T1[Ty1]) bool {
194196
func GreaterOrEqual1C[Ty1 Comparable[Ty1]](host, guest T1[Ty1]) bool {
195197
return Compare1C(host, guest).GE()
196198
}
199+
200+
// MarshalJSON marshals the tuple into a JSON array.
201+
func (t T1[Ty1]) MarshalJSON() ([]byte, error) {
202+
return json.Marshal(t.Slice())
203+
}
204+
205+
// MarshalJSON unmarshals the tuple from a JSON array.
206+
func (t *T1[Ty1]) UnmarshalJSON(data []byte) error {
207+
var slice []any
208+
if err := json.Unmarshal(data, &slice); err != nil {
209+
return err
210+
}
211+
212+
unmarshalled, err := FromSlice1[Ty1](slice)
213+
if err != nil {
214+
return err
215+
}
216+
217+
*t = unmarshalled
218+
return nil
219+
}

tuple1_test.go

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

33
import (
4+
"encoding/json"
45
"testing"
56

67
"github.com/stretchr/testify/require"
@@ -419,3 +420,90 @@ func TestT1_FromSlice(t *testing.T) {
419420
})
420421
}
421422
}
423+
424+
func TestT1_MarshalJSON(t *testing.T) {
425+
tup := New1("1")
426+
427+
got, err := json.Marshal(tup)
428+
require.NoError(t, err)
429+
require.Equal(t, got, []byte(`["1"]`))
430+
}
431+
432+
func TestT1_UnmarshalJSON(t *testing.T) {
433+
tests := []struct {
434+
name string
435+
data []byte
436+
want T1[string]
437+
wantErr bool
438+
}{
439+
{
440+
name: "nil data",
441+
data: nil,
442+
wantErr: true,
443+
},
444+
{
445+
name: "empty data",
446+
data: []byte{},
447+
wantErr: true,
448+
},
449+
{
450+
name: "string data",
451+
data: []byte(`"hi"`),
452+
wantErr: true,
453+
},
454+
{
455+
name: "empty json array",
456+
data: []byte(`[]`),
457+
wantErr: true,
458+
},
459+
{
460+
name: "longer json array",
461+
data: []byte(`["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]`),
462+
wantErr: true,
463+
},
464+
{
465+
name: "json array of invalid types",
466+
data: []byte(`[1]`),
467+
wantErr: true,
468+
},
469+
470+
{
471+
name: "json array of valid types",
472+
data: []byte(`["1"]`),
473+
want: New1("1"),
474+
wantErr: false,
475+
},
476+
{
477+
name: "json object of valid types",
478+
data: []byte(`{"V1": "1"}`),
479+
wantErr: true,
480+
},
481+
}
482+
483+
for _, tt := range tests {
484+
t.Run(tt.name, func(t *testing.T) {
485+
var got T1[string]
486+
err := json.Unmarshal(tt.data, &got)
487+
if tt.wantErr {
488+
require.Error(t, err)
489+
return
490+
}
491+
492+
require.NoError(t, err)
493+
require.Equal(t, tt.want, got)
494+
})
495+
}
496+
}
497+
498+
func TestT1_Marshal_Unmarshal(t *testing.T) {
499+
tup := New1("1")
500+
501+
marshalled, err := json.Marshal(tup)
502+
require.NoError(t, err)
503+
504+
var unmarshalled T1[string]
505+
err = json.Unmarshal(marshalled, &unmarshalled)
506+
507+
require.NoError(t, err)
508+
require.Equal(t, tup, unmarshalled)
509+
}

tuple2.go

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

33
import (
4+
"encoding/json"
45
"fmt"
6+
57
"golang.org/x/exp/constraints"
68
)
79

@@ -210,3 +212,24 @@ func GreaterOrEqual2[Ty1, Ty2 constraints.Ordered](host, guest T2[Ty1, Ty2]) boo
210212
func GreaterOrEqual2C[Ty1 Comparable[Ty1], Ty2 Comparable[Ty2]](host, guest T2[Ty1, Ty2]) bool {
211213
return Compare2C(host, guest).GE()
212214
}
215+
216+
// MarshalJSON marshals the tuple into a JSON array.
217+
func (t T2[Ty1, Ty2]) MarshalJSON() ([]byte, error) {
218+
return json.Marshal(t.Slice())
219+
}
220+
221+
// MarshalJSON unmarshals the tuple from a JSON array.
222+
func (t *T2[Ty1, Ty2]) UnmarshalJSON(data []byte) error {
223+
var slice []any
224+
if err := json.Unmarshal(data, &slice); err != nil {
225+
return err
226+
}
227+
228+
unmarshalled, err := FromSlice2[Ty1, Ty2](slice)
229+
if err != nil {
230+
return err
231+
}
232+
233+
*t = unmarshalled
234+
return nil
235+
}

0 commit comments

Comments
 (0)