Support map merging. Fixes #1.
diff --git a/decode.go b/decode.go index 75a2c42..ab2b8f7 100644 --- a/decode.go +++ b/decode.go
@@ -438,6 +438,10 @@ } l := len(n.children) for i := 0; i < l; i += 2 { + if isMerge(n.children[i]) { + d.merge(n.children[i+1], out) + continue + } k := reflect.New(kt).Elem() if d.unmarshal(n.children[i], k) { e := reflect.New(et).Elem() @@ -457,7 +461,12 @@ name := settableValueOf("") l := len(n.children) for i := 0; i < l; i += 2 { - if !d.unmarshal(n.children[i], name) { + ni := n.children[i] + if isMerge(ni) { + d.merge(n.children[i+1], out) + continue + } + if !d.unmarshal(ni, name) { continue } if info, ok := sinfo.FieldsMap[name.String()]; ok { @@ -472,3 +481,37 @@ } return true } + +func (d *decoder) merge(n *node, out reflect.Value) { + const wantMap = "map merge requires map or sequence of maps as the value" + switch n.kind { + case mappingNode: + d.unmarshal(n, out) + case aliasNode: + an, ok := d.doc.anchors[n.value] + if ok && an.kind != mappingNode { + panic(wantMap) + } + d.unmarshal(n, out) + case sequenceNode: + // Step backwards as earlier nodes take precedence. + for i := len(n.children)-1; i >= 0; i-- { + ni := n.children[i] + if ni.kind == aliasNode { + an, ok := d.doc.anchors[ni.value] + if ok && an.kind != mappingNode { + panic(wantMap) + } + } else if ni.kind != mappingNode { + panic(wantMap) + } + d.unmarshal(ni, out) + } + default: + panic(wantMap) + } +} + +func isMerge(n *node) bool { + return n.kind == scalarNode && n.value == "<<" && (n.implicit == true || n.tag == "!!merge" || n.tag == "tag:yaml.org,2002:merge") +}
diff --git a/decode_test.go b/decode_test.go index 55db804..5702909 100644 --- a/decode_test.go +++ b/decode_test.go
@@ -505,6 +505,98 @@ c.Assert(m["ghi"].value, Equals, 3) } +// From http://yaml.org/type/merge.html +var mergeTests = ` +anchors: + - &CENTER { "x": 1, "y": 2 } + - &LEFT { "x": 0, "y": 2 } + - &BIG { "r": 10 } + - &SMALL { "r": 1 } + +# All the following maps are equal: + +plain: + # Explicit keys + "x": 1 + "y": 2 + "r": 10 + label: center/big + +mergeOne: + # Merge one map + << : *CENTER + "r": 10 + label: center/big + +mergeMultiple: + # Merge multiple maps + << : [ *CENTER, *BIG ] + label: center/big + +override: + # Override + << : [ *BIG, *LEFT, *SMALL ] + "x": 1 + label: center/big + +shortTag: + # Explicit short merge tag + !!merge "<<" : [ *CENTER, *BIG ] + label: center/big + +longTag: + # Explicit merge long tag + !<tag:yaml.org,2002:merge> "<<" : [ *CENTER, *BIG ] + label: center/big + +inlineMap: + # Inlined map + << : {"x": 1, "y": 2, "r": 10} + label: center/big + +inlineSequenceMap: + # Inlined map in sequence + << : [ *CENTER, {"r": 10} ] + label: center/big +` + +func (s *S) TestMerge(c *C) { + var want = map[interface{}]interface{}{ + "x": 1, + "y": 2, + "r": 10, + "label": "center/big", + } + + var m map[string]interface{} + err := yaml.Unmarshal([]byte(mergeTests), &m) + c.Assert(err, IsNil) + for name, test := range m { + if name == "anchors" { + continue + } + c.Assert(test, DeepEquals, want, Commentf("test %q failed", name)) + } +} + +func (s *S) TestMergeStruct(c *C) { + type Data struct { + X, Y, R int + Label string + } + want := Data{1, 2, 10, "center/big"} + + var m map[string]Data + err := yaml.Unmarshal([]byte(mergeTests), &m) + c.Assert(err, IsNil) + for name, test := range m { + if name == "anchors" { + continue + } + c.Assert(test, Equals, want, Commentf("test %q failed", name)) + } +} + //var data []byte //func init() { // var err error
diff --git a/resolve.go b/resolve.go index dbda017..fdc4909 100644 --- a/resolve.go +++ b/resolve.go
@@ -45,6 +45,7 @@ {math.Inf(+1), "!!float", []string{".inf", ".Inf", ".INF"}}, {math.Inf(+1), "!!float", []string{"+.inf", "+.Inf", "+.INF"}}, {math.Inf(-1), "!!float", []string{"-.inf", "-.Inf", "-.INF"}}, + {"<<", "!!merge", []string{"<<"}}, } m := resolveMap @@ -139,9 +140,6 @@ } // XXX Handle timestamps here. - case '<': - // XXX Handle merge (<<) here. - default: panic("resolveTable item not yet handled: " + string([]byte{c}) + " (with " + in + ")")