Skip to content

Commit

Permalink
cm: implement JSON [un]marshaling for List types
Browse files Browse the repository at this point in the history
Merge pull request #279 from bytecodealliance/ydnar/serialize-list
  • Loading branch information
ydnar authored Dec 28, 2024
2 parents db23ebb + 411bd9a commit 9e12543
Show file tree
Hide file tree
Showing 5 changed files with 387 additions and 12 deletions.
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

0 comments on commit 9e12543

Please sign in to comment.