From 567579b50517e4f7efc459ab1d9d5ee2577af024 Mon Sep 17 00:00:00 2001 From: Jon Skeet Date: Mon, 23 Nov 2015 12:43:54 +0000 Subject: JSON formatting for Any. --- .../src/Google.Protobuf.Test/JsonFormatterTest.cs | 42 ++++++++ csharp/src/Google.Protobuf/JsonFormatter.cs | 111 +++++++++++++++++---- 2 files changed, 135 insertions(+), 18 deletions(-) (limited to 'csharp') diff --git a/csharp/src/Google.Protobuf.Test/JsonFormatterTest.cs b/csharp/src/Google.Protobuf.Test/JsonFormatterTest.cs index 08cedad8..8473b4be 100644 --- a/csharp/src/Google.Protobuf.Test/JsonFormatterTest.cs +++ b/csharp/src/Google.Protobuf.Test/JsonFormatterTest.cs @@ -35,6 +35,7 @@ using Google.Protobuf.TestProtos; using NUnit.Framework; using UnitTest.Issues.TestProtos; using Google.Protobuf.WellKnownTypes; +using Google.Protobuf.Reflection; namespace Google.Protobuf { @@ -420,6 +421,47 @@ namespace Google.Protobuf AssertJson("{ 'fileName': 'foo.proto' }", JsonFormatter.Default.Format(message)); } + [Test] + public void AnyWellKnownType() + { + var formatter = new JsonFormatter(new JsonFormatter.Settings(false, TypeRegistry.FromMessages(Timestamp.Descriptor))); + var timestamp = new DateTime(1673, 6, 19, 12, 34, 56, DateTimeKind.Utc).ToTimestamp(); + var any = Any.Pack(timestamp); + AssertJson("{ '@type': 'type.googleapis.com/google.protobuf.Timestamp', 'value': '1673-06-19T12:34:56Z' }", formatter.Format(any)); + } + + [Test] + public void AnyMessageType() + { + var formatter = new JsonFormatter(new JsonFormatter.Settings(false, TypeRegistry.FromMessages(TestAllTypes.Descriptor))); + var message = new TestAllTypes { SingleInt32 = 10, SingleNestedMessage = new TestAllTypes.Types.NestedMessage { Bb = 20 } }; + var any = Any.Pack(message); + AssertJson("{ '@type': 'type.googleapis.com/protobuf_unittest.TestAllTypes', 'singleInt32': 10, 'singleNestedMessage': { 'bb': 20 } }", formatter.Format(any)); + } + + [Test] + public void AnyNested() + { + var registry = TypeRegistry.FromMessages(TestWellKnownTypes.Descriptor, TestAllTypes.Descriptor); + var formatter = new JsonFormatter(new JsonFormatter.Settings(false, registry)); + + // Nest an Any as the value of an Any. + var doubleNestedMessage = new TestAllTypes { SingleInt32 = 20 }; + var nestedMessage = Any.Pack(doubleNestedMessage); + var message = new TestWellKnownTypes { AnyField = Any.Pack(nestedMessage) }; + AssertJson("{ 'anyField': { '@type': 'type.googleapis.com/google.protobuf.Any', 'value': { '@type': 'type.googleapis.com/protobuf_unittest.TestAllTypes', 'singleInt32': 20 } } }", + formatter.Format(message)); + } + + [Test] + public void AnyUnknownType() + { + // The default type registry doesn't have any types in it. + var message = new TestAllTypes(); + var any = Any.Pack(message); + Assert.Throws(() => JsonFormatter.Default.Format(any)); + } + /// /// Checks that the actual JSON is the same as the expected JSON - but after replacing /// all apostrophes in the expected JSON with double quotes. This basically makes the tests easier diff --git a/csharp/src/Google.Protobuf/JsonFormatter.cs b/csharp/src/Google.Protobuf/JsonFormatter.cs index 51bb4bf3..c7d392cd 100644 --- a/csharp/src/Google.Protobuf/JsonFormatter.cs +++ b/csharp/src/Google.Protobuf/JsonFormatter.cs @@ -55,6 +55,12 @@ namespace Google.Protobuf /// public sealed class JsonFormatter { + internal const string AnyTypeUrlField = "@type"; + internal const string AnyWellKnownTypeValueField = "value"; + private const string TypeUrlPrefix = "type.googleapis.com"; + private const string NameValueSeparator = ": "; + private const string PropertySeparator = ", "; + private static JsonFormatter defaultInstance = new JsonFormatter(Settings.Default); /// @@ -130,7 +136,7 @@ namespace Google.Protobuf /// The formatted message. public string Format(IMessage message) { - Preconditions.CheckNotNull(message, "message"); + Preconditions.CheckNotNull(message, nameof(message)); StringBuilder builder = new StringBuilder(); if (message.Descriptor.IsWellKnownType) { @@ -151,13 +157,18 @@ namespace Google.Protobuf return; } builder.Append("{ "); + bool writtenFields = WriteMessageFields(builder, message, false); + builder.Append(writtenFields ? " }" : "}"); + } + + private bool WriteMessageFields(StringBuilder builder, IMessage message, bool assumeFirstFieldWritten) + { var fields = message.Descriptor.Fields; - bool first = true; + bool first = !assumeFirstFieldWritten; // First non-oneof fields foreach (var field in fields.InFieldNumberOrder()) { var accessor = field.Accessor; - // Oneofs are written later if (field.ContainingOneof != null && field.ContainingOneof.Accessor.GetCaseFieldDescriptor(message) != field) { continue; @@ -178,14 +189,14 @@ namespace Google.Protobuf // Okay, all tests complete: let's write the field value... if (!first) { - builder.Append(", "); + builder.Append(PropertySeparator); } WriteString(builder, ToCamelCase(accessor.Descriptor.Name)); - builder.Append(": "); + builder.Append(NameValueSeparator); WriteValue(builder, value); first = false; } - builder.Append(first ? "}" : " }"); + return !first; } // Converted from src/google/protobuf/util/internal/utility.cc ToCamelCase @@ -378,6 +389,8 @@ namespace Google.Protobuf /// private void WriteWellKnownTypeValue(StringBuilder builder, MessageDescriptor descriptor, object value, bool inField) { + // Currently, we can never actually get here, because null values are always handled by the caller. But if we *could*, + // this would do the right thing. if (value == null) { WriteNull(builder); @@ -429,6 +442,11 @@ namespace Google.Protobuf WriteStructFieldValue(builder, (IMessage) value); return; } + if (descriptor.FullName == Any.Descriptor.FullName) + { + WriteAny(builder, (IMessage) value); + return; + } WriteMessage(builder, (IMessage) value); } @@ -496,6 +514,46 @@ namespace Google.Protobuf AppendEscapedString(builder, string.Join(",", paths.Cast().Select(ToCamelCase))); } + private void WriteAny(StringBuilder builder, IMessage value) + { + string typeUrl = (string) value.Descriptor.Fields[Any.TypeUrlFieldNumber].Accessor.GetValue(value); + ByteString data = (ByteString) value.Descriptor.Fields[Any.ValueFieldNumber].Accessor.GetValue(value); + string typeName = GetTypeName(typeUrl); + MessageDescriptor descriptor = settings.TypeRegistry.Find(typeName); + if (descriptor == null) + { + throw new InvalidOperationException($"Type registry has no descriptor for type name '{typeName}'"); + } + IMessage message = descriptor.Parser.ParseFrom(data); + builder.Append("{ "); + WriteString(builder, AnyTypeUrlField); + builder.Append(NameValueSeparator); + WriteString(builder, typeUrl); + + if (descriptor.IsWellKnownType) + { + builder.Append(PropertySeparator); + WriteString(builder, AnyWellKnownTypeValueField); + builder.Append(NameValueSeparator); + WriteWellKnownTypeValue(builder, descriptor, message, true); + } + else + { + WriteMessageFields(builder, message, true); + } + builder.Append(" }"); + } + + internal static string GetTypeName(String typeUrl) + { + string[] parts = typeUrl.Split('/'); + if (parts.Length != 2 || parts[0] != TypeUrlPrefix) + { + throw new InvalidProtocolBufferException($"Invalid type url: {typeUrl}"); + } + return parts[1]; + } + /// /// Appends a number of nanoseconds to a StringBuilder. Either 0 digits are added (in which /// case no "." is appended), or 3 6 or 9 digits. @@ -537,10 +595,10 @@ namespace Google.Protobuf if (!first) { - builder.Append(", "); + builder.Append(PropertySeparator); } WriteString(builder, key); - builder.Append(": "); + builder.Append(NameValueSeparator); WriteStructFieldValue(builder, value); first = false; } @@ -590,7 +648,7 @@ namespace Google.Protobuf } if (!first) { - builder.Append(", "); + builder.Append(PropertySeparator); } WriteValue(builder, value); first = false; @@ -611,7 +669,7 @@ namespace Google.Protobuf } if (!first) { - builder.Append(", "); + builder.Append(PropertySeparator); } string keyText; if (pair.Key is string) @@ -635,7 +693,7 @@ namespace Google.Protobuf throw new ArgumentException("Unhandled dictionary key type: " + pair.Key.GetType()); } WriteString(builder, keyText); - builder.Append(": "); + builder.Append(NameValueSeparator); WriteValue(builder, pair.Value); first = false; } @@ -755,23 +813,40 @@ namespace Google.Protobuf /// /// Default settings, as used by /// - public static Settings Default { get { return defaultInstance; } } - - private readonly bool formatDefaultValues; + public static Settings Default { get; } = new Settings(false); /// /// Whether fields whose values are the default for the field type (e.g. 0 for integers) /// should be formatted (true) or omitted (false). /// - public bool FormatDefaultValues { get { return formatDefaultValues; } } + public bool FormatDefaultValues { get; } + + /// + /// The type registry used to format messages. + /// + public TypeRegistry TypeRegistry { get; } + + // TODO: Work out how we're going to scale this to multiple settings. "WithXyz" methods? + + /// + /// Creates a new object with the specified formatting of default values + /// and an empty type registry. + /// + /// true if default values (0, empty strings etc) should be formatted; false otherwise. + public Settings(bool formatDefaultValues) : this(formatDefaultValues, TypeRegistry.Empty) + { + } /// - /// Creates a new object with the specified formatting of default values. + /// Creates a new object with the specified formatting of default values + /// and type registry. /// /// true if default values (0, empty strings etc) should be formatted; false otherwise. - public Settings(bool formatDefaultValues) + /// The to use when formatting messages. + public Settings(bool formatDefaultValues, TypeRegistry typeRegistry) { - this.formatDefaultValues = formatDefaultValues; + FormatDefaultValues = formatDefaultValues; + TypeRegistry = Preconditions.CheckNotNull(typeRegistry, nameof(typeRegistry)); } } } -- cgit v1.2.3