summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2022-04-18 01:30:40 +0700
committerShulhan <ms@kilabit.info>2022-04-18 01:35:59 +0700
commitf06c0d023a1ddd7b2d384995d975fe303d198ca9 (patch)
tree06d3a1d1a9878000012f033a1e1576eb759e6001
parentceba0704c6fcc2c3dd82876f6c45a3b24fe98fd6 (diff)
downloadpakakeh.go-f06c0d023a1ddd7b2d384995d975fe303d198ca9.tar.xz
lib/ini: implement marshaling and unmarshaling map with struct element
For a field F with type map[K]S `ini:"sec"`, where K is string and S is a struct or pointer to struct element, marshaling the field F will result in the following ini format, [sec "K"] <S.Field.Tag> = <S.Field.Value> Each field in struct S unmarshaled normally as "key = value". This rule is also applied when unmarshalling from ini text into map[K]V. This implementation allow multiple section with dynamic subsections as key.
-rw-r--r--lib/ini/doc.go41
-rw-r--r--lib/ini/ini.go39
-rw-r--r--lib/ini/ini_example_test.go192
-rw-r--r--lib/ini/ini_unmarshal.go173
-rw-r--r--lib/ini/tag_struct_field.go10
-rw-r--r--lib/ini/tag_struct_field_test.go8
6 files changed, 336 insertions, 127 deletions
diff --git a/lib/ini/doc.go b/lib/ini/doc.go
index 3f55e337..d7cf62e3 100644
--- a/lib/ini/doc.go
+++ b/lib/ini/doc.go
@@ -2,31 +2,30 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-//
// Package ini implement reading and writing INI text format as defined by
// Git configuration file syntax.
//
-// Features
+// # Features
//
// * Reading and writing on the same file should not change the content of
// file (including comment).
//
// * Template friendly, through Val(), Vals(), and Subs().
//
-// Unsupported features
+// # Unsupported features
//
// Git "include" and "includeIf" directives.
//
// In Git specification, an empty variable is equal to boolean true. This
// cause inconsistency between empty string and boolean true.
//
-// Syntax
+// # Syntax
//
// The '#' and ';' characters begin comments to the end of line.
//
// Blank lines are ignored.
//
-// Section
+// # Section
//
// A section begins with the name of the section in square brackets.
//
@@ -50,7 +49,7 @@
// section header) are recognized as setting variables, in the form
// `name = value`.
//
-// Subsection
+// # Subsection
//
// To begin a subsection put its name in double quotes, separated by
// space from the section name, in the section header, for example
@@ -67,7 +66,7 @@
// reading subsection name; for example, `\t` is read as `t` and `\0` is read
// as `0`.
//
-// Variable
+// # Variable
//
// Variable name must start with an alphabetic character.
//
@@ -80,7 +79,7 @@
// This ini library add extension to allow dot ('.') and underscore ('_')
// characters on variable name.
//
-// Value
+// # Value
//
// Value can be empty or not set.
// (EXT) Variable name without value is a short-hand to set the value to the
@@ -89,13 +88,12 @@
// [section]
// thisisempty # equal to thisisempty=
//
-//
// Internal whitespaces within the value are retained verbatim.
// Leading and trailing whitespaces on value without double quote will
// be discarded.
//
-// key = multiple strings # equal to "multiple strings"
-// key = " multiple strings " # equal to " multiple strings "
+// key = multiple strings # equal to "multiple strings"
+// key = " multiple strings " # equal to " multiple strings "
//
// Value can be continued to the next line by ending it with a backslash '\'
// character, the backquote and the end-of-line are stripped.
@@ -120,7 +118,7 @@
// Other char escape sequences (including octal escape sequences) are
// invalid.
//
-// Marshaling
+// # Marshaling
//
// The container to be passed when marshaling must be struct type.
// Each exported field in the struct with "ini" tags will be marshaled based
@@ -156,6 +154,7 @@
// SliceStruct []U `ini:"slice:struct"
// Map map[string]int `ini:"amap:"
// MapSub map[string]string `ini:"amap:sub"
+// MapStruct map[string]U `ini:"mapstruct"`
// }
//
// will be marshaled into
@@ -188,12 +187,24 @@
// ...
// <T.MapSub.Key[n]> = <T.MapSub.Value[n]>
//
-// Unmarshaling
+// ## On map[string]struct, each key become a subsection.
+// [mapstruct "<map.key[0]>"]
+// <U.Field[0]> = <U.Value[0]>
+// ...
+// <U.Field[n]> = <U.Value[n]>
+//
+// ...
+//
+// [mapstruct "<map.key[n]>"]
+// <U.Field[0]> = <U.Value[0]>
+// ...
+// <U.Field[n]> = <U.Value[n]>
+//
+// # Unmarshaling
//
// The syntax and rules for unmarshaling is equal to the marshaling.
//
-// References
+// # References
//
// https://git-scm.com/docs/git-config#_configuration_file
-//
package ini
diff --git a/lib/ini/ini.go b/lib/ini/ini.go
index fdaf4abc..7094b6ab 100644
--- a/lib/ini/ini.go
+++ b/lib/ini/ini.go
@@ -191,20 +191,39 @@ func (in *Ini) marshalStruct(
}
case reflect.Map:
- amap := map[string]string{}
- keys := make([]string, 0)
- iter := fvalue.MapRange()
+ var (
+ amap = map[string]reflect.Value{}
+ keys = make([]string, 0)
+ iter = fvalue.MapRange()
+
+ mapKey reflect.Value
+ mapValue reflect.Value
+ valueType reflect.Type
+ key string
+ )
+ // Collect all the map keys and sort it to make the
+ // output consistent.
for iter.Next() {
- mk := iter.Key()
- mv := iter.Value()
- key = strings.ToLower(fmt.Sprintf("%v", mk))
- value = fmt.Sprintf("%v", mv)
- amap[key] = value
+ mapKey = iter.Key()
+ mapValue = iter.Value()
+ key = strings.ToLower(fmt.Sprintf("%v", mapKey))
keys = append(keys, key)
+ amap[key] = mapValue
}
sort.Strings(keys)
for _, key = range keys {
- in.Set(sec, sub, key, amap[key])
+ mapValue = amap[key]
+ valueType = reflect.TypeOf(mapValue.Interface())
+ for valueType.Kind() == reflect.Ptr {
+ valueType = valueType.Elem()
+ mapValue = mapValue.Elem()
+ }
+ if valueType.Kind() == reflect.Struct {
+ in.marshalStruct(valueType, mapValue, sec, key)
+ } else {
+ value = fmt.Sprintf("%v", mapValue)
+ in.Set(sec, sub, key, value)
+ }
}
case reflect.Ptr:
@@ -280,7 +299,7 @@ func (in *Ini) Unmarshal(v interface{}) (err error) {
return fmt.Errorf("ini: Unmarshal: expecting pointer to struct, got %v", kind)
}
- tagField := unpackStruct(rtipe, rvalue)
+ tagField := unpackTagStructField(rtipe, rvalue)
in.unmarshal(tagField, rtipe, rvalue)
return nil
diff --git a/lib/ini/ini_example_test.go b/lib/ini/ini_example_test.go
index 0b7f11b5..d3b64143 100644
--- a/lib/ini/ini_example_test.go
+++ b/lib/ini/ini_example_test.go
@@ -176,9 +176,6 @@ func ExampleMarshal() {
PtrTime *time.Time `ini:"section:pointer:time" layout:"2006-01-02 15:04:05"`
PtrStruct *U `ini:"pointer:struct"`
- MapString map[string]string `ini:"map:string"`
- MapInt map[string]int `ini:"map:int"`
-
String string `ini:"section::string"`
SliceString []string `ini:"section:slice:string"`
@@ -207,13 +204,6 @@ func ExampleMarshal() {
Int: 3,
},
- MapString: map[string]string{
- "k": "v",
- },
- MapInt: map[string]int{
- "keyInt": 6,
- },
-
String: "a",
SliceString: []string{"c", "d"},
@@ -263,12 +253,6 @@ func ExampleMarshal() {
// string = PtrStruct.String
// int = 3
//
- // [map "string"]
- // k = v
- //
- // [map "int"]
- // keyint = 6
- //
// [section "slice"]
// string = c
// string = d
@@ -292,6 +276,108 @@ func ExampleMarshal() {
// int = 2
}
+func ExampleMarshal_map() {
+ type U struct {
+ String string `ini:"string"`
+ Int int `ini:"int"`
+ }
+ type ADT struct {
+ MapString map[string]string `ini:"map:subString"`
+ MapPtrString map[string]*string `ini:"map:subPtrString"`
+
+ MapInt map[string]int `ini:"map:subInt"`
+ MapPtrInt map[string]*int `ini:"map:subPtrInt"`
+
+ MapStruct map[string]U `ini:"mapStruct"`
+ MapPtrStruct map[string]*U `ini:"mapPtrStruct"`
+ }
+
+ var (
+ stringV = "v"
+ stringV2 = "v2"
+ intV = 6
+
+ t = ADT{
+ MapString: map[string]string{
+ "k": "v",
+ "k2": "v2",
+ },
+ MapPtrString: map[string]*string{
+ "k": &stringV,
+ "k2": &stringV2,
+ },
+
+ MapInt: map[string]int{
+ "keyInt": 6,
+ },
+ MapPtrInt: map[string]*int{
+ "keyInt": &intV,
+ },
+
+ MapStruct: map[string]U{
+ "struct-key-1": {
+ String: "struct-1-string",
+ Int: 1,
+ },
+ "struct-key-2": {
+ String: "struct-2-string",
+ Int: 2,
+ },
+ },
+ MapPtrStruct: map[string]*U{
+ "ptr-struct-key-1": {
+ String: "struct-1-string",
+ Int: 1,
+ },
+ "ptr-struct-key-2": {
+ String: "struct-2-string",
+ Int: 2,
+ },
+ },
+ }
+
+ iniText []byte
+ err error
+ )
+
+ iniText, err = Marshal(&t)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ fmt.Println(string(iniText))
+ //Output:
+ //[map "subString"]
+ //k = v
+ //k2 = v2
+ //
+ //[map "subPtrString"]
+ //k = v
+ //k2 = v2
+ //
+ //[map "subInt"]
+ //keyint = 6
+ //
+ //[map "subPtrInt"]
+ //keyint = 6
+ //
+ //[mapstruct "struct-key-1"]
+ //string = struct-1-string
+ //int = 1
+ //
+ //[mapstruct "struct-key-2"]
+ //string = struct-2-string
+ //int = 2
+ //
+ //[mapptrstruct "ptr-struct-key-1"]
+ //string = struct-1-string
+ //int = 1
+ //
+ //[mapptrstruct "ptr-struct-key-2"]
+ //string = struct-2-string
+ //int = 2
+}
+
func ExampleUnmarshal() {
iniText := `
[section]
@@ -319,12 +405,6 @@ int = 1
string = U.string 2
int = 2
-[map "string"]
-k = v
-
-[map "int"]
-k = 6
-
[section "pointer"]
string = b
int = 2
@@ -343,9 +423,6 @@ int = 2
PtrTime *time.Time `ini:"section:pointer:time" layout:"2006-01-02 15:04:05"`
PtrStruct *U `ini:"pointer:struct"`
- MapString map[string]string `ini:"map:string"`
- MapInt map[string]int `ini:"map:int"`
-
String string `ini:"section::string"`
SliceString []string `ini:"section:slice:string"`
@@ -380,8 +457,6 @@ int = 2
fmt.Printf("SliceUint: %v\n", t.SliceUint)
fmt.Printf("SliceBool: %v\n", t.SliceBool)
fmt.Printf("SliceStruct: %v\n", t.SliceStruct)
- fmt.Printf("MapString: %v\n", t.MapString)
- fmt.Printf("MapInt: %v\n", t.MapInt)
fmt.Printf("PtrString: %v\n", *t.PtrString)
fmt.Printf("PtrInt: %v\n", *t.PtrInt)
// Output:
@@ -395,12 +470,71 @@ int = 2
// SliceUint: [4 5]
// SliceBool: [true false]
// SliceStruct: [{U.string 1 1} {U.string 2 2}]
- // MapString: map[k:v]
- // MapInt: map[k:6]
// PtrString: b
// PtrInt: 2
}
+func ExampleUnmarshal_map() {
+ type U struct {
+ String string `ini:"string"`
+ Int int `ini:"int"`
+ }
+
+ type ADT struct {
+ MapString map[string]string `ini:"map:string"`
+ MapInt map[string]int `ini:"map:int"`
+ MapStruct map[string]U `ini:"mapstruct"`
+ MapPtrStruct map[string]*U `ini:"mapptrstruct"`
+ }
+
+ var (
+ iniText = `
+[map "string"]
+k = v
+k2 = v2
+
+[map "int"]
+k = 6
+k2 = 7
+
+[mapstruct "struct-key-1"]
+string = struct-1-string
+int = 1
+
+[mapstruct "struct-key-2"]
+string = struct-2-string
+int = 2
+
+[mapptrstruct "struct-key-1"]
+string = struct-1-string
+int = 1
+
+[mapptrstruct "struct-key-2"]
+string = struct-2-string
+int = 2
+`
+ t = ADT{}
+ err error
+ )
+
+ err = Unmarshal([]byte(iniText), &t)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ fmt.Printf("MapString: %v\n", t.MapString)
+ fmt.Printf("MapInt: %v\n", t.MapInt)
+ fmt.Printf("MapStruct: %v\n", t.MapStruct)
+ fmt.Printf("MapPtrStruct: struct-key-1: %v\n", t.MapPtrStruct["struct-key-1"])
+ fmt.Printf("MapPtrStruct: struct-key-2: %v\n", t.MapPtrStruct["struct-key-2"])
+ //Output:
+ //MapString: map[k:v k2:v2]
+ //MapInt: map[k:6 k2:7]
+ //MapStruct: map[struct-key-1:{struct-1-string 1} struct-key-2:{struct-2-string 2}]
+ //MapPtrStruct: struct-key-1: &{struct-1-string 1}
+ //MapPtrStruct: struct-key-2: &{struct-2-string 2}
+}
+
func ExampleIni_Prune() {
input := []byte(`
[section]
diff --git a/lib/ini/ini_unmarshal.go b/lib/ini/ini_unmarshal.go
index c3fc22d1..bb16893f 100644
--- a/lib/ini/ini_unmarshal.go
+++ b/lib/ini/ini_unmarshal.go
@@ -11,92 +11,141 @@ import (
"time"
)
-//
// unmarshal set each section-subsection variables into the struct
// fields.
-//
func (in *Ini) unmarshal(tagField tagStructField, rtype reflect.Type, rval reflect.Value) {
- for _, sec := range in.secs {
- tag := fmt.Sprintf("%s:%s", sec.name, sec.sub)
- sfield, ok := tagField[tag]
- if ok {
- switch sfield.fkind {
- case reflect.Map:
- // V map[S]T `ini:"section:sub"`
- unmarshalToMap(sec, sfield.ftype, sfield.fval)
-
- case reflect.Ptr:
- for sfield.fkind == reflect.Ptr {
- sfield.ftype = sfield.ftype.Elem()
- sfield.fkind = sfield.ftype.Kind()
- }
-
- if sfield.fkind == reflect.Struct {
- if sfield.fval.IsNil() {
- ptrfval := reflect.New(sfield.ftype)
- sfield.fval.Set(ptrfval)
- sfield.fval = ptrfval.Elem()
- } else {
- sfield.fval = sfield.fval.Elem()
+ var (
+ sec *Section
+ sfield *structField
+ v *variable
+ tag string
+ ok bool
+ )
+ for _, sec = range in.secs {
+ // Search field that tagged with subsection first.
+ tag = fmt.Sprintf("%s:%s", sec.nameLower, sec.sub)
+ sfield, ok = tagField[tag]
+ if !ok {
+ // Search field that tagged with section name only.
+ tag = sec.nameLower
+ sfield, ok = tagField[tag]
+ if !ok {
+ // Unmarshal each variable in section-sub into
+ // field directly.
+ for _, v = range sec.vars {
+ tag = fmt.Sprintf("%s:%s:%s", sec.nameLower, sec.sub, v.keyLower)
+ sfield, ok = tagField[tag]
+ if !ok {
+ continue
}
- unmarshalToStruct(sec, sfield.ftype, sfield.fval)
+ sfield.set(v.value)
}
+ continue
+ }
+ }
+ switch sfield.fkind {
+ case reflect.Map:
+ unmarshalToMap(sec, sfield.ftype, sfield.fval)
- case reflect.Slice:
- sliceElem := sfield.ftype.Elem()
- switch sliceElem.Kind() {
- case reflect.Struct:
- newStruct := reflect.New(sliceElem)
- unmarshalToStruct(sec, sliceElem, newStruct.Elem())
- newSlice := reflect.Append(sfield.fval, newStruct.Elem())
- sfield.fval.Set(newSlice)
-
- case reflect.Ptr:
- // V []*T
- for sliceElem.Kind() == reflect.Ptr {
- sliceElem = sliceElem.Elem()
- }
+ case reflect.Ptr:
+ for sfield.fkind == reflect.Ptr {
+ sfield.ftype = sfield.ftype.Elem()
+ sfield.fkind = sfield.ftype.Kind()
+ }
- if sliceElem.Kind() == reflect.Struct {
- ptrfval := reflect.New(sliceElem)
- unmarshalToStruct(sec, sliceElem, ptrfval.Elem())
- newSlice := reflect.Append(sfield.fval, ptrfval)
- sfield.fval.Set(newSlice)
- }
+ if sfield.fkind == reflect.Struct {
+ if sfield.fval.IsNil() {
+ ptrfval := reflect.New(sfield.ftype)
+ sfield.fval.Set(ptrfval)
+ sfield.fval = ptrfval.Elem()
+ } else {
+ sfield.fval = sfield.fval.Elem()
}
-
- case reflect.Struct:
unmarshalToStruct(sec, sfield.ftype, sfield.fval)
}
- continue
- }
- for _, v := range sec.vars {
- tag = fmt.Sprintf("%s:%s:%s", sec.name, sec.sub, v.keyLower)
- sfield, ok = tagField[tag]
- if !ok {
- continue
+ case reflect.Slice:
+ sliceElem := sfield.ftype.Elem()
+ switch sliceElem.Kind() {
+ case reflect.Struct:
+ newStruct := reflect.New(sliceElem)
+ unmarshalToStruct(sec, sliceElem, newStruct.Elem())
+ newSlice := reflect.Append(sfield.fval, newStruct.Elem())
+ sfield.fval.Set(newSlice)
+
+ case reflect.Ptr:
+ // V []*T
+ for sliceElem.Kind() == reflect.Ptr {
+ sliceElem = sliceElem.Elem()
+ }
+
+ if sliceElem.Kind() == reflect.Struct {
+ ptrfval := reflect.New(sliceElem)
+ unmarshalToStruct(sec, sliceElem, ptrfval.Elem())
+ newSlice := reflect.Append(sfield.fval, ptrfval)
+ sfield.fval.Set(newSlice)
+ }
}
- sfield.set(v.value)
+
+ case reflect.Struct:
+ unmarshalToStruct(sec, sfield.ftype, sfield.fval)
}
+
}
}
+// unmarshalToMap unmarshal the Section into a map.
+//
+// V map[S]T `ini:"section:sub"` for non-struct value or
+// V map[S]T `ini:"section"` for map of struct.
func unmarshalToMap(sec *Section, rtype reflect.Type, rval reflect.Value) bool {
if rtype.Key().Kind() != reflect.String {
return false
}
- amap := reflect.MakeMap(rtype)
- mapType := rtype.Elem()
+ var (
+ elType = rtype.Elem()
+ elKind = elType.Kind()
- for _, v := range sec.vars {
+ v *variable
+ amap reflect.Value
+ astruct reflect.Value
+ mapValue reflect.Value
+ isPtr bool
+ ok bool
+ )
+
+ if rval.IsNil() {
+ amap = reflect.MakeMap(rtype)
+ rval.Set(amap)
+ } else {
+ amap = rval
+ }
+ for elKind == reflect.Ptr {
+ elType = elType.Elem()
+ elKind = elType.Kind()
+ isPtr = true
+ }
+ if elKind == reflect.Struct {
+ astruct = reflect.New(elType)
+
+ unmarshalToStruct(sec, elType, astruct.Elem())
+ if isPtr {
+ amap.SetMapIndex(reflect.ValueOf(sec.sub), astruct)
+ } else {
+ amap.SetMapIndex(reflect.ValueOf(sec.sub), astruct.Elem())
+ }
+ return true
+ }
+
+ for _, v = range sec.vars {
if len(v.keyLower) == 0 {
continue
}
- rval, ok := unmarshalValue(mapType, v.value)
+
+ mapValue, ok = unmarshalValue(elType, v.value)
if ok {
- amap.SetMapIndex(reflect.ValueOf(v.keyLower), rval)
+ amap.SetMapIndex(reflect.ValueOf(v.keyLower), mapValue)
}
}
rval.Set(amap)
@@ -104,7 +153,7 @@ func unmarshalToMap(sec *Section, rtype reflect.Type, rval reflect.Value) bool {
}
func unmarshalToStruct(sec *Section, rtype reflect.Type, rval reflect.Value) {
- tagField := unpackStruct(rtype, rval)
+ tagField := unpackTagStructField(rtype, rval)
for _, v := range sec.vars {
sfield := tagField.getByKey(v.keyLower)
if sfield == nil {
@@ -114,10 +163,8 @@ func unmarshalToStruct(sec *Section, rtype reflect.Type, rval reflect.Value) {
}
}
-//
// unmarshalValue convert the value from string to primitive type based on its
// kind.
-//
func unmarshalValue(rtype reflect.Type, val string) (rval reflect.Value, ok bool) {
switch rtype.Kind() {
case reflect.Bool:
diff --git a/lib/ini/tag_struct_field.go b/lib/ini/tag_struct_field.go
index c014b57c..baeaa043 100644
--- a/lib/ini/tag_struct_field.go
+++ b/lib/ini/tag_struct_field.go
@@ -14,13 +14,11 @@ import (
type tagStructField map[string]*structField
//
-// unpackStruct read each tags in the struct field and store its section,
+// unpackTagStructField read each ini tag in the struct's field and store its section,
// subsection, and/or key along with their reflect type and value into
// structField.
//
-// The returned type is map of field name and the field tag.
-//
-func unpackStruct(rtype reflect.Type, rval reflect.Value) (out tagStructField) {
+func unpackTagStructField(rtype reflect.Type, rval reflect.Value) (out tagStructField) {
numField := rtype.NumField()
if numField == 0 {
return nil
@@ -42,7 +40,7 @@ func unpackStruct(rtype reflect.Type, rval reflect.Value) (out tagStructField) {
if len(tag) == 0 {
switch fkind {
case reflect.Struct:
- for k, v := range unpackStruct(ftype, fval) {
+ for k, v := range unpackTagStructField(ftype, fval) {
out[k] = v
}
@@ -54,7 +52,7 @@ func unpackStruct(rtype reflect.Type, rval reflect.Value) (out tagStructField) {
fval = fval.Elem()
kind := ftype.Kind()
if kind == reflect.Struct {
- for k, v := range unpackStruct(ftype, fval) {
+ for k, v := range unpackTagStructField(ftype, fval) {
out[k] = v
}
}
diff --git a/lib/ini/tag_struct_field_test.go b/lib/ini/tag_struct_field_test.go
index d9b90c78..ee3cfc83 100644
--- a/lib/ini/tag_struct_field_test.go
+++ b/lib/ini/tag_struct_field_test.go
@@ -53,7 +53,7 @@ func TestUnpackStruct(t *testing.T) {
rval := reflect.ValueOf(v)
rtype = rtype.Elem()
rval = rval.Elem()
- got := unpackStruct(rtype, rval)
+ got := unpackTagStructField(rtype, rval)
exp := []string{
"::int",
@@ -73,7 +73,7 @@ func TestUnpackStruct(t *testing.T) {
"slice:OfStruct",
}
- test.Assert(t, "unpackStruct", exp, got.keys())
+ test.Assert(t, "unpackTagStructField", exp, got.keys())
}
func TestUnpackStruct_embedded(t *testing.T) {
@@ -98,7 +98,7 @@ func TestUnpackStruct_embedded(t *testing.T) {
rval := reflect.ValueOf(v)
rtype = rtype.Elem()
rval = rval.Elem()
- got := unpackStruct(rtype, rval)
+ got := unpackTagStructField(rtype, rval)
exp := []string{
"a::x",
@@ -106,5 +106,5 @@ func TestUnpackStruct_embedded(t *testing.T) {
"b::z",
"c::xx",
}
- test.Assert(t, "unpackStruct embedded", exp, got.keys())
+ test.Assert(t, "unpackTagStructField: embedded", exp, got.keys())
}