encoding: add Format helper function and method

The Format function and MarshalOptions.Format method are helper
functions for directly obtaining the formatted string for a message
without having to deal with errors or convert a []byte to string.
It is only intended for human consumption (e.g., debugging or logging).

We also add a MarshalOptions.Multiline option to specify that the output
should use some default indentation in a multiline output.

This assists in the v1 to v2 migration where:
	protoV1.CompactTextString(m) => prototext.MarshalOptions{}.Format(m)
	protoV1.MarshalTextString(m) => prototext.Format(m)

At Google, there are approximately 10x more usages of MarshalTextString than
CompactTextString, so it makes sense that the top-level Format function
does multiline expansion by default.

Fixes #850

Change-Id: I149c9e190a6d99b985d3884df675499a3313e9b3
Reviewed-on: https://go-review.googlesource.com/c/protobuf/+/213460
Reviewed-by: Damien Neil <dneil@google.com>
Reviewed-by: Herbie Ong <herbie@google.com>
diff --git a/encoding/protojson/encode.go b/encoding/protojson/encode.go
index 9b2592d..02723c0 100644
--- a/encoding/protojson/encode.go
+++ b/encoding/protojson/encode.go
@@ -19,6 +19,16 @@
 	"google.golang.org/protobuf/reflect/protoregistry"
 )
 
+const defaultIndent = "  "
+
+// Format formats the message as a multiline string.
+// This function is only intended for human consumption and ignores errors.
+// Do not depend on the output being stable. It may change over time across
+// different versions of the program.
+func Format(m proto.Message) string {
+	return MarshalOptions{Multiline: true}.Format(m)
+}
+
 // Marshal writes the given proto.Message in JSON format using default options.
 // Do not depend on the output being stable. It may change over time across
 // different versions of the program.
@@ -59,9 +69,15 @@
 	//  ╚═══════╧════════════════════════════╝
 	EmitUnpopulated bool
 
-	// If Indent is a non-empty string, it causes entries for an Array or Object
-	// to be preceded by the indent and trailed by a newline. Indent can only be
-	// composed of space or tab characters.
+	// Multiline specifies whether the marshaler should format the output in
+	// indented-form with every textual element on a new line.
+	// If Indent is an empty string, then an arbitrary indent is chosen.
+	Multiline bool
+
+	// Indent specifies the set of indentation characters to use in a multiline
+	// formatted output such that every entry is preceded by Indent and
+	// terminated by a newline. If non-empty, then Multiline is treated as true.
+	// Indent can only be composed of space or tab characters.
 	Indent string
 
 	// Resolver is used for looking up types when expanding google.protobuf.Any
@@ -72,18 +88,35 @@
 	}
 }
 
+// Format formats the message as a string.
+// This method is only intended for human consumption and ignores errors.
+// Do not depend on the output being stable. It may change over time across
+// different versions of the program.
+func (o MarshalOptions) Format(m proto.Message) string {
+	if m == nil || !m.ProtoReflect().IsValid() {
+		return "<nil>" // invalid syntax, but okay since this is for debugging
+	}
+	o.AllowPartial = true
+	b, _ := o.Marshal(m)
+	return string(b)
+}
+
 // Marshal marshals the given proto.Message in the JSON format using options in
 // MarshalOptions. Do not depend on the output being stable. It may change over
 // time across different versions of the program.
 func (o MarshalOptions) Marshal(m proto.Message) ([]byte, error) {
+	if o.Multiline && o.Indent == "" {
+		o.Indent = defaultIndent
+	}
+	if o.Resolver == nil {
+		o.Resolver = protoregistry.GlobalTypes
+	}
+
 	var err error
 	o.encoder, err = json.NewEncoder(o.Indent)
 	if err != nil {
 		return nil, err
 	}
-	if o.Resolver == nil {
-		o.Resolver = protoregistry.GlobalTypes
-	}
 
 	err = o.marshalMessage(m.ProtoReflect())
 	if err != nil {
diff --git a/encoding/prototext/encode.go b/encoding/prototext/encode.go
index c7c3b8a..1fa5fe4 100644
--- a/encoding/prototext/encode.go
+++ b/encoding/prototext/encode.go
@@ -22,6 +22,16 @@
 	"google.golang.org/protobuf/reflect/protoregistry"
 )
 
+const defaultIndent = "  "
+
+// Format formats the message as a multiline string.
+// This function is only intended for human consumption and ignores errors.
+// Do not depend on the output being stable. It may change over time across
+// different versions of the program.
+func Format(m proto.Message) string {
+	return MarshalOptions{Multiline: true}.Format(m)
+}
+
 // Marshal writes the given proto.Message in textproto format using default
 // options. Do not depend on the output being stable. It may change over time
 // across different versions of the program.
@@ -43,9 +53,15 @@
 	// The default is to exclude unknown fields.
 	EmitUnknown bool
 
-	// If Indent is a non-empty string, it causes entries for a Message to be
-	// preceded by the indent and trailed by a newline. Indent can only be
-	// composed of space or tab characters.
+	// Multiline specifies whether the marshaler should format the output in
+	// indented-form with every textual element on a new line.
+	// If Indent is an empty string, then an arbitrary indent is chosen.
+	Multiline bool
+
+	// Indent specifies the set of indentation characters to use in a multiline
+	// formatted output such that every entry is preceded by Indent and
+	// terminated by a newline. If non-empty, then Multiline is treated as true.
+	// Indent can only be composed of space or tab characters.
 	Indent string
 
 	// Resolver is used for looking up types when expanding google.protobuf.Any
@@ -56,10 +72,27 @@
 	}
 }
 
+// Format formats the message as a string.
+// This method is only intended for human consumption and ignores errors.
+// Do not depend on the output being stable. It may change over time across
+// different versions of the program.
+func (o MarshalOptions) Format(m proto.Message) string {
+	if m == nil || !m.ProtoReflect().IsValid() {
+		return "<nil>" // invalid syntax, but okay since this is for debugging
+	}
+	o.AllowPartial = true
+	o.EmitUnknown = true
+	b, _ := o.Marshal(m)
+	return string(b)
+}
+
 // Marshal writes the given proto.Message in textproto format using options in
 // MarshalOptions object. Do not depend on the output being stable. It may
 // change over time across different versions of the program.
 func (o MarshalOptions) Marshal(m proto.Message) ([]byte, error) {
+	if o.Multiline && o.Indent == "" {
+		o.Indent = defaultIndent
+	}
 	if o.Resolver == nil {
 		o.Resolver = protoregistry.GlobalTypes
 	}
diff --git a/internal/impl/api_export.go b/internal/impl/api_export.go
index 54cf956..76dfdf7 100644
--- a/internal/impl/api_export.go
+++ b/internal/impl/api_export.go
@@ -166,14 +166,7 @@
 // MessageStringOf returns the message value as a string,
 // which is the message serialized in the protobuf text format.
 func (Export) MessageStringOf(m pref.ProtoMessage) string {
-	if m == nil || !m.ProtoReflect().IsValid() {
-		return "<nil>"
-	}
-	b, _ := prototext.MarshalOptions{
-		AllowPartial: true,
-		EmitUnknown:  true,
-	}.Marshal(m)
-	return string(b)
+	return prototext.MarshalOptions{Multiline: false}.Format(m)
 }
 
 // ExtensionDescFromType returns the legacy protoV1.ExtensionDesc for t.
diff --git a/proto/decode_test.go b/proto/decode_test.go
index 5ccb816..3b8a092 100644
--- a/proto/decode_test.go
+++ b/proto/decode_test.go
@@ -30,7 +30,7 @@
 				wire := append(([]byte)(nil), test.wire...)
 				got := reflect.New(reflect.TypeOf(want).Elem()).Interface().(proto.Message)
 				if err := opts.Unmarshal(wire, got); err != nil {
-					t.Errorf("Unmarshal error: %v\nMessage:\n%v", err, marshalText(want))
+					t.Errorf("Unmarshal error: %v\nMessage:\n%v", err, prototext.Format(want))
 					return
 				}
 
@@ -40,7 +40,7 @@
 					wire[i] = 0
 				}
 				if !proto.Equal(got, want) && got.ProtoReflect().IsValid() && want.ProtoReflect().IsValid() {
-					t.Errorf("Unmarshal returned unexpected result; got:\n%v\nwant:\n%v", marshalText(got), marshalText(want))
+					t.Errorf("Unmarshal returned unexpected result; got:\n%v\nwant:\n%v", prototext.Format(got), prototext.Format(want))
 				}
 			})
 		}
@@ -58,7 +58,7 @@
 				opts.AllowPartial = false
 				got := reflect.New(reflect.TypeOf(m).Elem()).Interface().(proto.Message)
 				if err := proto.Unmarshal(test.wire, got); err == nil {
-					t.Fatalf("Unmarshal succeeded (want error)\nMessage:\n%v", marshalText(got))
+					t.Fatalf("Unmarshal succeeded (want error)\nMessage:\n%v", prototext.Format(got))
 				}
 			})
 		}
@@ -76,7 +76,7 @@
 				opts.AllowPartial = test.partial
 				got := want.ProtoReflect().New().Interface()
 				if err := opts.Unmarshal(test.wire, got); err == nil {
-					t.Errorf("Unmarshal unexpectedly succeeded\ninput bytes: [%x]\nMessage:\n%v", test.wire, marshalText(got))
+					t.Errorf("Unmarshal unexpectedly succeeded\ninput bytes: [%x]\nMessage:\n%v", test.wire, prototext.Format(got))
 				}
 			})
 		}
@@ -147,15 +147,3 @@
 		proto.SetExtension(m, desc, value)
 	}
 }
-
-func marshalText(m proto.Message) string {
-	if m == nil {
-		return "<nil>\n"
-	}
-	b, _ := prototext.MarshalOptions{
-		AllowPartial: true,
-		EmitUnknown:  true,
-		Indent:       "\t",
-	}.Marshal(m)
-	return string(b)
-}
diff --git a/proto/encode_test.go b/proto/encode_test.go
index 339249c..c2fefc3 100644
--- a/proto/encode_test.go
+++ b/proto/encode_test.go
@@ -11,6 +11,7 @@
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
+	"google.golang.org/protobuf/encoding/prototext"
 	"google.golang.org/protobuf/internal/encoding/wire"
 	"google.golang.org/protobuf/proto"
 	pref "google.golang.org/protobuf/reflect/protoreflect"
@@ -29,12 +30,12 @@
 				}
 				wire, err := opts.Marshal(want)
 				if err != nil {
-					t.Fatalf("Marshal error: %v\nMessage:\n%v", err, marshalText(want))
+					t.Fatalf("Marshal error: %v\nMessage:\n%v", err, prototext.Format(want))
 				}
 
 				size := proto.Size(want)
 				if size != len(wire) {
-					t.Errorf("Size and marshal disagree: Size(m)=%v; len(Marshal(m))=%v\nMessage:\n%v", size, len(wire), marshalText(want))
+					t.Errorf("Size and marshal disagree: Size(m)=%v; len(Marshal(m))=%v\nMessage:\n%v", size, len(wire), prototext.Format(want))
 				}
 
 				got := want.ProtoReflect().New().Interface()
@@ -42,11 +43,11 @@
 					AllowPartial: test.partial,
 				}
 				if err := uopts.Unmarshal(wire, got); err != nil {
-					t.Errorf("Unmarshal error: %v\nMessage:\n%v", err, marshalText(want))
+					t.Errorf("Unmarshal error: %v\nMessage:\n%v", err, prototext.Format(want))
 					return
 				}
 				if !proto.Equal(got, want) && got.ProtoReflect().IsValid() && want.ProtoReflect().IsValid() {
-					t.Errorf("Unmarshal returned unexpected result; got:\n%v\nwant:\n%v", marshalText(got), marshalText(want))
+					t.Errorf("Unmarshal returned unexpected result; got:\n%v\nwant:\n%v", prototext.Format(got), prototext.Format(want))
 				}
 			})
 		}
@@ -63,11 +64,11 @@
 				}
 				wire, err := opts.Marshal(want)
 				if err != nil {
-					t.Fatalf("Marshal error: %v\nMessage:\n%v", err, marshalText(want))
+					t.Fatalf("Marshal error: %v\nMessage:\n%v", err, prototext.Format(want))
 				}
 				wire2, err := opts.Marshal(want)
 				if err != nil {
-					t.Fatalf("Marshal error: %v\nMessage:\n%v", err, marshalText(want))
+					t.Fatalf("Marshal error: %v\nMessage:\n%v", err, prototext.Format(want))
 				}
 				if !bytes.Equal(wire, wire2) {
 					t.Fatalf("deterministic marshal returned varying results:\n%v", cmp.Diff(wire, wire2))
@@ -78,11 +79,11 @@
 					AllowPartial: test.partial,
 				}
 				if err := uopts.Unmarshal(wire, got); err != nil {
-					t.Errorf("Unmarshal error: %v\nMessage:\n%v", err, marshalText(want))
+					t.Errorf("Unmarshal error: %v\nMessage:\n%v", err, prototext.Format(want))
 					return
 				}
 				if !proto.Equal(got, want) && got.ProtoReflect().IsValid() && want.ProtoReflect().IsValid() {
-					t.Errorf("Unmarshal returned unexpected result; got:\n%v\nwant:\n%v", marshalText(got), marshalText(want))
+					t.Errorf("Unmarshal returned unexpected result; got:\n%v\nwant:\n%v", prototext.Format(got), prototext.Format(want))
 				}
 			})
 		}
@@ -98,7 +99,7 @@
 			t.Run(fmt.Sprintf("%s (%T)", test.desc, m), func(t *testing.T) {
 				_, err := proto.Marshal(m)
 				if err == nil {
-					t.Fatalf("Marshal succeeded (want error)\nMessage:\n%v", marshalText(m))
+					t.Fatalf("Marshal succeeded (want error)\nMessage:\n%v", prototext.Format(m))
 				}
 			})
 		}
@@ -131,7 +132,7 @@
 				}
 				got, err := opts.Marshal(m)
 				if err == nil {
-					t.Fatalf("Marshal unexpectedly succeeded\noutput bytes: [%x]\nMessage:\n%v", got, marshalText(m))
+					t.Fatalf("Marshal unexpectedly succeeded\noutput bytes: [%x]\nMessage:\n%v", got, prototext.Format(m))
 				}
 			})
 		}
diff --git a/proto/equal_test.go b/proto/equal_test.go
index ac529ec..65a8bfb 100644
--- a/proto/equal_test.go
+++ b/proto/equal_test.go
@@ -8,6 +8,7 @@
 	"math"
 	"testing"
 
+	"google.golang.org/protobuf/encoding/prototext"
 	"google.golang.org/protobuf/internal/encoding/pack"
 	"google.golang.org/protobuf/proto"
 
@@ -431,13 +432,13 @@
 
 	for _, tt := range tests {
 		if !tt.eq && !proto.Equal(tt.x, tt.x) {
-			t.Errorf("Equal(x, x) = false, want true\n==== x ====\n%v", marshalText(tt.x))
+			t.Errorf("Equal(x, x) = false, want true\n==== x ====\n%v", prototext.Format(tt.x))
 		}
 		if !tt.eq && !proto.Equal(tt.y, tt.y) {
-			t.Errorf("Equal(y, y) = false, want true\n==== y ====\n%v", marshalText(tt.y))
+			t.Errorf("Equal(y, y) = false, want true\n==== y ====\n%v", prototext.Format(tt.y))
 		}
 		if eq := proto.Equal(tt.x, tt.y); eq != tt.eq {
-			t.Errorf("Equal(x, y) = %v, want %v\n==== x ====\n%v==== y ====\n%v", eq, tt.eq, marshalText(tt.x), marshalText(tt.y))
+			t.Errorf("Equal(x, y) = %v, want %v\n==== x ====\n%v==== y ====\n%v", eq, tt.eq, prototext.Format(tt.x), prototext.Format(tt.y))
 		}
 	}
 }
diff --git a/proto/isinit_test.go b/proto/isinit_test.go
index 6a3a8c9..e38b76d 100644
--- a/proto/isinit_test.go
+++ b/proto/isinit_test.go
@@ -9,6 +9,7 @@
 	"strings"
 	"testing"
 
+	"google.golang.org/protobuf/encoding/prototext"
 	"google.golang.org/protobuf/internal/flags"
 	"google.golang.org/protobuf/proto"
 
@@ -81,7 +82,7 @@
 				got = fmt.Sprintf("%q", err)
 			}
 			if !strings.Contains(got, tt.want) {
-				t.Errorf("IsInitialized(m):\n got: %v\nwant contains: %v\nMessage:\n%v", got, tt.want, marshalText(tt.m))
+				t.Errorf("IsInitialized(m):\n got: %v\nwant contains: %v\nMessage:\n%v", got, tt.want, prototext.Format(tt.m))
 			}
 		})
 	}
diff --git a/testing/prototest/prototest.go b/testing/prototest/prototest.go
index cabe0b0..7a403a9 100644
--- a/testing/prototest/prototest.go
+++ b/testing/prototest/prototest.go
@@ -72,25 +72,20 @@
 		AllowPartial: true,
 	}.Marshal(m2)
 	if err != nil {
-		t.Errorf("Marshal() = %v, want nil\n%v", err, marshalText(m2))
+		t.Errorf("Marshal() = %v, want nil\n%v", err, prototext.Format(m2))
 	}
 	m3 := m.ProtoReflect().New().Interface()
 	if err := (proto.UnmarshalOptions{
 		AllowPartial: true,
 		Resolver:     opts.Resolver,
 	}.Unmarshal(b, m3)); err != nil {
-		t.Errorf("Unmarshal() = %v, want nil\n%v", err, marshalText(m2))
+		t.Errorf("Unmarshal() = %v, want nil\n%v", err, prototext.Format(m2))
 	}
 	if !proto.Equal(m2, m3) {
-		t.Errorf("round-trip marshal/unmarshal did not preserve message\nOriginal:\n%v\nNew:\n%v", marshalText(m2), marshalText(m3))
+		t.Errorf("round-trip marshal/unmarshal did not preserve message\nOriginal:\n%v\nNew:\n%v", prototext.Format(m2), prototext.Format(m3))
 	}
 }
 
-func marshalText(m proto.Message) string {
-	b, _ := prototext.MarshalOptions{Indent: "  "}.Marshal(m)
-	return string(b)
-}
-
 func testType(t testing.TB, m proto.Message) {
 	want := reflect.TypeOf(m)
 	if got := reflect.TypeOf(m.ProtoReflect().Interface()); got != want {