encoding/jsonpb: basic JSON marshaling

This does not handle extensions, messagesets, and well-known types yet.

Change-Id: I2786c429f490fe8c57f3f85cd25058d936b58bf7
Reviewed-on: https://go-review.googlesource.com/c/162637
Reviewed-by: Damien Neil <dneil@google.com>
diff --git a/encoding/jsonpb/encode_test.go b/encoding/jsonpb/encode_test.go
new file mode 100644
index 0000000..5ee09b2
--- /dev/null
+++ b/encoding/jsonpb/encode_test.go
@@ -0,0 +1,727 @@
+// Copyright 2019 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 jsonpb_test
+
+import (
+	"math"
+	"strings"
+	"testing"
+
+	"github.com/golang/protobuf/v2/encoding/jsonpb"
+	"github.com/golang/protobuf/v2/internal/encoding/pack"
+	"github.com/golang/protobuf/v2/internal/scalar"
+	"github.com/golang/protobuf/v2/proto"
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+
+	// The legacy package must be imported prior to use of any legacy messages.
+	// TODO: Remove this when protoV1 registers these hooks for you.
+	_ "github.com/golang/protobuf/v2/internal/legacy"
+
+	"github.com/golang/protobuf/v2/encoding/testprotos/pb2"
+	"github.com/golang/protobuf/v2/encoding/testprotos/pb3"
+)
+
+// splitLines is a cmpopts.Option for comparing strings with line breaks.
+var splitLines = cmpopts.AcyclicTransformer("SplitLines", func(s string) []string {
+	return strings.Split(s, "\n")
+})
+
+func pb2Enum(i int32) *pb2.Enum {
+	p := new(pb2.Enum)
+	*p = pb2.Enum(i)
+	return p
+}
+
+func pb2Enums_NestedEnum(i int32) *pb2.Enums_NestedEnum {
+	p := new(pb2.Enums_NestedEnum)
+	*p = pb2.Enums_NestedEnum(i)
+	return p
+}
+
+func TestMarshal(t *testing.T) {
+	tests := []struct {
+		desc  string
+		mo    jsonpb.MarshalOptions
+		input proto.Message
+		want  string
+	}{{
+		desc:  "proto2 optional scalars not set",
+		input: &pb2.Scalars{},
+		want:  "{}",
+	}, {
+		desc:  "proto3 scalars not set",
+		input: &pb3.Scalars{},
+		want:  "{}",
+	}, {
+		desc: "proto2 optional scalars set to zero values",
+		input: &pb2.Scalars{
+			OptBool:     scalar.Bool(false),
+			OptInt32:    scalar.Int32(0),
+			OptInt64:    scalar.Int64(0),
+			OptUint32:   scalar.Uint32(0),
+			OptUint64:   scalar.Uint64(0),
+			OptSint32:   scalar.Int32(0),
+			OptSint64:   scalar.Int64(0),
+			OptFixed32:  scalar.Uint32(0),
+			OptFixed64:  scalar.Uint64(0),
+			OptSfixed32: scalar.Int32(0),
+			OptSfixed64: scalar.Int64(0),
+			OptFloat:    scalar.Float32(0),
+			OptDouble:   scalar.Float64(0),
+			OptBytes:    []byte{},
+			OptString:   scalar.String(""),
+		},
+		want: `{
+  "optBool": false,
+  "optInt32": 0,
+  "optInt64": "0",
+  "optUint32": 0,
+  "optUint64": "0",
+  "optSint32": 0,
+  "optSint64": "0",
+  "optFixed32": 0,
+  "optFixed64": "0",
+  "optSfixed32": 0,
+  "optSfixed64": "0",
+  "optFloat": 0,
+  "optDouble": 0,
+  "optBytes": "",
+  "optString": ""
+}`,
+	}, {
+		desc: "proto2 optional scalars set to some values",
+		input: &pb2.Scalars{
+			OptBool:     scalar.Bool(true),
+			OptInt32:    scalar.Int32(0xff),
+			OptInt64:    scalar.Int64(0xdeadbeef),
+			OptUint32:   scalar.Uint32(47),
+			OptUint64:   scalar.Uint64(0xdeadbeef),
+			OptSint32:   scalar.Int32(-1001),
+			OptSint64:   scalar.Int64(-0xffff),
+			OptFixed64:  scalar.Uint64(64),
+			OptSfixed32: scalar.Int32(-32),
+			OptFloat:    scalar.Float32(1.02),
+			OptDouble:   scalar.Float64(1.234),
+			OptBytes:    []byte("\xe8\xb0\xb7\xe6\xad\x8c"),
+			OptString:   scalar.String("谷歌"),
+		},
+		want: `{
+  "optBool": true,
+  "optInt32": 255,
+  "optInt64": "3735928559",
+  "optUint32": 47,
+  "optUint64": "3735928559",
+  "optSint32": -1001,
+  "optSint64": "-65535",
+  "optFixed64": "64",
+  "optSfixed32": -32,
+  "optFloat": 1.02,
+  "optDouble": 1.234,
+  "optBytes": "6LC35q2M",
+  "optString": "谷歌"
+}`,
+	}, {
+		desc: "float nan",
+		input: &pb3.Scalars{
+			SFloat: float32(math.NaN()),
+		},
+		want: `{
+  "sFloat": "NaN"
+}`,
+	}, {
+		desc: "float positive infinity",
+		input: &pb3.Scalars{
+			SFloat: float32(math.Inf(1)),
+		},
+		want: `{
+  "sFloat": "Infinity"
+}`,
+	}, {
+		desc: "float negative infinity",
+		input: &pb3.Scalars{
+			SFloat: float32(math.Inf(-1)),
+		},
+		want: `{
+  "sFloat": "-Infinity"
+}`,
+	}, {
+		desc: "double nan",
+		input: &pb3.Scalars{
+			SDouble: math.NaN(),
+		},
+		want: `{
+  "sDouble": "NaN"
+}`,
+	}, {
+		desc: "double positive infinity",
+		input: &pb3.Scalars{
+			SDouble: math.Inf(1),
+		},
+		want: `{
+  "sDouble": "Infinity"
+}`,
+	}, {
+		desc: "double negative infinity",
+		input: &pb3.Scalars{
+			SDouble: math.Inf(-1),
+		},
+		want: `{
+  "sDouble": "-Infinity"
+}`,
+	}, {
+		desc:  "proto2 enum not set",
+		input: &pb2.Enums{},
+		want:  "{}",
+	}, {
+		desc: "proto2 enum set to zero value",
+		input: &pb2.Enums{
+			OptEnum:       pb2Enum(0),
+			OptNestedEnum: pb2Enums_NestedEnum(0),
+		},
+		want: `{
+  "optEnum": 0,
+  "optNestedEnum": 0
+}`,
+	}, {
+		desc: "proto2 enum",
+		input: &pb2.Enums{
+			OptEnum:       pb2.Enum_ONE.Enum(),
+			OptNestedEnum: pb2.Enums_UNO.Enum(),
+		},
+		want: `{
+  "optEnum": "ONE",
+  "optNestedEnum": "UNO"
+}`,
+	}, {
+		desc: "proto2 enum set to numeric values",
+		input: &pb2.Enums{
+			OptEnum:       pb2Enum(2),
+			OptNestedEnum: pb2Enums_NestedEnum(2),
+		},
+		want: `{
+  "optEnum": "TWO",
+  "optNestedEnum": "DOS"
+}`,
+	}, {
+		desc: "proto2 enum set to unnamed numeric values",
+		input: &pb2.Enums{
+			OptEnum:       pb2Enum(101),
+			OptNestedEnum: pb2Enums_NestedEnum(-101),
+		},
+		want: `{
+  "optEnum": 101,
+  "optNestedEnum": -101
+}`,
+	}, {
+		desc:  "proto3 enum not set",
+		input: &pb3.Enums{},
+		want:  "{}",
+	}, {
+		desc: "proto3 enum set to zero value",
+		input: &pb3.Enums{
+			SEnum:       pb3.Enum_ZERO,
+			SNestedEnum: pb3.Enums_CERO,
+		},
+		want: "{}",
+	}, {
+		desc: "proto3 enum",
+		input: &pb3.Enums{
+			SEnum:       pb3.Enum_ONE,
+			SNestedEnum: pb3.Enums_UNO,
+		},
+		want: `{
+  "sEnum": "ONE",
+  "sNestedEnum": "UNO"
+}`,
+	}, {
+		desc: "proto3 enum set to numeric values",
+		input: &pb3.Enums{
+			SEnum:       2,
+			SNestedEnum: 2,
+		},
+		want: `{
+  "sEnum": "TWO",
+  "sNestedEnum": "DOS"
+}`,
+	}, {
+		desc: "proto3 enum set to unnamed numeric values",
+		input: &pb3.Enums{
+			SEnum:       -47,
+			SNestedEnum: 47,
+		},
+		want: `{
+  "sEnum": -47,
+  "sNestedEnum": 47
+}`,
+	}, {
+		desc:  "proto2 nested message not set",
+		input: &pb2.Nests{},
+		want:  "{}",
+	}, {
+		desc: "proto2 nested message set to empty",
+		input: &pb2.Nests{
+			OptNested: &pb2.Nested{},
+			Optgroup:  &pb2.Nests_OptGroup{},
+		},
+		want: `{
+  "optNested": {},
+  "optgroup": {}
+}`,
+	}, {
+		desc: "proto2 nested messages",
+		input: &pb2.Nests{
+			OptNested: &pb2.Nested{
+				OptString: scalar.String("nested message"),
+				OptNested: &pb2.Nested{
+					OptString: scalar.String("another nested message"),
+				},
+			},
+		},
+		want: `{
+  "optNested": {
+    "optString": "nested message",
+    "optNested": {
+      "optString": "another nested message"
+    }
+  }
+}`,
+	}, {
+		desc: "proto2 groups",
+		input: &pb2.Nests{
+			Optgroup: &pb2.Nests_OptGroup{
+				OptString: scalar.String("inside a group"),
+				OptNested: &pb2.Nested{
+					OptString: scalar.String("nested message inside a group"),
+				},
+				Optnestedgroup: &pb2.Nests_OptGroup_OptNestedGroup{
+					OptFixed32: scalar.Uint32(47),
+				},
+			},
+		},
+		want: `{
+  "optgroup": {
+    "optString": "inside a group",
+    "optNested": {
+      "optString": "nested message inside a group"
+    },
+    "optnestedgroup": {
+      "optFixed32": 47
+    }
+  }
+}`,
+	}, {
+		desc:  "proto3 nested message not set",
+		input: &pb3.Nests{},
+		want:  "{}",
+	}, {
+		desc: "proto3 nested message set to empty",
+		input: &pb3.Nests{
+			SNested: &pb3.Nested{},
+		},
+		want: `{
+  "sNested": {}
+}`,
+	}, {
+		desc: "proto3 nested message",
+		input: &pb3.Nests{
+			SNested: &pb3.Nested{
+				SString: "nested message",
+				SNested: &pb3.Nested{
+					SString: "another nested message",
+				},
+			},
+		},
+		want: `{
+  "sNested": {
+    "sString": "nested message",
+    "sNested": {
+      "sString": "another nested message"
+    }
+  }
+}`,
+	}, {
+		desc:  "oneof not set",
+		input: &pb3.Oneofs{},
+		want:  "{}",
+	}, {
+		desc: "oneof set to empty string",
+		input: &pb3.Oneofs{
+			Union: &pb3.Oneofs_OneofString{},
+		},
+		want: `{
+  "oneofString": ""
+}`,
+	}, {
+		desc: "oneof set to string",
+		input: &pb3.Oneofs{
+			Union: &pb3.Oneofs_OneofString{
+				OneofString: "hello",
+			},
+		},
+		want: `{
+  "oneofString": "hello"
+}`,
+	}, {
+		desc: "oneof set to enum",
+		input: &pb3.Oneofs{
+			Union: &pb3.Oneofs_OneofEnum{
+				OneofEnum: pb3.Enum_ZERO,
+			},
+		},
+		want: `{
+  "oneofEnum": "ZERO"
+}`,
+	}, {
+		desc: "oneof set to empty message",
+		input: &pb3.Oneofs{
+			Union: &pb3.Oneofs_OneofNested{
+				OneofNested: &pb3.Nested{},
+			},
+		},
+		want: `{
+  "oneofNested": {}
+}`,
+	}, {
+		desc: "oneof set to message",
+		input: &pb3.Oneofs{
+			Union: &pb3.Oneofs_OneofNested{
+				OneofNested: &pb3.Nested{
+					SString: "nested message",
+				},
+			},
+		},
+		want: `{
+  "oneofNested": {
+    "sString": "nested message"
+  }
+}`,
+	}, {
+		desc:  "repeated fields not set",
+		input: &pb2.Repeats{},
+		want:  "{}",
+	}, {
+		desc: "repeated fields set to empty slices",
+		input: &pb2.Repeats{
+			RptBool:   []bool{},
+			RptInt32:  []int32{},
+			RptInt64:  []int64{},
+			RptUint32: []uint32{},
+			RptUint64: []uint64{},
+			RptFloat:  []float32{},
+			RptDouble: []float64{},
+			RptBytes:  [][]byte{},
+		},
+		want: "{}",
+	}, {
+		desc: "repeated fields set to some values",
+		input: &pb2.Repeats{
+			RptBool:   []bool{true, false, true, true},
+			RptInt32:  []int32{1, 6, 0, 0},
+			RptInt64:  []int64{-64, 47},
+			RptUint32: []uint32{0xff, 0xffff},
+			RptUint64: []uint64{0xdeadbeef},
+			RptFloat:  []float32{float32(math.NaN()), float32(math.Inf(1)), float32(math.Inf(-1)), 1.034},
+			RptDouble: []float64{math.NaN(), math.Inf(1), math.Inf(-1), 1.23e-308},
+			RptString: []string{"hello", "世界"},
+			RptBytes: [][]byte{
+				[]byte("hello"),
+				[]byte("\xe4\xb8\x96\xe7\x95\x8c"),
+			},
+		},
+		want: `{
+  "rptBool": [
+    true,
+    false,
+    true,
+    true
+  ],
+  "rptInt32": [
+    1,
+    6,
+    0,
+    0
+  ],
+  "rptInt64": [
+    "-64",
+    "47"
+  ],
+  "rptUint32": [
+    255,
+    65535
+  ],
+  "rptUint64": [
+    "3735928559"
+  ],
+  "rptFloat": [
+    "NaN",
+    "Infinity",
+    "-Infinity",
+    1.034
+  ],
+  "rptDouble": [
+    "NaN",
+    "Infinity",
+    "-Infinity",
+    1.23e-308
+  ],
+  "rptString": [
+    "hello",
+    "世界"
+  ],
+  "rptBytes": [
+    "aGVsbG8=",
+    "5LiW55WM"
+  ]
+}`,
+	}, {
+		desc: "repeated enums",
+		input: &pb2.Enums{
+			RptEnum:       []pb2.Enum{pb2.Enum_ONE, 2, pb2.Enum_TEN, 42},
+			RptNestedEnum: []pb2.Enums_NestedEnum{2, 47, 10},
+		},
+		want: `{
+  "rptEnum": [
+    "ONE",
+    "TWO",
+    "TEN",
+    42
+  ],
+  "rptNestedEnum": [
+    "DOS",
+    47,
+    "DIEZ"
+  ]
+}`,
+	}, {
+		desc: "repeated messages set to empty",
+		input: &pb2.Nests{
+			RptNested: []*pb2.Nested{},
+			Rptgroup:  []*pb2.Nests_RptGroup{},
+		},
+		want: "{}",
+	}, {
+		desc: "repeated messages",
+		input: &pb2.Nests{
+			RptNested: []*pb2.Nested{
+				{
+					OptString: scalar.String("repeat nested one"),
+				},
+				{
+					OptString: scalar.String("repeat nested two"),
+					OptNested: &pb2.Nested{
+						OptString: scalar.String("inside repeat nested two"),
+					},
+				},
+				{},
+			},
+		},
+		want: `{
+  "rptNested": [
+    {
+      "optString": "repeat nested one"
+    },
+    {
+      "optString": "repeat nested two",
+      "optNested": {
+        "optString": "inside repeat nested two"
+      }
+    },
+    {}
+  ]
+}`,
+	}, {
+		desc: "repeated messages contains nil value",
+		input: &pb2.Nests{
+			RptNested: []*pb2.Nested{nil, {}},
+		},
+		want: `{
+  "rptNested": [
+    {},
+    {}
+  ]
+}`,
+	}, {
+		desc: "repeated groups",
+		input: &pb2.Nests{
+			Rptgroup: []*pb2.Nests_RptGroup{
+				{
+					RptString: []string{"hello", "world"},
+				},
+				{},
+				nil,
+			},
+		},
+		want: `{
+  "rptgroup": [
+    {
+      "rptString": [
+        "hello",
+        "world"
+      ]
+    },
+    {},
+    {}
+  ]
+}`,
+	}, {
+		desc:  "map fields not set",
+		input: &pb3.Maps{},
+		want:  "{}",
+	}, {
+		desc: "map fields set to empty",
+		input: &pb3.Maps{
+			Int32ToStr:   map[int32]string{},
+			BoolToUint32: map[bool]uint32{},
+			Uint64ToEnum: map[uint64]pb3.Enum{},
+			StrToNested:  map[string]*pb3.Nested{},
+			StrToOneofs:  map[string]*pb3.Oneofs{},
+		},
+		want: "{}",
+	}, {
+		desc: "map fields 1",
+		input: &pb3.Maps{
+			BoolToUint32: map[bool]uint32{
+				true:  42,
+				false: 101,
+			},
+		},
+		want: `{
+  "boolToUint32": {
+    "false": 101,
+    "true": 42
+  }
+}`,
+	}, {
+		desc: "map fields 2",
+		input: &pb3.Maps{
+			Int32ToStr: map[int32]string{
+				-101: "-101",
+				0xff: "0xff",
+				0:    "zero",
+			},
+		},
+		want: `{
+  "int32ToStr": {
+    "-101": "-101",
+    "0": "zero",
+    "255": "0xff"
+  }
+}`,
+	}, {
+		desc: "map fields 3",
+		input: &pb3.Maps{
+			Uint64ToEnum: map[uint64]pb3.Enum{
+				1:  pb3.Enum_ONE,
+				2:  pb3.Enum_TWO,
+				10: pb3.Enum_TEN,
+				47: 47,
+			},
+		},
+		want: `{
+  "uint64ToEnum": {
+    "1": "ONE",
+    "2": "TWO",
+    "10": "TEN",
+    "47": 47
+  }
+}`,
+	}, {
+		desc: "map fields 4",
+		input: &pb3.Maps{
+			StrToNested: map[string]*pb3.Nested{
+				"nested": &pb3.Nested{
+					SString: "nested in a map",
+				},
+			},
+		},
+		want: `{
+  "strToNested": {
+    "nested": {
+      "sString": "nested in a map"
+    }
+  }
+}`,
+	}, {
+		desc: "map fields 5",
+		input: &pb3.Maps{
+			StrToOneofs: map[string]*pb3.Oneofs{
+				"string": &pb3.Oneofs{
+					Union: &pb3.Oneofs_OneofString{
+						OneofString: "hello",
+					},
+				},
+				"nested": &pb3.Oneofs{
+					Union: &pb3.Oneofs_OneofNested{
+						OneofNested: &pb3.Nested{
+							SString: "nested oneof in map field value",
+						},
+					},
+				},
+			},
+		},
+		want: `{
+  "strToOneofs": {
+    "nested": {
+      "oneofNested": {
+        "sString": "nested oneof in map field value"
+      }
+    },
+    "string": {
+      "oneofString": "hello"
+    }
+  }
+}`,
+	}, {
+		desc: "map field contains nil value",
+		input: &pb3.Maps{
+			StrToNested: map[string]*pb3.Nested{
+				"nil": nil,
+			},
+		},
+		want: `{
+  "strToNested": {
+    "nil": {}
+  }
+}`,
+	}, {
+		desc: "unknown fields are ignored",
+		input: &pb2.Scalars{
+			OptString: scalar.String("no unknowns"),
+			XXX_unrecognized: pack.Message{
+				pack.Tag{101, pack.BytesType}, pack.String("hello world"),
+			}.Marshal(),
+		},
+		want: `{
+  "optString": "no unknowns"
+}`,
+	}, {
+		desc: "json_name",
+		input: &pb3.JSONNames{
+			SString: "json_name",
+		},
+		want: `{
+  "foo_bar": "json_name"
+}`,
+	}}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.desc, func(t *testing.T) {
+			t.Parallel()
+			b, err := tt.mo.Marshal(tt.input)
+			if err != nil {
+				t.Errorf("Marshal() returned error: %v\n", err)
+			}
+			got := string(b)
+			if got != tt.want {
+				t.Errorf("Marshal()\n<got>\n%v\n<want>\n%v\n", got, tt.want)
+				if diff := cmp.Diff(tt.want, got, splitLines); diff != "" {
+					t.Errorf("Marshal() diff -want +got\n%v\n", diff)
+				}
+			}
+		})
+	}
+}