Skip to content

Commit

Permalink
Merge pull request #10 from pasqal-io/yoric/reflect
Browse files Browse the repository at this point in the history
[FEAT] Support generating a deserializer from reflection
  • Loading branch information
David Teller authored Apr 2, 2024
2 parents 98b6aff + 43ba918 commit a7af3de
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 21 deletions.
111 changes: 91 additions & 20 deletions deserialize/deserialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ import (
jsonPkg "github.com/pasqal-io/godasse/deserialize/json"
"github.com/pasqal-io/godasse/deserialize/kvlist"
"github.com/pasqal-io/godasse/deserialize/shared"
"github.com/pasqal-io/godasse/deserialize/tags"
tagsPkg "github.com/pasqal-io/godasse/deserialize/tags"
"github.com/pasqal-io/godasse/validation"
)
Expand Down Expand Up @@ -191,6 +192,40 @@ func MakeMapDeserializer[T any](options Options) (MapDeserializer[T], error) {
unmarshaler: options.Unmarshaler,
})
}
func MakeMapDeserializerFromReflect(options Options, typ reflect.Type) (MapDeserializer[any], error) {
tagName := options.MainTagName
if tagName == "" {
return nil, errors.New("missing option MainTagName")
}
var placeholder = reflect.New(typ).Elem()
staticOptions := staticOptions{
renamingTagName: tagName,
allowNested: true,
unmarshaler: options.Unmarshaler,
}

noTags := tags.Empty()
reflectDeserializer, err := makeFieldDeserializerFromReflect(options.RootPath, typ, staticOptions, &noTags, placeholder, false)

if err != nil {
return nil, err
}
return mapDeserializer[any]{
deserializer: func(value shared.Dict) (*any, error) {
out := reflect.New(typ).Elem()
input := value.AsValue()
err := reflectDeserializer(&out, input)
if err != nil {
return nil, err
}

result := out.Interface()
return &result, nil
},
options: staticOptions,
}, nil

}

// Create a deserializer from (key, value list).
func MakeKVListDeserializer[T any](options Options) (KVListDeserializer[T], error) {
Expand Down Expand Up @@ -220,6 +255,35 @@ func MakeKVListDeserializer[T any](options Options) (KVListDeserializer[T], erro
options: innerOptions,
}, nil
}
func MakeKVDeserializerFromReflect(options Options, typ reflect.Type) (KVListDeserializer[any], error) {
tagName := options.MainTagName
if tagName == "" {
return nil, errors.New("missing option MainTagName")
}
innerOptions := staticOptions{
renamingTagName: tagName,
allowNested: false,
unmarshaler: options.Unmarshaler,
}
var placeholder = reflect.New(typ).Elem().Interface()
wrapped, err := makeOuterStructDeserializerFromReflect[any](".", innerOptions, &placeholder, typ)
if err != nil {
return nil, err
}

deserializer := func(value kvlist.KVList) (*any, error) {
normalized := make(jsonPkg.JSON)
err := deListMapReflect(typ, normalized, value, innerOptions)
if err != nil {
return nil, fmt.Errorf("error attempting to deserialize from a list of entries:\n\t * %w", err)
}
return wrapped.deserializer(normalized)
}
return kvListDeserializer[any]{
deserializer: deserializer,
options: innerOptions,
}, nil
}

// An error that arises because of a bug in a custom deserializer.
type CustomDeserializerError struct {
Expand Down Expand Up @@ -312,15 +376,13 @@ func (me kvListDeserializer[T]) DeserializeKVList(value kvlist.KVList) (*T, erro

// Convert a `map[string][]string` (as provided e.g. by the query parser) into a `Dict`
// (as consumed by this parsing mechanism).
func deListMap[T any](outMap map[string]any, inMap map[string][]string, options staticOptions) error {
var fakeValue *T
reflectedT := reflect.TypeOf(fakeValue).Elem()
if reflectedT.Kind() != reflect.Struct {
return fmt.Errorf("cannot implement a MapListDeserializer without a struct, got %s", reflectedT.Name())
func deListMapReflect(typ reflect.Type, outMap map[string]any, inMap map[string][]string, options staticOptions) error {
if typ.Kind() != reflect.Struct {
return fmt.Errorf("cannot implement a MapListDeserializer without a struct, got %s", typ.Name())
}

for i := 0; i < reflectedT.NumField(); i++ {
field := reflectedT.Field(i)
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
tags, err := tagsPkg.Parse(field.Tag)
if err != nil {
// This probably cannot happen as we have already failed in makeStructDeserializerFromReflect.
Expand Down Expand Up @@ -369,14 +431,19 @@ func deListMap[T any](outMap map[string]any, inMap map[string][]string, options
case 1: // One value, we can fit it into a single entry of outMap.
outMap[*publicFieldName] = inMap[*publicFieldName][0]
default:
return fmt.Errorf("cannot fit %d elements into a single entry of field %s.%s", length, reflectedT.Name(), field.Name)
return fmt.Errorf("cannot fit %d elements into a single entry of field %s.%s", length, typ.Name(), field.Name)
}
default:
panic("This should not happen")
}
}
return nil
}
func deListMap[T any](outMap map[string]any, inMap map[string][]string, options staticOptions) error {
var placeholder T
reflectedT := reflect.TypeOf(placeholder)
return deListMapReflect(reflectedT, outMap, inMap, options)
}

// A type of deserializers using reflection to perform any conversions.
type reflectDeserializer func(slot *reflect.Value, data shared.Value) error
Expand All @@ -392,22 +459,11 @@ var errorInterface = reflect.TypeOf((*error)(nil)).Elem()

const JSON = "json"

// Construct a statically-typed deserializer.
//
// Under the hood, this uses the reflectDeserializer.
//
// - `path` a human-readable path (e.g. the name of the endpoint) or "" if you have nothing
// useful for human beings;
// - `tagName` the name of tags to use for field renamings, e.g. `query`.
func makeOuterStructDeserializer[T any](path string, options staticOptions) (*mapDeserializer[T], error) {
container := new(T) // An uninitialized container, used to extract type information and call initializer methods.

func makeOuterStructDeserializerFromReflect[T any](path string, options staticOptions, container *T, typ reflect.Type) (*mapDeserializer[T], error) {
if options.unmarshaler == nil {
return nil, errors.New("please specify an unmarshaler")
}

// Pre-check if we're going to perform initialization.
typ := reflect.TypeOf(*container)
initializationMetadata, err := initializationData(path, typ, options)
if err != nil {
return nil, err
Expand Down Expand Up @@ -462,6 +518,21 @@ func makeOuterStructDeserializer[T any](path string, options staticOptions) (*ma
return &result, nil
}

// Construct a statically-typed deserializer.
//
// Under the hood, this uses the reflectDeserializer.
//
// - `path` a human-readable path (e.g. the name of the endpoint) or "" if you have nothing
// useful for human beings;
// - `tagName` the name of tags to use for field renamings, e.g. `query`.
func makeOuterStructDeserializer[T any](path string, options staticOptions) (*mapDeserializer[T], error) {
container := new(T) // An uninitialized container, used to extract type information and call initializer methods.

// Pre-check if we're going to perform initialization.
typ := reflect.TypeOf(*container)
return makeOuterStructDeserializerFromReflect[T](path, options, container, typ)
}

// Construct a dynamically-typed deserializer for structs.
//
// - `path` the human-readable path into the data structure, used for error-reporting;
Expand Down
65 changes: 65 additions & 0 deletions deserialize/deserialize_reflect_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package deserialize_test

import (
"encoding/json"
"fmt"
"reflect"
"testing"

"github.com/pasqal-io/godasse/deserialize"
jsonPkg "github.com/pasqal-io/godasse/deserialize/json"
"gotest.tools/v3/assert"
)

func twoWaysReflect[Input any, Output any](t *testing.T, sample Input) (*Output, error) {
var placeholderOutput Output
typeOutput := reflect.TypeOf(placeholderOutput)
deserializer, err := deserialize.MakeMapDeserializerFromReflect(deserialize.Options{
Unmarshaler: jsonPkg.Driver{},
MainTagName: "json",
RootPath: "",
}, typeOutput)
if err != nil {
t.Error(err)
return nil, err //nolint:wrapcheck
}

buf, err := json.Marshal(sample)
if err != nil {
t.Error(err)
return nil, err //nolint:wrapcheck
}
dict := make(jsonPkg.JSON)
err = json.Unmarshal(buf, &dict)
if err != nil {
t.Error(err)
return nil, err //nolint:wrapcheck
}
deserialized, err := deserializer.DeserializeDict(dict)
if err != nil {
return nil, err //nolint:wrapcheck
}
typed, ok := (*deserialized).(Output)
if !ok {
return nil, fmt.Errorf("invalid type after deserailization: expected %s, got %s",
typeOutput.Name(),
reflect.TypeOf(deserialized).Elem().Name())
}
return &typed, nil
}

func TestReflectDeserializer(t *testing.T) {
type Test struct {
String string
Int int
}
sample := Test{
String: "abc",
Int: 123,
}
out, err := twoWaysReflect[Test, Test](t, sample)
if err != nil {
t.Fatal(err)
}
assert.DeepEqual(t, &sample, out)
}
3 changes: 2 additions & 1 deletion deserialize/deserialize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -535,14 +535,15 @@ func TestStructDefaultValues(t *testing.T) {
assert.Equal(t, *result, expected, "Deserialization should have inserted default values")
}

// Test behavior when parsing default value fails.
func TestStructDefaultValuesInvalidSyntax(t *testing.T) {
type PairWithDefaults[T any, U any] struct {
Left T `default:"{}"`
Right U `default:"{}"`
}
_, err := deserialize.MakeMapDeserializer[PairWithDefaults[PairWithDefaults[EmptyStruct, int], int]](deserialize.JSONOptions(""))

assert.Equal(t, err.Error(), "could not generate a deserializer for PairWithDefaults[PairWithDefaults[EmptyStruct,int]·5,int].Left with type PairWithDefaults[EmptyStruct,int]:\n\t * could not generate a deserializer for PairWithDefaults[PairWithDefaults[EmptyStruct,int]·5,int].Left.Right with type int:\n\t * cannot parse default value at PairWithDefaults[PairWithDefaults[EmptyStruct,int]·5,int].Left.Right\n\t * strconv.Atoi: parsing \"{}\": invalid syntax", "MakeMapDeserializer should have detected an error")
assert.Equal(t, err.Error(), "could not generate a deserializer for PairWithDefaults[PairWithDefaults[EmptyStruct,int]·6,int].Left with type PairWithDefaults[EmptyStruct,int]:\n\t * could not generate a deserializer for PairWithDefaults[PairWithDefaults[EmptyStruct,int]·6,int].Left.Right with type int:\n\t * cannot parse default value at PairWithDefaults[PairWithDefaults[EmptyStruct,int]·6,int].Left.Right\n\t * strconv.Atoi: parsing \"{}\": invalid syntax", "MakeMapDeserializer should have detected an error")
}

// Check that when we have a default struct of {}, we're still going to
Expand Down

0 comments on commit a7af3de

Please sign in to comment.