-
Notifications
You must be signed in to change notification settings - Fork 14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
cm: Implement json Marshal/Unmarshal for List type #266
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,12 @@ package cm | |
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"fmt" | ||
"math" | ||
"reflect" | ||
"runtime" | ||
"strings" | ||
"testing" | ||
) | ||
|
||
|
@@ -14,3 +20,310 @@ func TestListMethods(t *testing.T) { | |
t.Errorf("got (%s) != want (%s)", string(got), string(want)) | ||
} | ||
} | ||
|
||
type listTestItem struct { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is all this machinery necessary? If There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The machinery is necessary to generalize the While
|
||
Name string `json:"name"` | ||
Age int `json:"age"` | ||
} | ||
|
||
type listTestInvalid struct { | ||
Name string `json:"name"` | ||
Age int `json:"age"` | ||
} | ||
|
||
type listTestWrapper[T comparable] struct { | ||
raw string | ||
outerList List[T] | ||
innerList []T | ||
err bool | ||
} | ||
|
||
func (w *listTestWrapper[T]) wantErr() bool { | ||
return w.err | ||
} | ||
|
||
func (w *listTestWrapper[T]) outer() any { | ||
return &w.outerList | ||
} | ||
|
||
func (w *listTestWrapper[T]) outerSlice() any { | ||
return w.outerList.Slice() | ||
} | ||
|
||
func (w *listTestWrapper[T]) inner() any { | ||
return w.innerList | ||
} | ||
|
||
func (w *listTestWrapper[T]) rawData() string { | ||
return w.raw | ||
} | ||
|
||
func newListEncoder[T comparable](raw string, want []T, wantErr bool) *listTestWrapper[T] { | ||
return &listTestWrapper[T]{raw: raw, outerList: ToList(want), err: wantErr} | ||
} | ||
|
||
func newListDecoder[T comparable](raw string, want []T, wantErr bool) *listTestWrapper[T] { | ||
return &listTestWrapper[T]{raw: raw, innerList: want, err: wantErr} | ||
} | ||
|
||
type listTester interface { | ||
outer() any | ||
inner() any | ||
outerSlice() any | ||
wantErr() bool | ||
rawData() string | ||
} | ||
|
||
func (_ listTestInvalid) MarshalJSON() ([]byte, error) { | ||
return nil, fmt.Errorf("can't encode") | ||
} | ||
|
||
func (_ *listTestInvalid) UnmarshalJSON(_ []byte) error { | ||
return fmt.Errorf("can't decode") | ||
} | ||
|
||
func TestListMarshalJSON(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
w listTester | ||
}{ | ||
{ | ||
name: "encode error", | ||
w: newListEncoder(``, []listTestInvalid{{}}, true), | ||
}, | ||
{ | ||
name: "f32 nan", | ||
w: newListEncoder(``, []float32{float32(math.NaN())}, true), | ||
}, | ||
{ | ||
name: "f64 nan", | ||
w: newListEncoder(``, []float64{float64(math.NaN())}, true), | ||
}, | ||
{ | ||
name: "null", | ||
w: newListEncoder[string](`null`, nil, false), | ||
}, | ||
{ | ||
name: "empty", | ||
w: newListEncoder(`[]`, []string{}, false), | ||
}, | ||
{ | ||
name: "bool", | ||
w: newListEncoder(`[true,false]`, []bool{true, false}, false), | ||
}, | ||
{ | ||
name: "string", | ||
w: newListEncoder(`["one","two","three"]`, []string{"one", "two", "three"}, false), | ||
}, | ||
{ | ||
name: "char", | ||
w: newListEncoder(`[104,105,127942]`, []rune{'h', 'i', '🏆'}, false), | ||
}, | ||
{ | ||
name: "s8", | ||
w: newListEncoder(`[123,-123,127]`, []int8{123, -123, math.MaxInt8}, false), | ||
}, | ||
{ | ||
name: "u8", | ||
w: newListEncoder(`[123,0,255]`, []uint8{123, 0, math.MaxUint8}, false), | ||
}, | ||
{ | ||
name: "s16", | ||
w: newListEncoder(`[123,-123,32767]`, []int16{123, -123, math.MaxInt16}, false), | ||
}, | ||
{ | ||
name: "u16", | ||
w: newListEncoder(`[123,0,65535]`, []uint16{123, 0, math.MaxUint16}, false), | ||
}, | ||
{ | ||
name: "s32", | ||
w: newListEncoder(`[123,-123,2147483647]`, []int32{123, -123, math.MaxInt32}, false), | ||
}, | ||
{ | ||
name: "u32", | ||
w: newListEncoder(`[123,0,4294967295]`, []uint32{123, 0, math.MaxUint32}, false), | ||
}, | ||
{ | ||
name: "s64", | ||
w: newListEncoder(`[123,-123,9223372036854775807]`, []int64{123, -123, math.MaxInt64}, false), | ||
}, | ||
{ | ||
name: "u64", | ||
w: newListEncoder(`[123,0,18446744073709551615]`, []uint64{123, 0, math.MaxUint64}, false), | ||
}, | ||
{ | ||
name: "f32", | ||
w: newListEncoder(`[1.01,2,3.4028235e+38]`, []float32{1.01, 2, math.MaxFloat32}, false), | ||
}, | ||
{ | ||
name: "f64", | ||
w: newListEncoder(`[1.01,2,1.7976931348623157e+308]`, []float64{1.01, 2, math.MaxFloat64}, false), | ||
}, | ||
{ | ||
name: "struct", | ||
w: newListEncoder(`[{"name":"joe","age":10},{"name":"jane","age":20}]`, []listTestItem{{Name: "joe", Age: 10}, {Name: "jane", Age: 20}}, false), | ||
}, | ||
{ | ||
name: "list", | ||
w: newListEncoder(`[["one","two","three"],["four","five","six"]]`, []List[string]{ToList([]string{"one", "two", "three"}), ToList([]string{"four", "five", "six"})}, false), | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
// NOTE(lxf): skip marshal errors in tinygo as it uses 'defer' | ||
// needs tinygo 0.35-dev | ||
if tt.w.wantErr() && runtime.Compiler == "tinygo" && strings.Contains(runtime.GOARCH, "wasm") { | ||
return | ||
} | ||
|
||
data, err := json.Marshal(tt.w.outer()) | ||
if err != nil { | ||
if tt.w.wantErr() { | ||
return | ||
} | ||
|
||
t.Fatal(err) | ||
} | ||
|
||
if tt.w.wantErr() { | ||
t.Fatalf("expect error, but got none. got (%s)", string(data)) | ||
} | ||
|
||
if got, want := data, []byte(tt.w.rawData()); !bytes.Equal(got, want) { | ||
t.Errorf("got (%v) != want (%v)", string(got), string(want)) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestListUnmarshalJSON(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
w listTester | ||
}{ | ||
{ | ||
name: "decode error", | ||
w: newListDecoder(`["joe"]`, []listTestInvalid{}, true), | ||
}, | ||
{ | ||
name: "invalid json", | ||
w: newListDecoder(`[joe]`, []string{}, true), | ||
}, | ||
{ | ||
name: "incompatible type", | ||
w: newListDecoder(`[123,456]`, []string{}, true), | ||
}, | ||
{ | ||
name: "incompatible bool", | ||
w: newListDecoder(`["true","false"]`, []bool{true, false}, true), | ||
}, | ||
{ | ||
name: "incompatible s32", | ||
w: newListDecoder(`["123","-123","2147483647"]`, []int32{}, true), | ||
}, | ||
{ | ||
name: "incompatible u32", | ||
w: newListDecoder(`["123","0","4294967295"]`, []uint32{}, true), | ||
}, | ||
|
||
{ | ||
name: "null", | ||
w: newListDecoder[string](`null`, nil, false), | ||
}, | ||
{ | ||
name: "empty", | ||
w: newListDecoder(`[]`, []string{}, false), | ||
}, | ||
{ | ||
name: "bool", | ||
w: newListDecoder(`[true,false]`, []bool{true, false}, false), | ||
}, | ||
{ | ||
name: "string", | ||
w: newListDecoder(`["one","two","three"]`, []string{"one", "two", "three"}, false), | ||
}, | ||
{ | ||
name: "char", | ||
w: newListDecoder(`[104,105,127942]`, []rune{'h', 'i', '🏆'}, false), | ||
}, | ||
{ | ||
name: "s8", | ||
w: newListDecoder(`[123,-123,127]`, []int8{123, -123, math.MaxInt8}, false), | ||
}, | ||
{ | ||
name: "u8", | ||
w: newListDecoder(`[123,0,255]`, []uint8{123, 0, math.MaxUint8}, false), | ||
}, | ||
{ | ||
name: "s16", | ||
w: newListDecoder(`[123,-123,32767]`, []int16{123, -123, math.MaxInt16}, false), | ||
}, | ||
{ | ||
name: "u16", | ||
w: newListDecoder(`[123,0,65535]`, []uint16{123, 0, math.MaxUint16}, false), | ||
}, | ||
{ | ||
name: "s32", | ||
w: newListDecoder(`[123,-123,2147483647]`, []int32{123, -123, math.MaxInt32}, false), | ||
}, | ||
{ | ||
name: "u32", | ||
w: newListDecoder(`[123,0,4294967295]`, []uint32{123, 0, math.MaxUint32}, false), | ||
}, | ||
{ | ||
name: "s64", | ||
w: newListDecoder(`[123,-123,9223372036854775807]`, []int64{123, -123, math.MaxInt64}, false), | ||
}, | ||
{ | ||
name: "u64", | ||
w: newListDecoder(`[123,0,18446744073709551615]`, []uint64{123, 0, math.MaxUint64}, false), | ||
}, | ||
{ | ||
name: "f32", | ||
w: newListDecoder(`[1.01,2,3.4028235e+38]`, []float32{1.01, 2, math.MaxFloat32}, false), | ||
}, | ||
{ | ||
name: "f32 nan", | ||
w: newListDecoder(`[null]`, []float32{0}, false), | ||
}, | ||
{ | ||
name: "f64", | ||
w: newListDecoder(`[1.01,2,1.7976931348623157e+308]`, []float64{1.01, 2, math.MaxFloat64}, false), | ||
}, | ||
{ | ||
name: "f64 nan", | ||
w: newListDecoder(`[null]`, []float64{0}, false), | ||
}, | ||
{ | ||
name: "struct", | ||
w: newListDecoder(`[{"name":"joe","age":10},{"name":"jane","age":20}]`, []listTestItem{{Name: "joe", Age: 10}, {Name: "jane", Age: 20}}, false), | ||
}, | ||
{ | ||
name: "list", | ||
w: newListDecoder(`[["one","two","three"],["four","five","six"]]`, []List[string]{ToList([]string{"one", "two", "three"}), ToList([]string{"four", "five", "six"})}, false), | ||
}, | ||
// tuple, result, option, and variant needs json implementation | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
err := json.Unmarshal([]byte(tt.w.rawData()), tt.w.outer()) | ||
if err != nil { | ||
if tt.w.wantErr() { | ||
return | ||
} | ||
|
||
t.Fatal(err) | ||
} | ||
|
||
if tt.w.wantErr() { | ||
t.Fatalf("expect error, but got none. got (%v)", tt.w.outerSlice()) | ||
} | ||
|
||
if got, want := tt.w.outerSlice(), tt.w.inner(); !reflect.DeepEqual(got, want) { | ||
t.Errorf("got (%v) != want (%v)", got, want) | ||
} | ||
}) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The caller can mutate the value of nullLiteral. Maybe mix this optimization for now?