Add support for ,inline flag with struct values.
This also adds the code to do inlining of maps, as supported by bson
(the code was copied from mgo/bson), but it's disabled for the moment
as I'll need more time to implement it.
R=jameinel, rog
CC=
https://codereview.appspot.com/10366044
diff --git a/decode.go b/decode.go
index e7fb29b..5dd2ce5 100644
--- a/decode.go
+++ b/decode.go
@@ -433,19 +433,24 @@
}
func (d *decoder) mappingStruct(n *node, out reflect.Value) (good bool) {
- fields, err := getStructFields(out.Type())
+ sinfo, err := getStructInfo(out.Type())
if err != nil {
panic(err)
}
name := settableValueOf("")
- fieldsMap := fields.Map
l := len(n.children)
for i := 0; i < l; i += 2 {
if !d.unmarshal(n.children[i], name) {
continue
}
- if info, ok := fieldsMap[name.String()]; ok {
- d.unmarshal(n.children[i+1], out.Field(info.Num))
+ if info, ok := sinfo.FieldsMap[name.String()]; ok {
+ var field reflect.Value
+ if info.Inline == nil {
+ field = out.Field(info.Num)
+ } else {
+ field = out.FieldByIndex(info.Inline)
+ }
+ d.unmarshal(n.children[i+1], field)
}
}
return true
diff --git a/decode_test.go b/decode_test.go
index bac544b..9b2a263 100644
--- a/decode_test.go
+++ b/decode_test.go
@@ -332,6 +332,24 @@
"Generic line break (glyphed)\n" +
"Line separator\u2028Paragraph separator\u2029",
},
+
+ // Struct inlining
+ {
+ "a: 1\nb: 2\nc: 3\n",
+ &struct {
+ A int
+ C inlineB `yaml:",inline"`
+ }{1, inlineB{2, inlineC{3}}},
+ },
+}
+
+type inlineB struct {
+ B int
+ inlineC `yaml:",inline"`
+}
+
+type inlineC struct {
+ C int
}
func (s *S) TestUnmarshal(c *C) {
diff --git a/encode.go b/encode.go
index ad7853c..b228a10 100644
--- a/encode.go
+++ b/encode.go
@@ -109,13 +109,18 @@
}
func (e *encoder) structv(tag string, in reflect.Value) {
- fields, err := getStructFields(in.Type())
+ sinfo, err := getStructInfo(in.Type())
if err != nil {
panic(err)
}
e.mappingv(tag, func() {
- for _, info := range fields.List {
- value := in.Field(info.Num)
+ for _, info := range sinfo.FieldsList {
+ var value reflect.Value
+ if info.Inline == nil {
+ value = in.Field(info.Num)
+ } else {
+ value = in.FieldByIndex(info.Inline)
+ }
if info.OmitEmpty && isZero(value) {
continue
}
diff --git a/encode_test.go b/encode_test.go
index 0d34d88..5d16426 100644
--- a/encode_test.go
+++ b/encode_test.go
@@ -200,6 +200,15 @@
}{1, 2},
"a: 1\n",
},
+
+ // Struct inlining
+ {
+ &struct {
+ A int
+ C inlineB `yaml:",inline"`
+ }{1, inlineB{2, inlineC{3}}},
+ "a: 1\nb: 2\nc: 3\n",
+ },
}
func (s *S) TestMarshal(c *C) {
@@ -210,17 +219,25 @@
}
}
-//var unmarshalErrorTests = []struct{data, error string}{
-// {"v: !!float 'error'", "Can't decode !!str 'error' as a !!float"},
-//}
-//
-//func (s *S) TestUnmarshalErrors(c *C) {
-// for _, item := range unmarshalErrorTests {
-// var value interface{}
-// err := goyaml.Unmarshal([]byte(item.data), &value)
-// c.Assert(err, Matches, item.error)
-// }
-//}
+var marshalErrorTests = []struct {
+ value interface{}
+ error string
+}{
+ {
+ &struct {
+ B int
+ inlineB ",inline"
+ }{1, inlineB{2, inlineC{3}}},
+ `Duplicated key 'b' in struct struct \{ B int; .*`,
+ },
+}
+
+func (s *S) TestMarshalErrors(c *C) {
+ for _, item := range marshalErrorTests {
+ _, err := goyaml.Marshal(item.value)
+ c.Assert(err, ErrorMatches, item.error)
+ }
+}
var marshalTaggedIfaceTest interface{} = &struct{ A string }{"B"}
diff --git a/goyaml.go b/goyaml.go
index 1b71c8e..dbc633e 100644
--- a/goyaml.go
+++ b/goyaml.go
@@ -1,12 +1,4 @@
-//
-// goyaml - YAML support for the Go language
-//
-// https://wiki.ubuntu.com/goyaml
-//
-// Copyright (c) 2011 Canonical Ltd.
-//
-// Written by Gustavo Niemeyer <gustavo.niemeyer@canonical.com>
-//
+// Package goyaml implements YAML support for the Go language.
package goyaml
import (
@@ -74,16 +66,20 @@
// tag value may be used to tweak the name. Everything before the first
// comma in the field tag will be used as the name. The values following
// the comma are used to tweak the marshalling process (see Marshal).
+// Conflicting names result in a runtime error.
//
// For example:
//
// type T struct {
-// F int "a,omitempty"
+// F int `yaml:"a,omitempty"`
// B int
// }
// var T t
// goyaml.Unmarshal([]byte("a: 1\nb: 2"), &t)
//
+// See the documentation of Marshal for the format of tags and a list of
+// supported tag options.
+//
func Unmarshal(in []byte, out interface{}) (err error) {
defer handleErr(&err)
d := newDecoder()
@@ -104,9 +100,8 @@
// The lowercased field name is used as the key for each exported field,
// but this behavior may be changed using the respective field tag.
// The tag may also contain flags to tweak the marshalling behavior for
-// the field. The tag formats accepted are:
-//
-// "[<key>][,<flag1>[,<flag2>]]"
+// the field. Conflicting names result in a runtime error. The tag format
+// accepted is:
//
// `(...) yaml:"[<key>][,<flag1>[,<flag2>]]" (...)`
//
@@ -119,6 +114,10 @@
// flow Marshal using a flow style (useful for structs,
// sequences and maps.
//
+// inline Inline the struct it's applied to, so its fields
+// are processed as if they were part of the outer
+// struct.
+//
// In addition, if the key is "-", the field is ignored.
//
// For example:
@@ -131,7 +130,7 @@
// goyaml.Marshal(&T{F: 1}} // Returns "a: 1\nb: 0\n"
//
func Marshal(in interface{}) (out []byte, err error) {
- //defer handleErr(&err)
+ defer handleErr(&err)
e := newEncoder()
defer e.destroy()
e.marshal("", reflect.ValueOf(in))
@@ -145,9 +144,15 @@
// The code in this section was copied from gobson.
-type structFields struct {
- Map map[string]fieldInfo
- List []fieldInfo
+// structInfo holds details for the serialization of fields of
+// a given struct.
+type structInfo struct {
+ FieldsMap map[string]fieldInfo
+ FieldsList []fieldInfo
+
+ // InlineMap is the number of the field in the struct that
+ // contains an ,inline map, or -1 if there's none.
+ InlineMap int
}
type fieldInfo struct {
@@ -155,9 +160,12 @@
Num int
OmitEmpty bool
Flow bool
+
+ // Inline holds the field index if the field is part of an inlined struct.
+ Inline []int
}
-var fieldMap = make(map[reflect.Type]*structFields)
+var structMap = make(map[reflect.Type]*structInfo)
var fieldMapMutex sync.RWMutex
type externalPanic string
@@ -166,17 +174,18 @@
return string(e)
}
-func getStructFields(st reflect.Type) (*structFields, error) {
+func getStructInfo(st reflect.Type) (*structInfo, error) {
fieldMapMutex.RLock()
- fields, found := fieldMap[st]
+ sinfo, found := structMap[st]
fieldMapMutex.RUnlock()
if found {
- return fields, nil
+ return sinfo, nil
}
n := st.NumField()
fieldsMap := make(map[string]fieldInfo)
- fieldsList := make([]fieldInfo, n)
+ fieldsList := make([]fieldInfo, 0, n)
+ inlineMap := -1
for i := 0; i != n; i++ {
field := st.Field(i)
if field.PkgPath != "" {
@@ -193,24 +202,7 @@
continue
}
- // XXX Drop this after a few releases.
- if s := strings.Index(tag, "/"); s >= 0 {
- recommend := tag[:s]
- for _, c := range tag[s+1:] {
- switch c {
- case 'c':
- recommend += ",omitempty"
- case 'f':
- recommend += ",flow"
- default:
- msg := fmt.Sprintf("Unsupported flag %q in tag %q of type %s", string([]byte{uint8(c)}), tag, st)
- panic(externalPanic(msg))
- }
- }
- msg := fmt.Sprintf("Replace tag %q in field %s of type %s by %q", tag, field.Name, st, recommend)
- panic(externalPanic(msg))
- }
-
+ inline := false
fields := strings.Split(tag, ",")
if len(fields) > 1 {
for _, flag := range fields[1:] {
@@ -219,6 +211,8 @@
info.OmitEmpty = true
case "flow":
info.Flow = true
+ case "inline":
+ inline = true
default:
msg := fmt.Sprintf("Unsupported flag %q in tag %q of type %s", flag, tag, st)
panic(externalPanic(msg))
@@ -227,6 +221,41 @@
tag = fields[0]
}
+ if inline {
+ switch field.Type.Kind() {
+ //case reflect.Map:
+ // if inlineMap >= 0 {
+ // return nil, errors.New("Multiple ,inline maps in struct " + st.String())
+ // }
+ // if field.Type.Key() != reflect.TypeOf("") {
+ // return nil, errors.New("Option ,inline needs a map with string keys in struct " + st.String())
+ // }
+ // inlineMap = info.Num
+ case reflect.Struct:
+ sinfo, err := getStructInfo(field.Type)
+ if err != nil {
+ return nil, err
+ }
+ for _, finfo := range sinfo.FieldsList {
+ if _, found := fieldsMap[finfo.Key]; found {
+ msg := "Duplicated key '" + finfo.Key + "' in struct " + st.String()
+ return nil, errors.New(msg)
+ }
+ if finfo.Inline == nil {
+ finfo.Inline = []int{i, finfo.Num}
+ } else {
+ finfo.Inline = append([]int{i}, finfo.Inline...)
+ }
+ fieldsMap[finfo.Key] = finfo
+ fieldsList = append(fieldsList, finfo)
+ }
+ default:
+ //panic("Option ,inline needs a struct value or map field")
+ panic("Option ,inline needs a struct value field")
+ }
+ continue
+ }
+
if tag != "" {
info.Key = tag
} else {
@@ -238,16 +267,16 @@
return nil, errors.New(msg)
}
- fieldsList[len(fieldsMap)] = info
+ fieldsList = append(fieldsList, info)
fieldsMap[info.Key] = info
}
- fields = &structFields{fieldsMap, fieldsList[:len(fieldsMap)]}
+ sinfo = &structInfo{fieldsMap, fieldsList, inlineMap}
fieldMapMutex.Lock()
- fieldMap[st] = fields
+ structMap[st] = sinfo
fieldMapMutex.Unlock()
- return fields, nil
+ return sinfo, nil
}
func isZero(v reflect.Value) bool {