From 70e819109543028e8e6d7acccb9ea0d7c7d931a8 Mon Sep 17 00:00:00 2001 From: Lucas Fontes Date: Mon, 9 Dec 2024 17:14:05 -0500 Subject: [PATCH 01/12] cm: Implement json Marshal/Unmarshal for List type Signed-off-by: Lucas Fontes --- cm/list.go | 34 +++++++++++++++++++++++++++++++- cm/list_test.go | 52 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/cm/list.go b/cm/list.go index 5c896d04..0393eb03 100644 --- a/cm/list.go +++ b/cm/list.go @@ -1,6 +1,15 @@ package cm -import "unsafe" +import ( + "bytes" + "encoding/json" + "unsafe" +) + +// nullLiteral is the JSON representation of a null literal. +// https://pkg.go.dev/encoding/json#Unmarshaler +// By convention, to approximate the behavior of Unmarshal itself, Unmarshalers implement UnmarshalJSON([]byte("null")) as a no-op. +var nullLiteral = []byte("null") // List represents a Component Model list. // The binary representation of list is similar to a Go slice minus the cap field. @@ -58,3 +67,26 @@ func (l list[T]) Data() *T { func (l list[T]) Len() uintptr { return l.len } + +// MarshalJSON implements json.Marshaler. +func (l list[T]) MarshalJSON() ([]byte, error) { + return json.Marshal(l.Slice()) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (l *list[T]) UnmarshalJSON(data []byte) error { + if bytes.Equal(data, nullLiteral) { + return nil + } + + var s []T + err := json.Unmarshal(data, &s) + if err != nil { + return err + } + + l.data = unsafe.SliceData([]T(s)) + l.len = uintptr(len(s)) + + return nil +} diff --git a/cm/list_test.go b/cm/list_test.go index 34d674ac..90aa836d 100644 --- a/cm/list_test.go +++ b/cm/list_test.go @@ -2,6 +2,8 @@ package cm import ( "bytes" + "encoding/json" + "reflect" "testing" ) @@ -14,3 +16,53 @@ func TestListMethods(t *testing.T) { t.Errorf("got (%s) != want (%s)", string(got), string(want)) } } + +func TestListJSON(t *testing.T) { + simpleList := []string{"one", "two", "three"} + simpleJSON, err := json.Marshal(simpleList) + if err != nil { + t.Fatal(err) + } + + t.Run("encoding", func(t *testing.T) { + l := ToList(simpleList) + data, err := json.Marshal(l) + if err != nil { + t.Fatal(err) + } + + if got, want := data, simpleJSON; !bytes.Equal(got, want) { + t.Errorf("got (%s) != want (%s)", string(got), string(want)) + } + + var emptyList List[string] + data, err = json.Marshal(emptyList) + if err != nil { + t.Fatal(err) + } + + if got, want := data, nullLiteral; !bytes.Equal(got, want) { + t.Errorf(" got (%s) when should have got nil", string(data)) + } + }) + + t.Run("decoding", func(t *testing.T) { + var decodedList List[string] + if err := json.Unmarshal(simpleJSON, &decodedList); err != nil { + t.Fatal(err) + } + + if got, want := decodedList.Slice(), simpleList; !reflect.DeepEqual(got, want) { + t.Errorf("got (%s) != want (%s)", got, want) + } + + var emptyList List[string] + if err := json.Unmarshal(nullLiteral, &emptyList); err != nil { + t.Fatal(err) + } + + if got, want := emptyList.Slice(), []string(nil); !reflect.DeepEqual(got, want) { + t.Errorf("got (%+v) != want (%+v)", got, want) + } + }) +} From 49db13e1b667b745df88839fe1d7ec370a73a6e9 Mon Sep 17 00:00:00 2001 From: Lucas Fontes Date: Tue, 10 Dec 2024 17:46:53 -0500 Subject: [PATCH 02/12] cm: Add json tests to List type Signed-off-by: Lucas Fontes --- cm/list.go | 19 ++- cm/list_test.go | 345 +++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 317 insertions(+), 47 deletions(-) diff --git a/cm/list.go b/cm/list.go index 0393eb03..b88ce397 100644 --- a/cm/list.go +++ b/cm/list.go @@ -3,6 +3,8 @@ package cm import ( "bytes" "encoding/json" + "fmt" + "strings" "unsafe" ) @@ -70,7 +72,22 @@ func (l list[T]) Len() uintptr { // MarshalJSON implements json.Marshaler. func (l list[T]) MarshalJSON() ([]byte, error) { - return json.Marshal(l.Slice()) + s := l.Slice() + + if s == nil { + return nullLiteral, nil + } + + // NOTE(lxf): Go JSON Encoder will serialize []byte as base64. + // We override that behavior so all int types have the same serialization format. + // []uint8{1,2,3} -> [1,2,3] + // []uint32{1,2,3} -> [1,2,3] + if byteArray, ok := any(s).([]byte); ok { + encoded := strings.Join(strings.Fields(fmt.Sprintf("%d", byteArray)), ",") + return []byte(encoded), nil + } + + return json.Marshal(s) } // UnmarshalJSON implements json.Unmarshaler. diff --git a/cm/list_test.go b/cm/list_test.go index 90aa836d..64560b3c 100644 --- a/cm/list_test.go +++ b/cm/list_test.go @@ -3,6 +3,8 @@ package cm import ( "bytes" "encoding/json" + "fmt" + "math" "reflect" "testing" ) @@ -17,52 +19,303 @@ func TestListMethods(t *testing.T) { } } -func TestListJSON(t *testing.T) { - simpleList := []string{"one", "two", "three"} - simpleJSON, err := json.Marshal(simpleList) - if err != nil { - t.Fatal(err) +type listTestItem struct { + 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), + }, } - t.Run("encoding", func(t *testing.T) { - l := ToList(simpleList) - data, err := json.Marshal(l) - if err != nil { - t.Fatal(err) - } - - if got, want := data, simpleJSON; !bytes.Equal(got, want) { - t.Errorf("got (%s) != want (%s)", string(got), string(want)) - } - - var emptyList List[string] - data, err = json.Marshal(emptyList) - if err != nil { - t.Fatal(err) - } - - if got, want := data, nullLiteral; !bytes.Equal(got, want) { - t.Errorf(" got (%s) when should have got nil", string(data)) - } - }) - - t.Run("decoding", func(t *testing.T) { - var decodedList List[string] - if err := json.Unmarshal(simpleJSON, &decodedList); err != nil { - t.Fatal(err) - } - - if got, want := decodedList.Slice(), simpleList; !reflect.DeepEqual(got, want) { - t.Errorf("got (%s) != want (%s)", got, want) - } - - var emptyList List[string] - if err := json.Unmarshal(nullLiteral, &emptyList); err != nil { - t.Fatal(err) - } - - if got, want := emptyList.Slice(), []string(nil); !reflect.DeepEqual(got, want) { - t.Errorf("got (%+v) != want (%+v)", got, want) - } - }) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + 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) + } + }) + } } From 579f09a793e3a49748a8b47477fcd60b72f52546 Mon Sep 17 00:00:00 2001 From: Lucas Fontes Date: Tue, 10 Dec 2024 20:30:12 -0500 Subject: [PATCH 03/12] cm: better uint8 handling & skipping tinygo 'defer' Signed-off-by: Lucas Fontes --- cm/list.go | 21 +++++++++++++-------- cm/list_test.go | 8 ++++++++ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/cm/list.go b/cm/list.go index b88ce397..57719681 100644 --- a/cm/list.go +++ b/cm/list.go @@ -3,8 +3,6 @@ package cm import ( "bytes" "encoding/json" - "fmt" - "strings" "unsafe" ) @@ -82,12 +80,7 @@ func (l list[T]) MarshalJSON() ([]byte, error) { // We override that behavior so all int types have the same serialization format. // []uint8{1,2,3} -> [1,2,3] // []uint32{1,2,3} -> [1,2,3] - if byteArray, ok := any(s).([]byte); ok { - encoded := strings.Join(strings.Fields(fmt.Sprintf("%d", byteArray)), ",") - return []byte(encoded), nil - } - - return json.Marshal(s) + return json.Marshal(sliceOf(s)) } // UnmarshalJSON implements json.Unmarshaler. @@ -107,3 +100,15 @@ func (l *list[T]) UnmarshalJSON(data []byte) error { return nil } + +type entry[T any] [1]T + +func (v entry[T]) MarshalJSON() ([]byte, error) { + return json.Marshal(v[0]) +} + +type slice[T any] []entry[T] + +func sliceOf[S ~[]E, E any](s S) slice[E] { + return *(*slice[E])(unsafe.Pointer(&s)) +} diff --git a/cm/list_test.go b/cm/list_test.go index 64560b3c..bf0d041b 100644 --- a/cm/list_test.go +++ b/cm/list_test.go @@ -6,6 +6,8 @@ import ( "fmt" "math" "reflect" + "runtime" + "strings" "testing" ) @@ -169,6 +171,12 @@ func TestListMarshalJSON(t *testing.T) { 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() { From 0a2389bdc092eea84059336fad08e88bd0bf7260 Mon Sep 17 00:00:00 2001 From: Randy Reddig Date: Sun, 29 Dec 2024 10:56:30 +1300 Subject: [PATCH 04/12] cm: do not return nullLiteral from MarshalJSON --- cm/list.go | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/cm/list.go b/cm/list.go index 57719681..d90b0368 100644 --- a/cm/list.go +++ b/cm/list.go @@ -6,11 +6,6 @@ import ( "unsafe" ) -// nullLiteral is the JSON representation of a null literal. -// https://pkg.go.dev/encoding/json#Unmarshaler -// By convention, to approximate the behavior of Unmarshal itself, Unmarshalers implement UnmarshalJSON([]byte("null")) as a no-op. -var nullLiteral = []byte("null") - // List represents a Component Model list. // The binary representation of list is similar to a Go slice minus the cap field. type List[T any] struct { @@ -70,17 +65,28 @@ func (l list[T]) Len() uintptr { // MarshalJSON implements json.Marshaler. func (l list[T]) MarshalJSON() ([]byte, error) { - s := l.Slice() - - if s == nil { - return nullLiteral, nil + if l.data == nil { + // This cannot return nullLiteral because the caller can mutate the slice. + return []byte("null"), nil } // NOTE(lxf): Go JSON Encoder will serialize []byte as base64. // We override that behavior so all int types have the same serialization format. // []uint8{1,2,3} -> [1,2,3] // []uint32{1,2,3} -> [1,2,3] - return json.Marshal(sliceOf(s)) + return json.Marshal(sliceOf(l.Slice())) +} + +type slice[T any] []entry[T] + +func sliceOf[S ~[]E, E any](s S) slice[E] { + return *(*slice[E])(unsafe.Pointer(&s)) +} + +type entry[T any] [1]T + +func (v entry[T]) MarshalJSON() ([]byte, error) { + return json.Marshal(v[0]) } // UnmarshalJSON implements json.Unmarshaler. @@ -101,14 +107,8 @@ func (l *list[T]) UnmarshalJSON(data []byte) error { return nil } -type entry[T any] [1]T - -func (v entry[T]) MarshalJSON() ([]byte, error) { - return json.Marshal(v[0]) -} - -type slice[T any] []entry[T] - -func sliceOf[S ~[]E, E any](s S) slice[E] { - return *(*slice[E])(unsafe.Pointer(&s)) -} +// nullLiteral is the JSON representation of a null literal. +// By convention, to approximate the behavior of Unmarshal itself, +// Unmarshalers implement UnmarshalJSON([]byte("null")) as a no-op. +// See https://pkg.go.dev/encoding/json#Unmarshaler for more information. +var nullLiteral = []byte("null") From b405795470c35b13378daf4562ab2b342beb3944 Mon Sep 17 00:00:00 2001 From: Randy Reddig Date: Sun, 29 Dec 2024 11:01:31 +1300 Subject: [PATCH 05/12] cm: optimize json.Marshaler for List[uint8] --- cm/list.go | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/cm/list.go b/cm/list.go index d90b0368..7a75d779 100644 --- a/cm/list.go +++ b/cm/list.go @@ -70,11 +70,16 @@ func (l list[T]) MarshalJSON() ([]byte, error) { return []byte("null"), nil } - // NOTE(lxf): Go JSON Encoder will serialize []byte as base64. - // We override that behavior so all int types have the same serialization format. - // []uint8{1,2,3} -> [1,2,3] - // []uint32{1,2,3} -> [1,2,3] - return json.Marshal(sliceOf(l.Slice())) + s := l.Slice() + var zero T + if unsafe.Sizeof(zero) == 1 { + // The default Go json.Encoder will marshal []byte as base64. + // We override that behavior so all int types have the same serialization format. + // []uint8{1,2,3} -> [1,2,3] + // []uint32{1,2,3} -> [1,2,3] + return json.Marshal(sliceOf(s)) + } + return json.Marshal(s) } type slice[T any] []entry[T] From 4fb3a2beb02a7f58448dc02fee4f5b7319dbc9b4 Mon Sep 17 00:00:00 2001 From: Randy Reddig Date: Sun, 29 Dec 2024 11:04:41 +1300 Subject: [PATCH 06/12] cm: update CHANGELOG --- cm/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/cm/CHANGELOG.md b/cm/CHANGELOG.md index 7974b70e..5dcb3dd2 100644 --- a/cm/CHANGELOG.md +++ b/cm/CHANGELOG.md @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Added - Initial support for Component Model [async](https://github.com/WebAssembly/component-model/blob/main/design/mvp/Async.md) types `stream`, `future`, and `error-context`. +- Initial support for JSON serialization of WIT types, starting with `list` and `record`. ## [v0.1.0] — 2024-12-14 From a22a1572d4383bb8f35601677eba3458df110cd7 Mon Sep 17 00:00:00 2001 From: Randy Reddig Date: Sun, 29 Dec 2024 11:11:04 +1300 Subject: [PATCH 07/12] .github/workflows/test: bump timeout to 15m --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 259037b1..475584db 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -41,7 +41,7 @@ jobs: test-go: name: Test with Go runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 15 strategy: matrix: go-version: ["1.22", "1.23"] From 2e6a4b4bfbb549ecbc83caba41bd0f36eb32b19d Mon Sep 17 00:00:00 2001 From: Randy Reddig Date: Sun, 29 Dec 2024 11:39:16 +1300 Subject: [PATCH 08/12] cm: reorganize List JSON tests --- cm/list_test.go | 130 ++++++++++++++++++++++++------------------------ 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/cm/list_test.go b/cm/list_test.go index bf0d041b..42e19656 100644 --- a/cm/list_test.go +++ b/cm/list_test.go @@ -21,67 +21,6 @@ func TestListMethods(t *testing.T) { } } -type listTestItem struct { - 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 @@ -89,7 +28,7 @@ func TestListMarshalJSON(t *testing.T) { }{ { name: "encode error", - w: newListEncoder(``, []listTestInvalid{{}}, true), + w: newListEncoder(``, []errorEntry{{}}, true), }, { name: "f32 nan", @@ -161,7 +100,7 @@ func TestListMarshalJSON(t *testing.T) { }, { name: "struct", - w: newListEncoder(`[{"name":"joe","age":10},{"name":"jane","age":20}]`, []listTestItem{{Name: "joe", Age: 10}, {Name: "jane", Age: 20}}, false), + w: newListEncoder(`[{"name":"joe","age":10},{"name":"jane","age":20}]`, []testEntry{{Name: "joe", Age: 10}, {Name: "jane", Age: 20}}, false), }, { name: "list", @@ -204,7 +143,7 @@ func TestListUnmarshalJSON(t *testing.T) { }{ { name: "decode error", - w: newListDecoder(`["joe"]`, []listTestInvalid{}, true), + w: newListDecoder(`["joe"]`, []errorEntry{}, true), }, { name: "invalid json", @@ -297,7 +236,7 @@ func TestListUnmarshalJSON(t *testing.T) { }, { name: "struct", - w: newListDecoder(`[{"name":"joe","age":10},{"name":"jane","age":20}]`, []listTestItem{{Name: "joe", Age: 10}, {Name: "jane", Age: 20}}, false), + w: newListDecoder(`[{"name":"joe","age":10},{"name":"jane","age":20}]`, []testEntry{{Name: "joe", Age: 10}, {Name: "jane", Age: 20}}, false), }, { name: "list", @@ -327,3 +266,64 @@ func TestListUnmarshalJSON(t *testing.T) { }) } } + +type listTester interface { + outer() any + inner() any + outerSlice() any + wantErr() bool + rawData() string +} + +type listWrapper[T comparable] struct { + raw string + outerList List[T] + innerList []T + err bool +} + +func (w *listWrapper[T]) wantErr() bool { + return w.err +} + +func (w *listWrapper[T]) outer() any { + return &w.outerList +} + +func (w *listWrapper[T]) outerSlice() any { + return w.outerList.Slice() +} + +func (w *listWrapper[T]) inner() any { + return w.innerList +} + +func (w *listWrapper[T]) rawData() string { + return w.raw +} + +func newListEncoder[T comparable](raw string, want []T, wantErr bool) *listWrapper[T] { + return &listWrapper[T]{raw: raw, outerList: ToList(want), err: wantErr} +} + +func newListDecoder[T comparable](raw string, want []T, wantErr bool) *listWrapper[T] { + return &listWrapper[T]{raw: raw, innerList: want, err: wantErr} +} + +type testEntry struct { + Name string `json:"name"` + Age int `json:"age"` +} + +type errorEntry struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func (errorEntry) MarshalJSON() ([]byte, error) { + return nil, fmt.Errorf("can't encode") +} + +func (*errorEntry) UnmarshalJSON(_ []byte) error { + return fmt.Errorf("can't decode") +} From 833bf3240f53a006125107266da65d3c6a94cb8d Mon Sep 17 00:00:00 2001 From: Randy Reddig Date: Sun, 29 Dec 2024 12:39:58 +1300 Subject: [PATCH 09/12] cm: encode empty Lists as [] --- cm/list.go | 5 ++--- cm/list_test.go | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/cm/list.go b/cm/list.go index 7a75d779..0a171dbd 100644 --- a/cm/list.go +++ b/cm/list.go @@ -65,9 +65,8 @@ func (l list[T]) Len() uintptr { // MarshalJSON implements json.Marshaler. func (l list[T]) MarshalJSON() ([]byte, error) { - if l.data == nil { - // This cannot return nullLiteral because the caller can mutate the slice. - return []byte("null"), nil + if l.len == 0 { + return []byte("[]"), nil } s := l.Slice() diff --git a/cm/list_test.go b/cm/list_test.go index 42e19656..68408aed 100644 --- a/cm/list_test.go +++ b/cm/list_test.go @@ -39,8 +39,8 @@ func TestListMarshalJSON(t *testing.T) { w: newListEncoder(``, []float64{float64(math.NaN())}, true), }, { - name: "null", - w: newListEncoder[string](`null`, nil, false), + name: "nil", + w: newListEncoder[string](`[]`, nil, false), }, { name: "empty", From 472bcf6e9f71886d79898a6f71511829c72f54e8 Mon Sep 17 00:00:00 2001 From: Randy Reddig Date: Sun, 29 Dec 2024 12:40:28 +1300 Subject: [PATCH 10/12] cm: use errors.New --- cm/list_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cm/list_test.go b/cm/list_test.go index 68408aed..92a03da1 100644 --- a/cm/list_test.go +++ b/cm/list_test.go @@ -3,7 +3,7 @@ package cm import ( "bytes" "encoding/json" - "fmt" + "errors" "math" "reflect" "runtime" @@ -321,9 +321,9 @@ type errorEntry struct { } func (errorEntry) MarshalJSON() ([]byte, error) { - return nil, fmt.Errorf("can't encode") + return nil, errors.New("MarshalJSON") } func (*errorEntry) UnmarshalJSON(_ []byte) error { - return fmt.Errorf("can't decode") + return errors.New("UnmarshalJSON") } From 921df284e49fe953918ec4eaf8d8a4e9b11850e2 Mon Sep 17 00:00:00 2001 From: Randy Reddig Date: Sun, 29 Dec 2024 12:40:42 +1300 Subject: [PATCH 11/12] cm: reduce test noise --- cm/result_test.go | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/cm/result_test.go b/cm/result_test.go index dc8f7330..759c03ea 100644 --- a/cm/result_test.go +++ b/cm/result_test.go @@ -1,7 +1,6 @@ package cm import ( - "fmt" "runtime" "testing" "unsafe" @@ -177,9 +176,9 @@ func TestIssue95String(t *testing.T) { want := "hello" res := OK[stringResult](want) got := *res.OK() - fmt.Printf("unsafe.Sizeof(res): %d\n", unsafe.Sizeof(res)) - fmt.Printf("got: %v (%d) want: %v (%d)\n", - unsafe.StringData(got), len(got), unsafe.StringData(want), len(want)) + // fmt.Printf("unsafe.Sizeof(res): %d\n", unsafe.Sizeof(res)) + // fmt.Printf("got: %v (%d) want: %v (%d)\n", + // unsafe.StringData(got), len(got), unsafe.StringData(want), len(want)) if got != want { t.Errorf("*res.OK(): %v, expected %v", got, want) } @@ -196,8 +195,8 @@ func TestIssue95Uint64(t *testing.T) { want := uint64(123) res := OK[uint64Result](want) got := *res.OK() - fmt.Printf("unsafe.Sizeof(res): %d\n", unsafe.Sizeof(res)) - fmt.Printf("got: %v want: %v\n", got, want) + // fmt.Printf("unsafe.Sizeof(res): %d\n", unsafe.Sizeof(res)) + // fmt.Printf("got: %v want: %v\n", got, want) if got != want { t.Errorf("*res.OK(): %v, expected %v", got, want) } @@ -221,8 +220,8 @@ func TestIssue95Struct(t *testing.T) { want := stringStruct{s: "hello"} res := OK[structResult](want) got := *res.OK() - fmt.Printf("unsafe.Sizeof(res): %d\n", unsafe.Sizeof(res)) - fmt.Printf("got: %v want: %v\n", got, want) + // fmt.Printf("unsafe.Sizeof(res): %d\n", unsafe.Sizeof(res)) + // fmt.Printf("got: %v want: %v\n", got, want) if got != want { t.Errorf("*res.OK(): %v, expected %v", got, want) } @@ -233,8 +232,8 @@ func TestIssue95BoolInt64(t *testing.T) { want := int64(1234567890) res := Err[boolInt64Result](1234567890) got := *res.Err() - fmt.Printf("unsafe.Sizeof(res): %d\n", unsafe.Sizeof(res)) - fmt.Printf("got: %v want: %v\n", got, want) + // fmt.Printf("unsafe.Sizeof(res): %d\n", unsafe.Sizeof(res)) + // fmt.Printf("got: %v want: %v\n", got, want) if got != want { t.Errorf("*res.OK(): %v, expected %v", got, want) } From 411bd9a55aa528d261ec42bed9e274fd0868eda1 Mon Sep 17 00:00:00 2001 From: Randy Reddig Date: Sun, 29 Dec 2024 12:43:59 +1300 Subject: [PATCH 12/12] cm: revise List JSON tests --- cm/list_test.go | 166 +++++++++++++++++++++++++----------------------- 1 file changed, 85 insertions(+), 81 deletions(-) diff --git a/cm/list_test.go b/cm/list_test.go index 92a03da1..80101203 100644 --- a/cm/list_test.go +++ b/cm/list_test.go @@ -28,83 +28,83 @@ func TestListMarshalJSON(t *testing.T) { }{ { name: "encode error", - w: newListEncoder(``, []errorEntry{{}}, true), + w: listMarshalTest(``, []errorEntry{{}}, true), }, { name: "f32 nan", - w: newListEncoder(``, []float32{float32(math.NaN())}, true), + w: listMarshalTest(``, []float32{float32(math.NaN())}, true), }, { name: "f64 nan", - w: newListEncoder(``, []float64{float64(math.NaN())}, true), + w: listMarshalTest(``, []float64{float64(math.NaN())}, true), }, { name: "nil", - w: newListEncoder[string](`[]`, nil, false), + w: listMarshalTest[string](`[]`, nil, false), }, { name: "empty", - w: newListEncoder(`[]`, []string{}, false), + w: listMarshalTest(`[]`, []string{}, false), }, { name: "bool", - w: newListEncoder(`[true,false]`, []bool{true, false}, false), + w: listMarshalTest(`[true,false]`, []bool{true, false}, false), }, { name: "string", - w: newListEncoder(`["one","two","three"]`, []string{"one", "two", "three"}, false), + w: listMarshalTest(`["one","two","three"]`, []string{"one", "two", "three"}, false), }, { name: "char", - w: newListEncoder(`[104,105,127942]`, []rune{'h', 'i', '🏆'}, false), + w: listMarshalTest(`[104,105,127942]`, []rune{'h', 'i', '🏆'}, false), }, { name: "s8", - w: newListEncoder(`[123,-123,127]`, []int8{123, -123, math.MaxInt8}, false), + w: listMarshalTest(`[123,-123,127]`, []int8{123, -123, math.MaxInt8}, false), }, { name: "u8", - w: newListEncoder(`[123,0,255]`, []uint8{123, 0, math.MaxUint8}, false), + w: listMarshalTest(`[123,0,255]`, []uint8{123, 0, math.MaxUint8}, false), }, { name: "s16", - w: newListEncoder(`[123,-123,32767]`, []int16{123, -123, math.MaxInt16}, false), + w: listMarshalTest(`[123,-123,32767]`, []int16{123, -123, math.MaxInt16}, false), }, { name: "u16", - w: newListEncoder(`[123,0,65535]`, []uint16{123, 0, math.MaxUint16}, false), + w: listMarshalTest(`[123,0,65535]`, []uint16{123, 0, math.MaxUint16}, false), }, { name: "s32", - w: newListEncoder(`[123,-123,2147483647]`, []int32{123, -123, math.MaxInt32}, false), + w: listMarshalTest(`[123,-123,2147483647]`, []int32{123, -123, math.MaxInt32}, false), }, { name: "u32", - w: newListEncoder(`[123,0,4294967295]`, []uint32{123, 0, math.MaxUint32}, false), + w: listMarshalTest(`[123,0,4294967295]`, []uint32{123, 0, math.MaxUint32}, false), }, { name: "s64", - w: newListEncoder(`[123,-123,9223372036854775807]`, []int64{123, -123, math.MaxInt64}, false), + w: listMarshalTest(`[123,-123,9223372036854775807]`, []int64{123, -123, math.MaxInt64}, false), }, { name: "u64", - w: newListEncoder(`[123,0,18446744073709551615]`, []uint64{123, 0, math.MaxUint64}, false), + w: listMarshalTest(`[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), + w: listMarshalTest(`[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), + w: listMarshalTest(`[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}]`, []testEntry{{Name: "joe", Age: 10}, {Name: "jane", Age: 20}}, false), + w: listMarshalTest(`[{"name":"joe","age":10},{"name":"jane","age":20}]`, []testEntry{{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), + w: listMarshalTest(`[["one","two","three"],["four","five","six"]]`, []List[string]{ToList([]string{"one", "two", "three"}), ToList([]string{"four", "five", "six"})}, false), }, } @@ -112,24 +112,26 @@ func TestListMarshalJSON(t *testing.T) { 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") { + if tt.w.WantErr() && runtime.Compiler == "tinygo" && strings.Contains(runtime.GOARCH, "wasm") { return } - data, err := json.Marshal(tt.w.outer()) + data, err := json.Marshal(tt.w.List()) if err != nil { - if tt.w.wantErr() { + if tt.w.WantErr() { return } - t.Fatal(err) + t.Error(err) + return } - if tt.w.wantErr() { - t.Fatalf("expect error, but got none. got (%s)", string(data)) + if tt.w.WantErr() { + t.Errorf("expected error, but got none. got (%s)", string(data)) + return } - if got, want := data, []byte(tt.w.rawData()); !bytes.Equal(got, want) { + if got, want := data, tt.w.JSON(); !bytes.Equal(got, want) { t.Errorf("got (%v) != want (%v)", string(got), string(want)) } }) @@ -143,124 +145,126 @@ func TestListUnmarshalJSON(t *testing.T) { }{ { name: "decode error", - w: newListDecoder(`["joe"]`, []errorEntry{}, true), + w: listUnmarshalTest(`["joe"]`, []errorEntry{}, true), }, { name: "invalid json", - w: newListDecoder(`[joe]`, []string{}, true), + w: listUnmarshalTest(`[joe]`, []string{}, true), }, { name: "incompatible type", - w: newListDecoder(`[123,456]`, []string{}, true), + w: listUnmarshalTest(`[123,456]`, []string{}, true), }, { name: "incompatible bool", - w: newListDecoder(`["true","false"]`, []bool{true, false}, true), + w: listUnmarshalTest(`["true","false"]`, []bool{true, false}, true), }, { name: "incompatible s32", - w: newListDecoder(`["123","-123","2147483647"]`, []int32{}, true), + w: listUnmarshalTest(`["123","-123","2147483647"]`, []int32{}, true), }, { name: "incompatible u32", - w: newListDecoder(`["123","0","4294967295"]`, []uint32{}, true), + w: listUnmarshalTest(`["123","0","4294967295"]`, []uint32{}, true), }, { name: "null", - w: newListDecoder[string](`null`, nil, false), + w: listUnmarshalTest[string](`null`, nil, false), }, { name: "empty", - w: newListDecoder(`[]`, []string{}, false), + w: listUnmarshalTest(`[]`, []string{}, false), }, { name: "bool", - w: newListDecoder(`[true,false]`, []bool{true, false}, false), + w: listUnmarshalTest(`[true,false]`, []bool{true, false}, false), }, { name: "string", - w: newListDecoder(`["one","two","three"]`, []string{"one", "two", "three"}, false), + w: listUnmarshalTest(`["one","two","three"]`, []string{"one", "two", "three"}, false), }, { name: "char", - w: newListDecoder(`[104,105,127942]`, []rune{'h', 'i', '🏆'}, false), + w: listUnmarshalTest(`[104,105,127942]`, []rune{'h', 'i', '🏆'}, false), }, { name: "s8", - w: newListDecoder(`[123,-123,127]`, []int8{123, -123, math.MaxInt8}, false), + w: listUnmarshalTest(`[123,-123,127]`, []int8{123, -123, math.MaxInt8}, false), }, { name: "u8", - w: newListDecoder(`[123,0,255]`, []uint8{123, 0, math.MaxUint8}, false), + w: listUnmarshalTest(`[123,0,255]`, []uint8{123, 0, math.MaxUint8}, false), }, { name: "s16", - w: newListDecoder(`[123,-123,32767]`, []int16{123, -123, math.MaxInt16}, false), + w: listUnmarshalTest(`[123,-123,32767]`, []int16{123, -123, math.MaxInt16}, false), }, { name: "u16", - w: newListDecoder(`[123,0,65535]`, []uint16{123, 0, math.MaxUint16}, false), + w: listUnmarshalTest(`[123,0,65535]`, []uint16{123, 0, math.MaxUint16}, false), }, { name: "s32", - w: newListDecoder(`[123,-123,2147483647]`, []int32{123, -123, math.MaxInt32}, false), + w: listUnmarshalTest(`[123,-123,2147483647]`, []int32{123, -123, math.MaxInt32}, false), }, { name: "u32", - w: newListDecoder(`[123,0,4294967295]`, []uint32{123, 0, math.MaxUint32}, false), + w: listUnmarshalTest(`[123,0,4294967295]`, []uint32{123, 0, math.MaxUint32}, false), }, { name: "s64", - w: newListDecoder(`[123,-123,9223372036854775807]`, []int64{123, -123, math.MaxInt64}, false), + w: listUnmarshalTest(`[123,-123,9223372036854775807]`, []int64{123, -123, math.MaxInt64}, false), }, { name: "u64", - w: newListDecoder(`[123,0,18446744073709551615]`, []uint64{123, 0, math.MaxUint64}, false), + w: listUnmarshalTest(`[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), + w: listUnmarshalTest(`[1.01,2,3.4028235e+38]`, []float32{1.01, 2, math.MaxFloat32}, false), }, { name: "f32 nan", - w: newListDecoder(`[null]`, []float32{0}, false), + w: listUnmarshalTest(`[null]`, []float32{0}, false), }, { name: "f64", - w: newListDecoder(`[1.01,2,1.7976931348623157e+308]`, []float64{1.01, 2, math.MaxFloat64}, false), + w: listUnmarshalTest(`[1.01,2,1.7976931348623157e+308]`, []float64{1.01, 2, math.MaxFloat64}, false), }, { name: "f64 nan", - w: newListDecoder(`[null]`, []float64{0}, false), + w: listUnmarshalTest(`[null]`, []float64{0}, false), }, { name: "struct", - w: newListDecoder(`[{"name":"joe","age":10},{"name":"jane","age":20}]`, []testEntry{{Name: "joe", Age: 10}, {Name: "jane", Age: 20}}, false), + w: listUnmarshalTest(`[{"name":"joe","age":10},{"name":"jane","age":20}]`, []testEntry{{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), + w: listUnmarshalTest(`[["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()) + err := json.Unmarshal(tt.w.JSON(), tt.w.List()) if err != nil { - if tt.w.wantErr() { + if tt.w.WantErr() { return } - t.Fatal(err) + t.Error(err) + return } - if tt.w.wantErr() { - t.Fatalf("expect error, but got none. got (%v)", tt.w.outerSlice()) + if tt.w.WantErr() { + t.Errorf("expected error, but got none. got (%v)", tt.w.Slice()) + return } - if got, want := tt.w.outerSlice(), tt.w.inner(); !reflect.DeepEqual(got, want) { + if got, want := tt.w.Slice(), tt.w.WantSlice(); !reflect.DeepEqual(got, want) { t.Errorf("got (%v) != want (%v)", got, want) } }) @@ -268,46 +272,46 @@ func TestListUnmarshalJSON(t *testing.T) { } type listTester interface { - outer() any - inner() any - outerSlice() any - wantErr() bool - rawData() string + List() any + WantSlice() any + Slice() any + WantErr() bool + JSON() []byte } type listWrapper[T comparable] struct { - raw string - outerList List[T] - innerList []T - err bool + json string + list List[T] + slice []T + wantErr bool } -func (w *listWrapper[T]) wantErr() bool { - return w.err +func (w *listWrapper[T]) WantErr() bool { + return w.wantErr } -func (w *listWrapper[T]) outer() any { - return &w.outerList +func (w *listWrapper[T]) List() any { + return &w.list } -func (w *listWrapper[T]) outerSlice() any { - return w.outerList.Slice() +func (w *listWrapper[T]) Slice() any { + return w.list.Slice() } -func (w *listWrapper[T]) inner() any { - return w.innerList +func (w *listWrapper[T]) WantSlice() any { + return w.slice } -func (w *listWrapper[T]) rawData() string { - return w.raw +func (w *listWrapper[T]) JSON() []byte { + return []byte(w.json) } -func newListEncoder[T comparable](raw string, want []T, wantErr bool) *listWrapper[T] { - return &listWrapper[T]{raw: raw, outerList: ToList(want), err: wantErr} +func listMarshalTest[T comparable](json string, want []T, wantErr bool) *listWrapper[T] { + return &listWrapper[T]{json: json, list: ToList(want), wantErr: wantErr} } -func newListDecoder[T comparable](raw string, want []T, wantErr bool) *listWrapper[T] { - return &listWrapper[T]{raw: raw, innerList: want, err: wantErr} +func listUnmarshalTest[T comparable](json string, want []T, wantErr bool) *listWrapper[T] { + return &listWrapper[T]{json: json, slice: want, wantErr: wantErr} } type testEntry struct {