Parcourir la source

Support map merging.

Fixes #1.
Gustavo Niemeyer il y a 12 ans
Parent
commit
72c33f6840
3 fichiers modifiés avec 137 ajouts et 4 suppressions
  1. 44 1
      decode.go
  2. 92 0
      decode_test.go
  3. 1 3
      resolve.go

+ 44 - 1
decode.go

@@ -438,6 +438,10 @@ func (d *decoder) mapping(n *node, out reflect.Value) (good bool) {
 	}
 	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 @@ func (d *decoder) mappingStruct(n *node, out reflect.Value) (good bool) {
 	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 @@ func (d *decoder) mappingStruct(n *node, out reflect.Value) (good bool) {
 	}
 	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")
+}

+ 92 - 0
decode_test.go

@@ -505,6 +505,98 @@ func (s *S) TestUnmarshalWithFalseSetterIgnoresValue(c *C) {
 	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

+ 1 - 3
resolve.go

@@ -45,6 +45,7 @@ func init() {
 		{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 @@ func resolve(tag string, in string) (rtag string, out interface{}) {
 		}
 	// XXX Handle timestamps here.
 
-	case '<':
-		// XXX Handle merge (<<) here.
-
 	default:
 		panic("resolveTable item not yet handled: " +
 			string([]byte{c}) + " (with " + in + ")")