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 {