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

[FEATURE] Deserialize composite structs #16

Merged
merged 1 commit into from
Nov 25, 2024
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
117 changes: 77 additions & 40 deletions deserialize/deserialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ func MakeMapDeserializerFromReflect(options Options, typ reflect.Type) (MapRefle
}

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

if err != nil {
return nil, err
Expand Down Expand Up @@ -296,7 +296,7 @@ func MakeKVDeserializerFromReflect(options Options, typ reflect.Type) (KVListRef
}
var placeholder = reflect.New(typ).Elem()
noTags := tags.Empty()
wrapped, err := makeFieldDeserializerFromReflect(".", typ, innerOptions, &noTags, placeholder, false)
wrapped, err := makeFieldDeserializerFromReflect(".", typ, innerOptions, &noTags, placeholder, false, false)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -445,11 +445,16 @@ func deListMapReflect(typ reflect.Type, outMap map[string]any, inMap map[string]
publicFieldName = &field.Name
}

switch field.Type.Kind() {
case reflect.Array:
switch {
case field.Type.Kind() == reflect.Array:
fallthrough
case reflect.Slice:
case field.Type.Kind() == reflect.Slice:
outMap[*publicFieldName] = inMap[*publicFieldName]
case field.Type.Kind() == reflect.Struct && (tags.IsFlattened() || field.Anonymous):
err = deListMapReflect(field.Type, outMap, inMap, options)
if err != nil {
return err
}
default:
length := len(inMap[*publicFieldName])
switch length {
Expand Down Expand Up @@ -632,34 +637,63 @@ func makeStructDeserializerFromReflect(path string, typ reflect.Type, options in

fieldPath := fmt.Sprint(path, ".", *publicFieldName)

var fieldContentDeserializer reflectDeserializer
fieldContentDeserializer, err = makeFieldDeserializerFromReflect(fieldPath, fieldType, options, &tags, selfContainer, willPreinitialize)
if err != nil {
return nil, err
}
fieldDeserializer := func(outPtr *reflect.Value, inMap shared.Dict) error {
// Note: maps are references, so there is no loss to passing a `map` instead of a `*map`.
// Use the `fieldName` to access the field in the record.
outReflect := outPtr.FieldByName(fieldNativeName)

// Use the `publicFieldName` to access the field in the map.
var fieldValue shared.Value
if isPublic {
// If the field is public, we can accept external data, if provided.
var ok bool
fieldValue, ok = inMap.Lookup(*publicFieldName)
if !ok {
fieldValue = nil
var fieldDeserializer func(*reflect.Value, shared.Dict) error
if tags.IsFlattened() || field.Anonymous {
// The field is flattened either explicitly (tag `flatten`) or implicitly
// (because it's an anonymous field). In either case, the *contents* of that
// struct are pulled from *the same outer map* `inMap`.

fieldContentDeserializer, err := makeFieldDeserializerFromReflect(fieldPath, fieldType, options, &tags, selfContainer, willPreinitialize, true)
if err != nil {
return nil, err
}

fieldDeserializer = func(outPtr *reflect.Value, inMap shared.Dict) error {
// Note: maps are references, so there is no loss to passing a `map` instead of a `*map`.
// Use the `fieldName` to access the field in the record.
outReflect := outPtr.FieldByName(fieldNativeName)

err := fieldContentDeserializer(&outReflect, inMap.AsValue())
if err != nil {
return err
}

// At this stage, the field has already been validated by using `Validator.Validate()`.
// In future versions, we may wish to add support for further validation using tags.
return nil
}
err := fieldContentDeserializer(&outReflect, fieldValue)

} else {
// The field is nested, so we'll try to move into the corresponding entry in the map.
fieldContentDeserializer, err := makeFieldDeserializerFromReflect(fieldPath, fieldType, options, &tags, selfContainer, willPreinitialize, false)
if err != nil {
return err
return nil, err
}

// At this stage, the field has already been validated by using `Validator.Validate()`.
// In future versions, we may wish to add support for further validation using tags.
return nil
fieldDeserializer = func(outPtr *reflect.Value, inMap shared.Dict) error {
// Note: maps are references, so there is no loss to passing a `map` instead of a `*map`.
// Use the `fieldName` to access the field in the record.
outReflect := outPtr.FieldByName(fieldNativeName)

// Use the `publicFieldName` to access the field in the map.
var fieldValue shared.Value
if isPublic {
// If the field is public, we can accept external data, if provided.
var ok bool
fieldValue, ok = inMap.Lookup(*publicFieldName)
if !ok {
fieldValue = nil
}
} // otherwise, use the zero value for that field.
err := fieldContentDeserializer(&outReflect, fieldValue)
if err != nil {
return err
}

// At this stage, the field has already been validated by using `Validator.Validate()`.
// In future versions, we may wish to add support for further validation using tags.
return nil
}
}

deserializers[field.Name] = fieldDeserializer
Expand Down Expand Up @@ -771,8 +805,8 @@ func makeStructDeserializerFromReflect(path string, typ reflect.Type, options in
}

// We may now deserialize fields.
for _, fieldDeserializationData := range deserializers {
err = fieldDeserializationData(&result, inMap)
for _, fieldDeserializer := range deserializers {
err = fieldDeserializer(&result, inMap)
if err != nil {
return err
}
Expand Down Expand Up @@ -820,7 +854,7 @@ func makeMapDeserializerFromReflect(path string, typ reflect.Type, options inner
subPath := path + "[]"
subTags := tagsPkg.Empty()
subTyp := typ.Elem()
contentDeserializer, err := makeFieldDeserializerFromReflect(subPath, subTyp, options, &subTags, selfContainer, initializationMetadata.willPreinitialize)
contentDeserializer, err := makeFieldDeserializerFromReflect(subPath, subTyp, options, &subTags, selfContainer, initializationMetadata.willPreinitialize, false)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -929,7 +963,7 @@ func makeSliceDeserializer(fieldPath string, fieldType reflect.Type, options inn

// Prepare a deserializer for elements in this slice.
childPreinitialized := wasPreinitialized || tags.IsPreinitialized()
elementDeserializer, err := makeFieldDeserializerFromReflect(arrayPath, fieldType.Elem(), options, &subTags, subContainer, childPreinitialized)
elementDeserializer, err := makeFieldDeserializerFromReflect(arrayPath, fieldType.Elem(), options, &subTags, subContainer, childPreinitialized, false)
if err != nil {
return nil, fmt.Errorf("failed to generate a deserializer for %s\n\t * %w", fieldPath, err)
}
Expand Down Expand Up @@ -1020,7 +1054,7 @@ func makePointerDeserializer(fieldPath string, fieldType reflect.Type, options i
subTags := tagsPkg.Empty()
subContainer := reflect.New(fieldType).Elem()
childPreinitialized := wasPreinitialized || tags.IsPreinitialized()
elementDeserializer, err := makeFieldDeserializerFromReflect(ptrPath, fieldType.Elem(), options, &subTags, subContainer, childPreinitialized)
elementDeserializer, err := makeFieldDeserializerFromReflect(ptrPath, fieldType.Elem(), options, &subTags, subContainer, childPreinitialized, false)
if err != nil {
return nil, fmt.Errorf("failed to generate a deserializer for %s\n\t * %w", fieldPath, err)
}
Expand Down Expand Up @@ -1251,15 +1285,18 @@ func makeFlatFieldDeserializer(fieldPath string, fieldType reflect.Type, options
// - `typ` the dynamic type for the field being compiled;
// - `tagName` the name of tags to use for field renamings, e.g. `query`;
// - `tags` the table of tags for this field.
func makeFieldDeserializerFromReflect(fieldPath string, fieldType reflect.Type, options innerOptions, tags *tagsPkg.Tags, container reflect.Value, wasPreinitialized bool) (reflectDeserializer, error) {
err := options.unmarshaler.Enter(fieldPath, fieldType)
if err != nil {
return nil, err //nolint:wrapcheck
func makeFieldDeserializerFromReflect(fieldPath string, fieldType reflect.Type, options innerOptions, tags *tagsPkg.Tags, container reflect.Value, wasPreinitialized bool, wasFlattened bool) (reflectDeserializer, error) {
if !wasFlattened {
err := options.unmarshaler.Enter(fieldPath, fieldType)
if err != nil {
return nil, err //nolint:wrapcheck
}
defer func() {
options.unmarshaler.Exit(fieldType)
}()
}
defer func() {
options.unmarshaler.Exit(fieldType)
}()

var err error
var structured reflectDeserializer

switch fieldType.Kind() {
Expand Down
89 changes: 89 additions & 0 deletions deserialize/deserialize_reflect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,29 @@ func TestReflectMapDeserializer(t *testing.T) {
assert.DeepEqual(t, &sample, out)
}

func TestReflectMapEmbeddedDeserializer(t *testing.T) {
type Inner struct {
Nested string
}
type Outer struct {
Inner
String string
Int int
}
sample := Outer{
Inner: Inner{
Nested: "def",
},
String: "abc",
Int: 123,
}
out, err := twoWaysReflect[Outer, Outer](t, sample)
if err != nil {
t.Fatal(err)
}
assert.DeepEqual(t, &sample, out)
}

func TestReflectKVDeserializer(t *testing.T) {
type Test struct {
String string
Expand All @@ -86,3 +109,69 @@ func TestReflectKVDeserializer(t *testing.T) {
assert.NilError(t, err)
assert.Equal(t, *deserialized, sample)
}

// Should be useful for books, as we wouldn't have to recreate a Pagination struct for each route for example.
Yoric marked this conversation as resolved.
Show resolved Hide resolved
func TestNestedStructReflectKVDeserializer(t *testing.T) {
type NestedStruct struct {
BBB string
}
type MainStruct struct {
AAA string
NestedStruct NestedStruct `flatten:""`
}
sample := MainStruct{
AAA: "aaa",
NestedStruct: NestedStruct{
BBB: "bbb",
},
}

deserializer, err := deserialize.MakeKVDeserializerFromReflect(deserialize.Options{
Unmarshaler: jsonPkg.Driver,
MainTagName: "json",
RootPath: "",
}, reflect.TypeOf(sample))
assert.NilError(t, err)

kvList := map[string][]string{}
kvList["AAA"] = []string{sample.AAA}
kvList["BBB"] = []string{sample.NestedStruct.BBB}

deserialized := new(MainStruct)
reflectDeserialized := reflect.ValueOf(deserialized).Elem()
err = deserializer.DeserializeKVListTo(kvList, &reflectDeserialized)
assert.NilError(t, err)
assert.Equal(t, *deserialized, sample)
}

// Not mandatory, but could be nice to have.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Not mandatory, but could be nice to have.

func TestAnonymStructReflectKVDeserializer(t *testing.T) {
type EmbeddedStruct struct {
BBB string
}
type MainStruct struct {
AAA string
EmbeddedStruct // Embedded struct are anonymous fields in reflection, flattened automatically.
}
sample := MainStruct{
AAA: "aaa",
EmbeddedStruct: EmbeddedStruct{BBB: "bbb"},
}

deserializer, err := deserialize.MakeKVDeserializerFromReflect(deserialize.Options{
Unmarshaler: jsonPkg.Driver,
MainTagName: "json",
RootPath: "",
}, reflect.TypeOf(sample))
assert.NilError(t, err)

kvList := map[string][]string{}
kvList["AAA"] = []string{sample.AAA}
kvList["BBB"] = []string{sample.BBB} // Embedded struct fields can be accessed like if it was at root level

deserialized := new(MainStruct)
reflectDeserialized := reflect.ValueOf(deserialized).Elem()
err = deserializer.DeserializeKVListTo(kvList, &reflectDeserialized)
assert.NilError(t, err)
assert.Equal(t, *deserialized, sample)
}
77 changes: 77 additions & 0 deletions deserialize/deserialize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1481,3 +1481,80 @@ func TestKVCallsInnerValidation(t *testing.T) {
_, err = deserializer.DeserializeKVList(kvlist)
assert.ErrorContains(t, err, "custom validation error")
}

// ------ Test that flattened structs are deserialized properly.
func TestMapDeserializerFlattened(t *testing.T) {
type Inner struct {
Left string
Right string
}
type Outer struct {
Flattened Inner `flatten:""`
Inner
Regular Inner
}

deserializer, err := deserialize.MakeMapDeserializer[Outer](deserialize.JSONOptions(""))
assert.NilError(t, err)

data := `
{
"Left": "flattened_left",
"Right": "flattened_right",
"Regular": {
"Left": "regular_left",
"Right": "regular_right"
}
}`
expected := Outer{
Flattened: Inner{
Left: "flattened_left",
Right: "flattened_right",
},
Inner: Inner{
Left: "flattened_left",
Right: "flattened_right",
},
Regular: Inner{
Left: "regular_left",
Right: "regular_right",
},
}
found, err := deserializer.DeserializeBytes([]byte(data))
assert.NilError(t, err)

assert.DeepEqual(t, *found, expected)
}

func TestKVDeserializerFlattened(t *testing.T) {
type Inner struct {
Left string
Right string
}
type Outer struct {
Flattened Inner `flatten:""`
Inner
}

deserializer, err := deserialize.MakeKVListDeserializer[Outer](deserialize.QueryOptions(""))
assert.NilError(t, err)

data := make(map[string][]string)
data["Left"] = []string{"flattened_left"}
data["Right"] = []string{"flattened_right"}

expected := Outer{
Flattened: Inner{
Left: "flattened_left",
Right: "flattened_right",
},
Inner: Inner{
Left: "flattened_left",
Right: "flattened_right",
},
}
found, err := deserializer.DeserializeKVList(data)
assert.NilError(t, err)

assert.DeepEqual(t, *found, expected)
}
Loading
Loading