Skip to content

Commit 6166631

Browse files
DarkiTsagikazarmark
authored andcommitted
fix(mapstructure): add multi-tag support and regression tests
1 parent 6471aa6 commit 6166631

File tree

2 files changed

+113
-9
lines changed

2 files changed

+113
-9
lines changed

mapstructure.go

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -699,7 +699,7 @@ func (d *Decoder) decodeString(name string, data any, val reflect.Value) error {
699699
case reflect.Uint8:
700700
var uints []uint8
701701
if dataKind == reflect.Array {
702-
uints = make([]uint8, dataVal.Len(), dataVal.Len())
702+
uints = make([]uint8, dataVal.Len())
703703
for i := range uints {
704704
uints[i] = dataVal.Index(i).Interface().(uint8)
705705
}
@@ -1091,7 +1091,7 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re
10911091
)
10921092
}
10931093

1094-
tagValue := f.Tag.Get(d.config.TagName)
1094+
tagValue, _ := getTagValue(f, d.config.TagName)
10951095
keyName := d.config.MapFieldName(f.Name)
10961096

10971097
if tagValue == "" && d.config.IgnoreUntaggedFields {
@@ -1112,12 +1112,12 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re
11121112
continue
11131113
}
11141114
// If "omitempty" is specified in the tag, it ignores empty values.
1115-
if strings.Index(tagValue[index+1:], "omitempty") != -1 && isEmptyValue(v) {
1115+
if strings.Contains(tagValue[index+1:], "omitempty") && isEmptyValue(v) {
11161116
continue
11171117
}
11181118

11191119
// If "omitzero" is specified in the tag, it ignores zero values.
1120-
if strings.Index(tagValue[index+1:], "omitzero") != -1 && v.IsZero() {
1120+
if strings.Contains(tagValue[index+1:], "omitzero") && v.IsZero() {
11211121
continue
11221122
}
11231123

@@ -1137,7 +1137,7 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re
11371137
)
11381138
}
11391139
} else {
1140-
if strings.Index(tagValue[index+1:], "remain") != -1 {
1140+
if strings.Contains(tagValue[index+1:], "remain") {
11411141
if v.Kind() != reflect.Map {
11421142
return newDecodeError(
11431143
name+"."+f.Name,
@@ -1153,7 +1153,7 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re
11531153
}
11541154
}
11551155

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

11581158
if keyNameTagValue := tagValue[:index]; keyNameTagValue != "" {
11591159
keyName = keyNameTagValue
@@ -1543,7 +1543,10 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
15431543
remain := false
15441544

15451545
// We always parse the tags cause we're looking for other tags too
1546-
tagParts := strings.Split(fieldType.Tag.Get(d.config.TagName), ",")
1546+
tagParts := getTagParts(fieldType, d.config.TagName)
1547+
if len(tagParts) == 0 {
1548+
tagParts = []string{""}
1549+
}
15471550
for _, tag := range tagParts[1:] {
15481551
if tag == d.config.SquashTagOption {
15491552
squash = true
@@ -1600,7 +1603,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
16001603
field, fieldValue := f.field, f.val
16011604
fieldName := field.Name
16021605

1603-
tagValue := field.Tag.Get(d.config.TagName)
1606+
tagValue, _ := getTagValue(field, d.config.TagName)
16041607
if tagValue == "" && d.config.IgnoreUntaggedFields {
16051608
continue
16061609
}
@@ -1784,7 +1787,7 @@ func isStructTypeConvertibleToMap(typ reflect.Type, checkMapstructureTags bool,
17841787
if f.PkgPath == "" && !checkMapstructureTags { // check for unexported fields
17851788
return true
17861789
}
1787-
if checkMapstructureTags && f.Tag.Get(tagName) != "" { // check for mapstructure tags inside
1790+
if checkMapstructureTags && hasAnyTag(f, tagName) { // check for mapstructure tags inside
17881791
return true
17891792
}
17901793
}
@@ -1812,3 +1815,36 @@ func dereferencePtrToStructIfNeeded(v reflect.Value, tagName string) reflect.Val
18121815
return v
18131816
}
18141817
}
1818+
1819+
func hasAnyTag(field reflect.StructField, tagName string) bool {
1820+
_, ok := getTagValue(field, tagName)
1821+
return ok
1822+
}
1823+
1824+
func getTagParts(field reflect.StructField, tagName string) []string {
1825+
tagValue, ok := getTagValue(field, tagName)
1826+
if !ok {
1827+
return nil
1828+
}
1829+
return strings.Split(tagValue, ",")
1830+
}
1831+
1832+
func getTagValue(field reflect.StructField, tagName string) (string, bool) {
1833+
for _, name := range splitTagNames(tagName) {
1834+
if tag := field.Tag.Get(name); tag != "" {
1835+
return tag, true
1836+
}
1837+
}
1838+
return "", false
1839+
}
1840+
1841+
func splitTagNames(tagName string) []string {
1842+
if tagName == "" {
1843+
return []string{"mapstructure"}
1844+
}
1845+
parts := strings.Split(tagName, ",")
1846+
for i, name := range parts {
1847+
parts[i] = strings.TrimSpace(name)
1848+
}
1849+
return parts
1850+
}

mapstructure_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3235,6 +3235,74 @@ func TestDecoder_IgnoreUntaggedFieldsWithStruct(t *testing.T) {
32353235
}
32363236
}
32373237

3238+
func TestDecoder_MultiTagInline(t *testing.T) {
3239+
type Inner struct {
3240+
A int `yaml:"a"`
3241+
}
3242+
3243+
type Wrap struct {
3244+
Inner `yaml:",inline"`
3245+
}
3246+
3247+
input := map[string]any{"a": 1}
3248+
var result Wrap
3249+
3250+
dec, err := NewDecoder(&DecoderConfig{
3251+
TagName: "config,yaml",
3252+
SquashTagOption: "inline",
3253+
WeaklyTypedInput: true,
3254+
Result: &result,
3255+
})
3256+
if err != nil {
3257+
t.Fatalf("NewDecoder error: %v", err)
3258+
}
3259+
3260+
if err := dec.Decode(input); err != nil {
3261+
t.Fatalf("Decode error: %v", err)
3262+
}
3263+
3264+
if result.Inner.A != 1 {
3265+
t.Fatalf("expected inline field A=1, got %d", result.Inner.A)
3266+
}
3267+
}
3268+
3269+
func TestDecoder_MultiTagRemain(t *testing.T) {
3270+
type Wrap struct {
3271+
Known string `yaml:"known"`
3272+
Extra map[string]any `yaml:",remain"`
3273+
}
3274+
3275+
input := map[string]any{
3276+
"known": "ok",
3277+
"extra1": "v1",
3278+
"extra2": 2,
3279+
}
3280+
var result Wrap
3281+
3282+
dec, err := NewDecoder(&DecoderConfig{
3283+
TagName: "config,yaml",
3284+
WeaklyTypedInput: true,
3285+
Result: &result,
3286+
})
3287+
if err != nil {
3288+
t.Fatalf("NewDecoder error: %v", err)
3289+
}
3290+
3291+
if err := dec.Decode(input); err != nil {
3292+
t.Fatalf("Decode error: %v", err)
3293+
}
3294+
3295+
if result.Known != "ok" {
3296+
t.Fatalf("expected Known=ok, got %q", result.Known)
3297+
}
3298+
if result.Extra == nil || len(result.Extra) != 2 {
3299+
t.Fatalf("expected Extra to contain 2 items, got %v", result.Extra)
3300+
}
3301+
if result.Extra["extra1"] != "v1" {
3302+
t.Fatalf("expected extra1=v1, got %v", result.Extra["extra1"])
3303+
}
3304+
}
3305+
32383306
func TestDecoder_DecodeNilOption(t *testing.T) {
32393307
t.Parallel()
32403308

0 commit comments

Comments
 (0)