[release-branch.r60] json: add struct tag option to wrap literals in strings

««« CL 4918051 / ba6daf799367
json: add struct tag option to wrap literals in strings

Since JavaScript doesn't have [u]int64 types, some JSON APIs
encode such types as strings to avoid losing precision.

This adds a new struct tag option ",string" to cause
fields to be wrapped in JSON strings on encoding
and unwrapped from strings when decoding.

R=rsc, gustavo
CC=golang-dev
https://golang.org/cl/4918051
»»»

R=dsymonds
CC=golang-dev
https://golang.org/cl/5049043
diff --git a/src/pkg/json/Makefile b/src/pkg/json/Makefile
index 4e5a8a1..28ed62b 100644
--- a/src/pkg/json/Makefile
+++ b/src/pkg/json/Makefile
@@ -11,5 +11,6 @@
 	indent.go\
 	scanner.go\
 	stream.go\
+	tags.go\
 
 include ../../Make.pkg
diff --git a/src/pkg/json/decode.go b/src/pkg/json/decode.go
index 6782c76..b7129f9 100644
--- a/src/pkg/json/decode.go
+++ b/src/pkg/json/decode.go
@@ -140,6 +140,7 @@
 	scan       scanner
 	nextscan   scanner // for calls to nextValue
 	savedError os.Error
+	tempstr    string // scratch space to avoid some allocations
 }
 
 // errPhase is used for errors that should not happen unless
@@ -470,6 +471,8 @@
 
 		// Figure out field corresponding to key.
 		var subv reflect.Value
+		destring := false // whether the value is wrapped in a string to be decoded first
+
 		if mv.IsValid() {
 			elemType := mv.Type().Elem()
 			if !mapElem.IsValid() {
@@ -486,7 +489,8 @@
 			if isValidTag(key) {
 				for i := 0; i < sv.NumField(); i++ {
 					f = st.Field(i)
-					if tagName(f.Tag.Get("json")) == key {
+					tagName, _ := parseTag(f.Tag.Get("json"))
+					if tagName == key {
 						ok = true
 						break
 					}
@@ -508,6 +512,8 @@
 				} else {
 					subv = sv.FieldByIndex(f.Index)
 				}
+				_, opts := parseTag(f.Tag.Get("json"))
+				destring = opts.Contains("string")
 			}
 		}
 
@@ -520,8 +526,12 @@
 		}
 
 		// Read value.
-		d.value(subv)
-
+		if destring {
+			d.value(reflect.ValueOf(&d.tempstr))
+			d.literalStore([]byte(d.tempstr), subv)
+		} else {
+			d.value(subv)
+		}
 		// Write value back to map;
 		// if using struct, subv points into struct already.
 		if mv.IsValid() {
@@ -550,8 +560,12 @@
 	// Scan read one byte too far; back up.
 	d.off--
 	d.scan.undo(op)
-	item := d.data[start:d.off]
 
+	d.literalStore(d.data[start:d.off], v)
+}
+
+// literalStore decodes a literal stored in item into v.
+func (d *decodeState) literalStore(item []byte, v reflect.Value) {
 	// Check for unmarshaler.
 	wantptr := item[0] == 'n' // null
 	unmarshaler, pv := d.indirect(v, wantptr)
@@ -918,13 +932,3 @@
 	}
 	return b[0:w], true
 }
-
-// tagName extracts the field name part out of the "json" struct tag
-// value. The json struct tag format is an optional name, followed by
-// zero or more ",option" values.
-func tagName(v string) string {
-	if idx := strings.Index(v, ","); idx != -1 {
-		return v[:idx]
-	}
-	return v
-}
diff --git a/src/pkg/json/decode_test.go b/src/pkg/json/decode_test.go
index 4c179de..5f6c3f5 100644
--- a/src/pkg/json/decode_test.go
+++ b/src/pkg/json/decode_test.go
@@ -265,6 +265,8 @@
 	Foo  string `json:"bar"`
 	Foo2 string `json:"bar2,dummyopt"`
 
+	IntStr int64 `json:",string"`
+
 	PBool    *bool
 	PInt     *int
 	PInt8    *int8
@@ -333,6 +335,7 @@
 	Float64: 15.1,
 	Foo:     "foo",
 	Foo2:    "foo2",
+	IntStr:  42,
 	String:  "16",
 	Map: map[string]Small{
 		"17": {Tag: "tag17"},
@@ -394,6 +397,7 @@
 	"Float64": 15.1,
 	"bar": "foo",
 	"bar2": "foo2",
+	"IntStr": "42",
 	"PBool": null,
 	"PInt": null,
 	"PInt8": null,
@@ -485,6 +489,7 @@
 	"Float64": 0,
 	"bar": "",
 	"bar2": "",
+        "IntStr": "0",
 	"PBool": true,
 	"PInt": 2,
 	"PInt8": 3,
diff --git a/src/pkg/json/encode.go b/src/pkg/json/encode.go
index 0e6529c..16be5e2 100644
--- a/src/pkg/json/encode.go
+++ b/src/pkg/json/encode.go
@@ -17,7 +17,6 @@
 	"runtime"
 	"sort"
 	"strconv"
-	"strings"
 	"unicode"
 	"utf8"
 )
@@ -62,6 +61,12 @@
 //   // Note the leading comma.
 //   Field int `json:",omitempty"`
 //
+// The "string" option signals that a field is stored as JSON inside a
+// JSON-encoded string.  This extra level of encoding is sometimes
+// used when communicating with JavaScript programs:
+//
+//    Int64String int64 `json:",string"`
+//
 // The key name will be used if it's a non-empty string consisting of
 // only Unicode letters, digits, dollar signs, hyphens, and underscores.
 //
@@ -224,6 +229,12 @@
 }
 
 func (e *encodeState) reflectValue(v reflect.Value) {
+	e.reflectValueQuoted(v, false)
+}
+
+// reflectValueQuoted writes the value in v to the output.
+// If quoted is true, the serialization is wrapped in a JSON string.
+func (e *encodeState) reflectValueQuoted(v reflect.Value, quoted bool) {
 	if !v.IsValid() {
 		e.WriteString("null")
 		return
@@ -241,26 +252,39 @@
 		return
 	}
 
+	writeString := (*encodeState).WriteString
+	if quoted {
+		writeString = (*encodeState).string
+	}
+
 	switch v.Kind() {
 	case reflect.Bool:
 		x := v.Bool()
 		if x {
-			e.WriteString("true")
+			writeString(e, "true")
 		} else {
-			e.WriteString("false")
+			writeString(e, "false")
 		}
 
 	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
-		e.WriteString(strconv.Itoa64(v.Int()))
+		writeString(e, strconv.Itoa64(v.Int()))
 
 	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
-		e.WriteString(strconv.Uitoa64(v.Uint()))
+		writeString(e, strconv.Uitoa64(v.Uint()))
 
 	case reflect.Float32, reflect.Float64:
-		e.WriteString(strconv.FtoaN(v.Float(), 'g', -1, v.Type().Bits()))
+		writeString(e, strconv.FtoaN(v.Float(), 'g', -1, v.Type().Bits()))
 
 	case reflect.String:
-		e.string(v.String())
+		if quoted {
+			sb, err := Marshal(v.String())
+			if err != nil {
+				e.error(err)
+			}
+			e.string(string(sb))
+		} else {
+			e.string(v.String())
+		}
 
 	case reflect.Struct:
 		e.WriteByte('{')
@@ -272,17 +296,14 @@
 			if f.PkgPath != "" {
 				continue
 			}
-			tag, omitEmpty := f.Name, false
+			tag, omitEmpty, quoted := f.Name, false, false
 			if tv := f.Tag.Get("json"); tv != "" {
-				ss := strings.SplitN(tv, ",", 2)
-				if isValidTag(ss[0]) {
-					tag = ss[0]
+				name, opts := parseTag(tv)
+				if isValidTag(name) {
+					tag = name
 				}
-				if len(ss) > 1 {
-					// Currently the only option is omitempty,
-					// so parsing is trivial.
-					omitEmpty = ss[1] == "omitempty"
-				}
+				omitEmpty = opts.Contains("omitempty")
+				quoted = opts.Contains("string")
 			}
 			fieldValue := v.Field(i)
 			if omitEmpty && isEmptyValue(fieldValue) {
@@ -295,7 +316,7 @@
 			}
 			e.string(tag)
 			e.WriteByte(':')
-			e.reflectValue(fieldValue)
+			e.reflectValueQuoted(fieldValue, quoted)
 		}
 		e.WriteByte('}')
 
@@ -383,7 +404,8 @@
 func (sv stringValues) Less(i, j int) bool { return sv.get(i) < sv.get(j) }
 func (sv stringValues) get(i int) string   { return sv[i].String() }
 
-func (e *encodeState) string(s string) {
+func (e *encodeState) string(s string) (int, os.Error) {
+	len0 := e.Len()
 	e.WriteByte('"')
 	start := 0
 	for i := 0; i < len(s); {
@@ -428,4 +450,5 @@
 		e.WriteString(s[start:])
 	}
 	e.WriteByte('"')
+	return e.Len() - len0, nil
 }
diff --git a/src/pkg/json/encode_test.go b/src/pkg/json/encode_test.go
index 0e4b637..012e9f1 100644
--- a/src/pkg/json/encode_test.go
+++ b/src/pkg/json/encode_test.go
@@ -5,6 +5,8 @@
 package json
 
 import (
+	"bytes"
+	"reflect"
 	"testing"
 )
 
@@ -42,3 +44,39 @@
 		t.Errorf(" got: %s\nwant: %s\n", got, optionalsExpected)
 	}
 }
+
+type StringTag struct {
+	BoolStr bool   `json:",string"`
+	IntStr  int64  `json:",string"`
+	StrStr  string `json:",string"`
+}
+
+var stringTagExpected = `{
+ "BoolStr": "true",
+ "IntStr": "42",
+ "StrStr": "\"xzbit\""
+}`
+
+func TestStringTag(t *testing.T) {
+	var s StringTag
+	s.BoolStr = true
+	s.IntStr = 42
+	s.StrStr = "xzbit"
+	got, err := MarshalIndent(&s, "", " ")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if got := string(got); got != stringTagExpected {
+		t.Fatalf(" got: %s\nwant: %s\n", got, stringTagExpected)
+	}
+
+	// Verify that it round-trips.
+	var s2 StringTag
+	err = NewDecoder(bytes.NewBuffer(got)).Decode(&s2)
+	if err != nil {
+		t.Fatalf("Decode: %v", err)
+	}
+	if !reflect.DeepEqual(s, s2) {
+		t.Fatalf("decode didn't match.\nsource: %#v\nEncoded as:\n%s\ndecode: %#v", s, string(got), s2)
+	}
+}
diff --git a/src/pkg/json/tags.go b/src/pkg/json/tags.go
new file mode 100644
index 0000000..58cda20
--- /dev/null
+++ b/src/pkg/json/tags.go
@@ -0,0 +1,44 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package json
+
+import (
+	"strings"
+)
+
+// tagOptions is the string following a comma in a struct field's "json"
+// tag, or the empty string. It does not include the leading comma.
+type tagOptions string
+
+// parseTag splits a struct field's json tag into its name and
+// comma-separated options.
+func parseTag(tag string) (string, tagOptions) {
+	if idx := strings.Index(tag, ","); idx != -1 {
+		return tag[:idx], tagOptions(tag[idx+1:])
+	}
+	return tag, tagOptions("")
+}
+
+// Contains returns whether checks that a comma-separated list of options
+// contains a particular substr flag. substr must be surrounded by a
+// string boundary or commas.
+func (o tagOptions) Contains(optionName string) bool {
+	if len(o) == 0 {
+		return false
+	}
+	s := string(o)
+	for s != "" {
+		var next string
+		i := strings.Index(s, ",")
+		if i >= 0 {
+			s, next = s[:i], s[i+1:]
+		}
+		if s == optionName {
+			return true
+		}
+		s = next
+	}
+	return false
+}
diff --git a/src/pkg/json/tags_test.go b/src/pkg/json/tags_test.go
new file mode 100644
index 0000000..91fb188
--- /dev/null
+++ b/src/pkg/json/tags_test.go
@@ -0,0 +1,28 @@
+// Copyright 2011 The Go Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package json
+
+import (
+	"testing"
+)
+
+func TestTagParsing(t *testing.T) {
+	name, opts := parseTag("field,foobar,foo")
+	if name != "field" {
+		t.Fatalf("name = %q, want field", name)
+	}
+	for _, tt := range []struct {
+		opt  string
+		want bool
+	}{
+		{"foobar", true},
+		{"foo", true},
+		{"bar", false},
+	} {
+		if opts.Contains(tt.opt) != tt.want {
+			t.Errorf("Contains(%q) = %v", tt.opt, !tt.want)
+		}
+	}
+}