internal/encoding/json: initial commit of JSON parser/serializer

Package json provides a parser and serializer for the JSON format.
This focuses on the grammar of the format and is agnostic towards specific
semantics of protobuf types.

High-level API:
	func Marshal(v Value, indent string) ([]byte, error)
	func Unmarshal(b []byte) (Value, error)
	type Type uint8
	    const Null Type ...
	type Value struct{ ... }
	    func ValueOf(v interface{}) Value
		func (v Value) Type() Type
		func (v Value) Bool() bool
		func (v Value) Number() float64
		func (v Value) String() string
		func (v Value) Array() []Value
		func (v Value) Object() [][2]Value
		func (v Value) Raw() []byte

Change-Id: I26422f6b3881ef1a11b8aa95160645b1384b27b8
Reviewed-on: https://go-review.googlesource.com/127824
Reviewed-by: Herbie Ong <herbie@google.com>
diff --git a/internal/encoding/json/json_test.go b/internal/encoding/json/json_test.go
new file mode 100644
index 0000000..3e96fa6
--- /dev/null
+++ b/internal/encoding/json/json_test.go
@@ -0,0 +1,416 @@
+// Copyright 2018 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 (
+	"math"
+	"strings"
+	"testing"
+	"unicode/utf8"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+)
+
+func Test(t *testing.T) {
+	const space = " \n\r\t"
+	var V = ValueOf
+	type Arr = []Value
+	type Obj = [][2]Value
+
+	tests := []struct {
+		in            string
+		wantVal       Value
+		wantOut       string
+		wantOutIndent string
+		wantErr       string
+	}{{
+		in:      ``,
+		wantErr: `unexpected EOF`,
+	}, {
+		in:      space,
+		wantErr: `unexpected EOF`,
+	}, {
+		in:            space + `null` + space,
+		wantVal:       V(nil),
+		wantOut:       `null`,
+		wantOutIndent: `null`,
+	}, {
+		in:            space + `true` + space,
+		wantVal:       V(true),
+		wantOut:       `true`,
+		wantOutIndent: `true`,
+	}, {
+		in:            space + `false` + space,
+		wantVal:       V(false),
+		wantOut:       `false`,
+		wantOutIndent: `false`,
+	}, {
+		in:            space + `0` + space,
+		wantVal:       V(0.0),
+		wantOut:       `0`,
+		wantOutIndent: `0`,
+	}, {
+		in:            space + `"hello"` + space,
+		wantVal:       V("hello"),
+		wantOut:       `"hello"`,
+		wantOutIndent: `"hello"`,
+	}, {
+		in:            space + `[]` + space,
+		wantVal:       V(Arr{}),
+		wantOut:       `[]`,
+		wantOutIndent: `[]`,
+	}, {
+		in:            space + `{}` + space,
+		wantVal:       V(Obj{}),
+		wantOut:       `{}`,
+		wantOutIndent: `{}`,
+	}, {
+		in:      `null#invalid`,
+		wantErr: `8 bytes of unconsumed input`,
+	}, {
+		in:      `0#invalid`,
+		wantErr: `8 bytes of unconsumed input`,
+	}, {
+		in:      `"hello"#invalid`,
+		wantErr: `8 bytes of unconsumed input`,
+	}, {
+		in:      `[]#invalid`,
+		wantErr: `8 bytes of unconsumed input`,
+	}, {
+		in:      `{}#invalid`,
+		wantErr: `8 bytes of unconsumed input`,
+	}, {
+		in:      `[truee,true]`,
+		wantErr: `invalid "truee" as literal`,
+	}, {
+		in:      `[falsee,false]`,
+		wantErr: `invalid "falsee" as literal`,
+	}, {
+		in:      `[`,
+		wantErr: `unexpected EOF`,
+	}, {
+		in:            `[{}]`,
+		wantVal:       V(Arr{V(Obj{})}),
+		wantOut:       "[{}]",
+		wantOutIndent: "[\n\t{}\n]",
+	}, {
+		in:      `[{]}`,
+		wantErr: `invalid character ']' at start of string`,
+	}, {
+		in:      `[,]`,
+		wantErr: `invalid "," as value`,
+	}, {
+		in:      `{,}`,
+		wantErr: `invalid character ',' at start of string`,
+	}, {
+		in:      `{"key""val"}`,
+		wantErr: `invalid character '"', expected ':' in object`,
+	}, {
+		in:      `["elem0""elem1"]`,
+		wantErr: `invalid character '"', expected ']' at end of array`,
+	}, {
+		in:      `{"hello"`,
+		wantErr: `unexpected EOF`,
+	}, {
+		in:      `{"hello"}`,
+		wantErr: `invalid character '}', expected ':' in object`,
+	}, {
+		in:      `{"hello":`,
+		wantErr: `unexpected EOF`,
+	}, {
+		in:      `{"hello":}`,
+		wantErr: `invalid "}" as value`,
+	}, {
+		in:      `{"hello":"goodbye"`,
+		wantErr: `unexpected EOF`,
+	}, {
+		in:      `{"hello":"goodbye"]`,
+		wantErr: `invalid character ']', expected '}' at end of object`,
+	}, {
+		in:            `{"hello":"goodbye"}`,
+		wantVal:       V(Obj{{V("hello"), V("goodbye")}}),
+		wantOut:       `{"hello":"goodbye"}`,
+		wantOutIndent: "{\n\t\"hello\": \"goodbye\"\n}",
+	}, {
+		in:      `{"hello":"goodbye",}`,
+		wantErr: `invalid character '}' at start of string`,
+	}, {
+		in: `{"k":"v1","k":"v2"}`,
+		wantVal: V(Obj{
+			{V("k"), V("v1")}, {V("k"), V("v2")},
+		}),
+		wantOut:       `{"k":"v1","k":"v2"}`,
+		wantOutIndent: "{\n\t\"k\": \"v1\",\n\t\"k\": \"v2\"\n}",
+	}, {
+		in: `{"k":{"k":{"k":"v"}}}`,
+		wantVal: V(Obj{
+			{V("k"), V(Obj{
+				{V("k"), V(Obj{
+					{V("k"), V("v")},
+				})},
+			})},
+		}),
+		wantOut:       `{"k":{"k":{"k":"v"}}}`,
+		wantOutIndent: "{\n\t\"k\": {\n\t\t\"k\": {\n\t\t\t\"k\": \"v\"\n\t\t}\n\t}\n}",
+	}, {
+		in: `{"k":{"k":{"k":"v1","k":"v2"}}}`,
+		wantVal: V(Obj{
+			{V("k"), V(Obj{
+				{V("k"), V(Obj{
+					{V("k"), V("v1")},
+					{V("k"), V("v2")},
+				})},
+			})},
+		}),
+		wantOut:       `{"k":{"k":{"k":"v1","k":"v2"}}}`,
+		wantOutIndent: "{\n\t\"k\": {\n\t\t\"k\": {\n\t\t\t\"k\": \"v1\",\n\t\t\t\"k\": \"v2\"\n\t\t}\n\t}\n}",
+	}, {
+		in:      "  x",
+		wantErr: `syntax error (line 1:3)`,
+	}, {
+		in:      `["💩"x`,
+		wantErr: `syntax error (line 1:5)`,
+	}, {
+		in:      "\n\n[\"🔥🔥🔥\"x",
+		wantErr: `syntax error (line 3:7)`,
+	}, {
+		in:      `["👍🏻👍🏿"x`,
+		wantErr: `syntax error (line 1:8)`, // multi-rune emojis; could be column:6
+	}, {
+		in:      "\"\x00\"",
+		wantErr: `invalid character '\x00' in string`,
+	}, {
+		in:      "\"\xff\"",
+		wantErr: `invalid UTF-8 detected`,
+		wantVal: V(string("\xff")),
+	}, {
+		in:      `"` + string(utf8.RuneError) + `"`,
+		wantVal: V(string(utf8.RuneError)),
+		wantOut: `"` + string(utf8.RuneError) + `"`,
+	}, {
+		in:      `"\uFFFD"`,
+		wantVal: V(string(utf8.RuneError)),
+		wantOut: `"` + string(utf8.RuneError) + `"`,
+	}, {
+		in:      `"\x"`,
+		wantErr: `invalid escape code "\\x" in string`,
+	}, {
+		in:      `"\uXXXX"`,
+		wantErr: `invalid escape code "\\uXXXX" in string`,
+	}, {
+		in:      `"\uDEAD"`, // unmatched surrogate pair
+		wantErr: `unexpected EOF`,
+	}, {
+		in:      `"\uDEAD\uBEEF"`, // invalid surrogate half
+		wantErr: `invalid escape code "\\uBEEF" in string`,
+	}, {
+		in:      `"\uD800\udead"`, // valid surrogate pair
+		wantVal: V("𐊭"),
+		wantOut: `"𐊭"`,
+	}, {
+		in:      `"\u0000\"\\\/\b\f\n\r\t"`,
+		wantVal: V("\u0000\"\\/\b\f\n\r\t"),
+		wantOut: `"\u0000\"\\/\b\f\n\r\t"`,
+	}, {
+		in:      `-`,
+		wantErr: `invalid "-" as number`,
+	}, {
+		in:      `-0`,
+		wantVal: V(math.Copysign(0, -1)),
+		wantOut: `-0`,
+	}, {
+		in:      `+0`,
+		wantErr: `invalid "+0" as value`,
+	}, {
+		in:      `-+`,
+		wantErr: `invalid "-+" as number`,
+	}, {
+		in:      `0.`,
+		wantErr: `invalid "0." as number`,
+	}, {
+		in:      `.1`,
+		wantErr: `invalid ".1" as value`,
+	}, {
+		in:      `0.e1`,
+		wantErr: `invalid "0.e1" as number`,
+	}, {
+		in:      `0.0`,
+		wantVal: V(0.0),
+		wantOut: "0",
+	}, {
+		in:      `01`,
+		wantErr: `invalid "01" as number`,
+	}, {
+		in:      `0e`,
+		wantErr: `invalid "0e" as number`,
+	}, {
+		in:      `0e0`,
+		wantVal: V(0.0),
+		wantOut: "0",
+	}, {
+		in:      `0E0`,
+		wantVal: V(0.0),
+		wantOut: "0",
+	}, {
+		in:      `0Ee`,
+		wantErr: `invalid "0Ee" as number`,
+	}, {
+		in:      `-1.0E+1`,
+		wantVal: V(-10.0),
+		wantOut: "-10",
+	}, {
+		in: `
+		{
+		  "firstName" : "John",
+		  "lastName" : "Smith" ,
+		  "isAlive" : true,
+		  "age" : 27,
+		  "address" : {
+		    "streetAddress" : "21 2nd Street" ,
+		    "city" : "New York" ,
+		    "state" : "NY" ,
+		    "postalCode" : "10021-3100"
+		  },
+		  "phoneNumbers" : [
+		    {
+		      "type" : "home" ,
+		      "number" : "212 555-1234"
+		    } ,
+		    {
+		      "type" : "office" ,
+		      "number" : "646 555-4567"
+		    } ,
+		    {
+		      "type" : "mobile" ,
+		      "number" : "123 456-7890"
+		    }
+		  ],
+		  "children" : [] ,
+		  "spouse" : null
+		}
+		`,
+		wantVal: V(Obj{
+			{V("firstName"), V("John")},
+			{V("lastName"), V("Smith")},
+			{V("isAlive"), V(true)},
+			{V("age"), V(27.0)},
+			{V("address"), V(Obj{
+				{V("streetAddress"), V("21 2nd Street")},
+				{V("city"), V("New York")},
+				{V("state"), V("NY")},
+				{V("postalCode"), V("10021-3100")},
+			})},
+			{V("phoneNumbers"), V(Arr{
+				V(Obj{
+					{V("type"), V("home")},
+					{V("number"), V("212 555-1234")},
+				}),
+				V(Obj{
+					{V("type"), V("office")},
+					{V("number"), V("646 555-4567")},
+				}),
+				V(Obj{
+					{V("type"), V("mobile")},
+					{V("number"), V("123 456-7890")},
+				}),
+			})},
+			{V("children"), V(Arr{})},
+			{V("spouse"), V(nil)},
+		}),
+		wantOut: `{"firstName":"John","lastName":"Smith","isAlive":true,"age":27,"address":{"streetAddress":"21 2nd Street","city":"New York","state":"NY","postalCode":"10021-3100"},"phoneNumbers":[{"type":"home","number":"212 555-1234"},{"type":"office","number":"646 555-4567"},{"type":"mobile","number":"123 456-7890"}],"children":[],"spouse":null}`,
+		wantOutIndent: `{
+	"firstName": "John",
+	"lastName": "Smith",
+	"isAlive": true,
+	"age": 27,
+	"address": {
+		"streetAddress": "21 2nd Street",
+		"city": "New York",
+		"state": "NY",
+		"postalCode": "10021-3100"
+	},
+	"phoneNumbers": [
+		{
+			"type": "home",
+			"number": "212 555-1234"
+		},
+		{
+			"type": "office",
+			"number": "646 555-4567"
+		},
+		{
+			"type": "mobile",
+			"number": "123 456-7890"
+		}
+	],
+	"children": [],
+	"spouse": null
+}`,
+	}}
+
+	opts := cmp.Options{
+		cmpopts.EquateEmpty(),
+		cmp.Transformer("", func(v Value) interface{} {
+			switch v.typ {
+			case 0:
+				return nil // special case so Value{} == Value{}
+			case Null:
+				return nil
+			case Bool:
+				return v.Bool()
+			case Number:
+				return v.Number()
+			case String:
+				return v.String()
+			case Array:
+				return v.Array()
+			case Object:
+				return v.Object()
+			default:
+				panic("invalid type")
+			}
+		}),
+	}
+	for _, tt := range tests {
+		t.Run("", func(t *testing.T) {
+			if tt.in != "" || tt.wantVal.Type() != 0 || tt.wantErr != "" {
+				gotVal, err := Unmarshal([]byte(tt.in))
+				if err == nil {
+					if tt.wantErr != "" {
+						t.Errorf("Unmarshal(): got nil error, want %v", tt.wantErr)
+					}
+				} else {
+					if tt.wantErr == "" {
+						t.Errorf("Unmarshal(): got %v, want nil error", err)
+					} else if !strings.Contains(err.Error(), tt.wantErr) {
+						t.Errorf("Unmarshal(): got %v, want %v", err, tt.wantErr)
+					}
+				}
+				if diff := cmp.Diff(gotVal, tt.wantVal, opts); diff != "" {
+					t.Errorf("Unmarshal(): output mismatch (-got +want):\n%s", diff)
+				}
+			}
+			if tt.wantOut != "" {
+				gotOut, err := Marshal(tt.wantVal, "")
+				if err != nil {
+					t.Errorf("Marshal(): got %v, want nil error", err)
+				}
+				if string(gotOut) != tt.wantOut {
+					t.Errorf("Marshal():\ngot:  %s\nwant: %s", gotOut, tt.wantOut)
+				}
+			}
+			if tt.wantOutIndent != "" {
+				gotOut, err := Marshal(tt.wantVal, "\t")
+				if err != nil {
+					t.Errorf("Marshal(Indent): got %v, want nil error", err)
+				}
+				if string(gotOut) != tt.wantOutIndent {
+					t.Errorf("Marshal(Indent):\ngot:  %s\nwant: %s", gotOut, tt.wantOutIndent)
+				}
+			}
+		})
+	}
+}