Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
`Get`/`Put`/`Delete`/`Range`/`Watch` implemented as thin `Tx`
wrappers, and `Codec[T].Bind` for cheap binding. Multi-codec
transactions lower to a single storage call.
- marshaller: `TypedJSONMarshaller[T]` for `encoding/json`-based
marshalling and `TypedBytesMarshaller` passthrough
`TypedMarshaller[[]byte]` for values that are already serialized or
stored as opaque blobs.

### Changed

Expand Down
52 changes: 52 additions & 0 deletions marshaller/typed.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package marshaller

import (
"encoding/json"

"gopkg.in/yaml.v3"
)

Expand Down Expand Up @@ -33,3 +35,53 @@ func (m TypedYamlMarshaller[T]) Unmarshal(data []byte) (T, error) {

return out, nil
}

// TypedJSONMarshaller is a generic JSON marshaller for typed objects.
type TypedJSONMarshaller[T any] struct{}

// NewTypedJSONMarshaller creates a new TypedJSONMarshaller for the specified type.
func NewTypedJSONMarshaller[T any]() TypedJSONMarshaller[T] {
return TypedJSONMarshaller[T]{}
}

// Marshal serializes the typed data to JSON format.
func (m TypedJSONMarshaller[T]) Marshal(data T) ([]byte, error) {
marshalled, err := json.Marshal(data)
if err != nil {
return []byte{}, errMarshal(err)
}

return marshalled, nil
}

// Unmarshal deserializes JSON data into a typed object.
func (m TypedJSONMarshaller[T]) Unmarshal(data []byte) (T, error) {
var out T

err := json.Unmarshal(data, &out)
if err != nil {
return zero[T](), errUnmarshal(err)
}

return out, nil
}

// TypedBytesMarshaller is a passthrough marshaller for raw []byte payloads.
// It implements TypedMarshaller[[]byte] without performing any encoding,
// useful when values are already serialized or stored as opaque blobs.
type TypedBytesMarshaller struct{}

// NewTypedBytesMarshaller creates a new TypedBytesMarshaller.
func NewTypedBytesMarshaller() TypedBytesMarshaller {
return TypedBytesMarshaller{}
}

// Marshal returns the input bytes unchanged.
func (m TypedBytesMarshaller) Marshal(data []byte) ([]byte, error) {
return data, nil
}

// Unmarshal returns the input bytes unchanged.
func (m TypedBytesMarshaller) Unmarshal(data []byte) ([]byte, error) {
return data, nil
}
101 changes: 95 additions & 6 deletions marshaller/typed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ import (
)

type TestStruct struct {
Name string `yaml:"name"`
Value int `yaml:"value"`
Tags []string `yaml:"tags,omitempty"`
Name string `json:"name" yaml:"name"`
Value int `json:"value" yaml:"value"`
Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"`
}

type NestedStruct struct {
ID int `yaml:"id"`
Data TestStruct `yaml:"data"`
Active bool `yaml:"active"`
ID int `json:"id" yaml:"id"`
Data TestStruct `json:"data" yaml:"data"`
Active bool `json:"active" yaml:"active"`
}

func TestTypedYamlMarshaller_New(t *testing.T) {
Expand Down Expand Up @@ -241,3 +241,92 @@ func TestTypedYamlMarshaller_WithSliceType(t *testing.T) {
require.NoError(t, err)
require.Equal(t, original, unmarshaled)
}

func TestTypedJSONMarshaller_RoundTrip(t *testing.T) {
t.Parallel()

marsh := marshaller.NewTypedJSONMarshaller[TestStruct]()

original := TestStruct{
Name: "roundtrip",
Value: 99,
Tags: []string{"a", "b", "c"},
}

marshaled, err := marsh.Marshal(original)
require.NoError(t, err)
require.JSONEq(t, `{"name":"roundtrip","value":99,"tags":["a","b","c"]}`, string(marshaled))

unmarshaled, err := marsh.Unmarshal(marshaled)
require.NoError(t, err)
require.Equal(t, original, unmarshaled)
}

func TestTypedJSONMarshaller_NestedStruct(t *testing.T) {
t.Parallel()

marsh := marshaller.NewTypedJSONMarshaller[NestedStruct]()

original := NestedStruct{
ID: 1,
Data: TestStruct{
Name: "nested",
Value: 100,
Tags: nil,
},
Active: true,
}

marshaled, err := marsh.Marshal(original)
require.NoError(t, err)

unmarshaled, err := marsh.Unmarshal(marshaled)
require.NoError(t, err)
require.Equal(t, original, unmarshaled)
}

func TestTypedJSONMarshaller_Unmarshal_InvalidJSON(t *testing.T) {
t.Parallel()

marsh := marshaller.NewTypedJSONMarshaller[TestStruct]()

_, err := marsh.Unmarshal([]byte(`{not json}`))
require.Error(t, err)
require.Contains(t, err.Error(), "Failed to unmarshal")
}

func TestTypedBytesMarshaller_Passthrough(t *testing.T) {
t.Parallel()

marsh := marshaller.NewTypedBytesMarshaller()

original := []byte{0x00, 0x01, 0x02, 0xff}

marshaled, err := marsh.Marshal(original)
require.NoError(t, err)
require.Equal(t, original, marshaled)

unmarshaled, err := marsh.Unmarshal(marshaled)
require.NoError(t, err)
require.Equal(t, original, unmarshaled)
}

func TestTypedBytesMarshaller_Nil(t *testing.T) {
t.Parallel()

marsh := marshaller.NewTypedBytesMarshaller()

marshaled, err := marsh.Marshal(nil)
require.NoError(t, err)
require.Nil(t, marshaled)

unmarshaled, err := marsh.Unmarshal(nil)
require.NoError(t, err)
require.Nil(t, unmarshaled)
}

func TestTypedBytesMarshaller_ImplementsTypedMarshaller(t *testing.T) {
t.Parallel()

var _ marshaller.TypedMarshaller[[]byte] = marshaller.NewTypedBytesMarshaller()
}
Loading