Skip to content
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 [un]marshaling for List types #279

Merged
merged 12 commits into from
Dec 28, 2024
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
1 change: 1 addition & 0 deletions cm/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
60 changes: 59 additions & 1 deletion cm/list.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package cm

import "unsafe"
import (
"bytes"
"encoding/json"
"unsafe"
)

// List represents a Component Model list.
// The binary representation of list<T> is similar to a Go slice minus the cap field.
Expand Down Expand Up @@ -58,3 +62,57 @@ 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) {
if l.len == 0 {
return []byte("[]"), nil
}

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]

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.
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
}

// 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")
Loading
Loading