Browse Source

Merge pull request #313 from rogpeppe/020-quote-merge-key

quote "<<" keys on output
Roger Peppe 8 years ago
parent
commit
a64b821473
5 changed files with 50 additions and 11 deletions
  1. 8 1
      decode.go
  2. 6 0
      decode_test.go
  3. 9 2
      encode.go
  4. 11 0
      encode_test.go
  5. 16 8
      resolve.go

+ 8 - 1
decode.go

@@ -234,6 +234,7 @@ var (
 	ifaceType      = defaultMapType.Elem()
 	timeType       = reflect.TypeOf(time.Time{})
 	ptrTimeType    = reflect.TypeOf(&time.Time{})
+	mergeTagType   = reflect.TypeOf(MergeTag)
 )
 
 func newDecoder(strict bool) *decoder {
@@ -760,5 +761,11 @@ func (d *decoder) merge(n *node, out reflect.Value) {
 }
 
 func isMerge(n *node) bool {
-	return n.kind == scalarNode && n.value == "<<" && (n.implicit == true || n.tag == yaml_MERGE_TAG)
+	// Quick test so we avoid the overhead of calling resolve
+	// unless we're pretty sure it can be a merge node.
+	if n.kind != scalarNode || n.value != "<<" {
+		return false
+	}
+	_, v := resolve(n.tag, n.value)
+	return v == MergeTag
 }

+ 6 - 0
decode_test.go

@@ -412,6 +412,11 @@ var unmarshalTests = []struct {
 		"v: ! test",
 		map[string]interface{}{"v": "test"},
 	},
+	// Non-specific tag with resolvable item.
+	{
+		"v: ! 99",
+		map[string]interface{}{"v": "99"},
+	},
 
 	// Anchors and aliases.
 	{
@@ -1080,6 +1085,7 @@ inlineSequenceMap:
   # Inlined map in sequence
   << : [ *CENTER, {"r": 10} ]
   label: center/big
+
 `
 
 func (s *S) TestMerge(c *C) {

+ 9 - 2
encode.go

@@ -126,9 +126,12 @@ func (e *encoder) marshal(tag string, in reflect.Value) {
 			e.marshal(tag, in.Elem())
 		}
 	case reflect.Struct:
-		if in.Type() == timeType {
+		switch in.Type() {
+		case timeType:
 			e.timev(tag, in)
-		} else {
+		case mergeTagType:
+			e.mergeTagv(tag)
+		default:
 			e.structv(tag, in)
 		}
 	case reflect.Slice:
@@ -334,6 +337,10 @@ func (e *encoder) timev(tag string, in reflect.Value) {
 	e.emitScalar(t.Format(time.RFC3339Nano), "", tag, yaml_PLAIN_SCALAR_STYLE)
 }
 
+func (e *encoder) mergeTagv(tag string) {
+	e.emitScalar("<<", "", tag, yaml_PLAIN_SCALAR_STYLE)
+}
+
 func (e *encoder) floatv(tag string, in reflect.Value) {
 	s := strconv.FormatFloat(in.Float(), 'g', -1, 64)
 	switch s {

+ 11 - 0
encode_test.go

@@ -356,6 +356,17 @@ var marshalTests = []struct {
 		map[string]string{"a": "你好 #comment"},
 		"a: '你好 #comment'\n",
 	},
+
+	// << should be quoted so that it's not treated as a merge key.
+	{
+		map[string]string{"<<": "x"},
+		"\"<<\": x\n",
+	},
+	// Explicit merge tag key is left unquoted.
+	{
+		map[interface{}]interface{}{yaml.MergeTag: map[string]string{"a": "b"}},
+		"<<:\n  a: b\n",
+	},
 }
 
 func (s *S) TestMarshal(c *C) {

+ 16 - 8
resolve.go

@@ -14,20 +14,28 @@ type resolveMapItem struct {
 	tag   string
 }
 
+// MergeTag is the value used for a merge tag (!!merge "<<").
+// When marshaled as a map key, the << will not be quoted,
+// which makes it possible to include merge tags explicitly
+// when encoding YAML values.
+var MergeTag = mergeTag{}
+
+type mergeTag struct{}
+
 var resolveTable = make([]byte, 256)
 var resolveMap = make(map[string]resolveMapItem)
 
 func init() {
 	t := resolveTable
-	t[int('+')] = 'S' // Sign
-	t[int('-')] = 'S'
+	t['+'] = 'S' // Sign
+	t['-'] = 'S'
 	for _, c := range "0123456789" {
-		t[int(c)] = 'D' // Digit
+		t[c] = 'D' // Digit
 	}
-	for _, c := range "yYnNtTfFoO~" {
-		t[int(c)] = 'M' // In map
+	for _, c := range "yYnNtTfFoO~<" {
+		t[c] = 'M' // In resolveMap
 	}
-	t[int('.')] = '.' // Float (potentially in map)
+	t['.'] = '.' // Float (potentially in map)
 
 	var resolveMapList = []struct {
 		v   interface{}
@@ -45,7 +53,7 @@ func init() {
 		{math.Inf(+1), yaml_FLOAT_TAG, []string{".inf", ".Inf", ".INF"}},
 		{math.Inf(+1), yaml_FLOAT_TAG, []string{"+.inf", "+.Inf", "+.INF"}},
 		{math.Inf(-1), yaml_FLOAT_TAG, []string{"-.inf", "-.Inf", "-.INF"}},
-		{"<<", yaml_MERGE_TAG, []string{"<<"}},
+		{MergeTag, yaml_MERGE_TAG, []string{"<<"}},
 	}
 
 	m := resolveMap
@@ -75,7 +83,7 @@ func longTag(tag string) string {
 
 func resolvableTag(tag string) bool {
 	switch tag {
-	case "", yaml_STR_TAG, yaml_BOOL_TAG, yaml_INT_TAG, yaml_FLOAT_TAG, yaml_NULL_TAG, yaml_TIMESTAMP_TAG:
+	case "", yaml_STR_TAG, yaml_BOOL_TAG, yaml_INT_TAG, yaml_FLOAT_TAG, yaml_NULL_TAG, yaml_TIMESTAMP_TAG, yaml_MERGE_TAG:
 		return true
 	}
 	return false