Skip to content

Commit

Permalink
Fix slice deep map (#1)
Browse files Browse the repository at this point in the history
* Implement deep mapping of nested slices of structs

* Add tests for  deep mapping of nested slices of structs
  • Loading branch information
ayratsa authored Nov 30, 2024
1 parent 8101c0b commit 1784735
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 6 deletions.
71 changes: 65 additions & 6 deletions mapstructure.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,13 @@ type DecoderConfig struct {
// }
Squash bool

// Deep will map structures in slices instead of copying them
//
// type Parent struct {
// Children []Child `mapstructure:",deep"`
// }
Deep bool

// Metadata is the struct that will contain extra metadata about
// the decoding. If this is nil, then no metadata will be tracked.
Metadata *Metadata
Expand Down Expand Up @@ -999,6 +1006,9 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re
// If Squash is set in the config, we squash the field down.
squash := d.config.Squash && v.Kind() == reflect.Struct && f.Anonymous

// If Deep is set in the config, set as default value.
deep := d.config.Deep

v = dereferencePtrToStructIfNeeded(v, d.config.TagName)

// Determine the name of the key in the map
Expand Down Expand Up @@ -1036,6 +1046,9 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re
continue
}
}

deep = deep || strings.Index(tagValue[index+1:], "deep") != -1

if keyNameTagValue := tagValue[:index]; keyNameTagValue != "" {
keyName = keyNameTagValue
}
Expand Down Expand Up @@ -1082,6 +1095,41 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re
valMap.SetMapIndex(reflect.ValueOf(keyName), vMap)
}

case reflect.Slice:
if deep {
var childType reflect.Type
switch v.Type().Elem().Kind() {
case reflect.Struct:
childType = reflect.TypeOf(map[string]interface{}{})
default:
childType = v.Type().Elem()
}

sType := reflect.SliceOf(childType)

addrVal := reflect.New(sType)

vSlice := reflect.MakeSlice(sType, v.Len(), v.Cap())

if v.Len() > 0 {
reflect.Indirect(addrVal).Set(vSlice)

err := d.decode(keyName, v.Interface(), reflect.Indirect(addrVal))
if err != nil {
return err
}
}

vSlice = reflect.Indirect(addrVal)

valMap.SetMapIndex(reflect.ValueOf(keyName), vSlice)

break
}

// When deep mapping is not needed, fallthrough to normal copy
fallthrough

default:
valMap.SetMapIndex(reflect.ValueOf(keyName), v)
}
Expand Down Expand Up @@ -1608,13 +1656,24 @@ func isStructTypeConvertibleToMap(typ reflect.Type, checkMapstructureTags bool,
}

func dereferencePtrToStructIfNeeded(v reflect.Value, tagName string) reflect.Value {
if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct {

if v.Kind() != reflect.Ptr {
return v
}
deref := v.Elem()
derefT := deref.Type()
if isStructTypeConvertibleToMap(derefT, true, tagName) {
return deref

switch v.Elem().Kind() {
case reflect.Slice:
return v.Elem()

case reflect.Struct:
deref := v.Elem()
derefT := deref.Type()
if isStructTypeConvertibleToMap(derefT, true, tagName) {
return deref
}
return v

default:
return v
}
return v
}
43 changes: 43 additions & 0 deletions mapstructure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3368,6 +3368,49 @@ func testArrayInput(t *testing.T, input map[string]interface{}, expected *Array)
}
}

func TestDecode_structArrayDeepMap(t *testing.T) {
type SourceChild struct {
String string `mapstructure:"some-string"`
}

type SourceParent struct {
ChildrenA []SourceChild `mapstructure:"children-a,deep"`
ChildrenB *[]SourceChild `mapstructure:"children-b,deep"`
}

var target map[string]interface{}

source := SourceParent{
ChildrenA: []SourceChild{
{String: "one"},
{String: "two"},
},
ChildrenB: &[]SourceChild{
{String: "one"},
{String: "two"},
},
}

if err := Decode(source, &target); err != nil {
t.Fatalf("got error: %s", err)
}

expected := map[string]interface{}{
"children-a": []map[string]interface{}{
{"some-string": "one"},
{"some-string": "two"},
},
"children-b": []map[string]interface{}{
{"some-string": "one"},
{"some-string": "two"},
},
}

if !reflect.DeepEqual(target, expected) {
t.Fatalf("failed: \nexpected: %#v\nresult: %#v", expected, target)
}
}

func stringPtr(v string) *string { return &v }
func intPtr(v int) *int { return &v }
func uintPtr(v uint) *uint { return &v }
Expand Down

0 comments on commit 1784735

Please sign in to comment.