瀏覽代碼

JSON formatting for Any.

Jon Skeet 10 年之前
父節點
當前提交
567579b505
共有 2 個文件被更改,包括 135 次插入18 次删除
  1. 42 0
      csharp/src/Google.Protobuf.Test/JsonFormatterTest.cs
  2. 93 18
      csharp/src/Google.Protobuf/JsonFormatter.cs

+ 42 - 0
csharp/src/Google.Protobuf.Test/JsonFormatterTest.cs

@@ -35,6 +35,7 @@ using Google.Protobuf.TestProtos;
 using NUnit.Framework;
 using NUnit.Framework;
 using UnitTest.Issues.TestProtos;
 using UnitTest.Issues.TestProtos;
 using Google.Protobuf.WellKnownTypes;
 using Google.Protobuf.WellKnownTypes;
+using Google.Protobuf.Reflection;
 
 
 namespace Google.Protobuf
 namespace Google.Protobuf
 {
 {
@@ -420,6 +421,47 @@ namespace Google.Protobuf
             AssertJson("{ 'fileName': 'foo.proto' }", JsonFormatter.Default.Format(message));
             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<InvalidOperationException>(() => JsonFormatter.Default.Format(any));
+        }
+
         /// <summary>
         /// <summary>
         /// Checks that the actual JSON is the same as the expected JSON - but after replacing
         /// 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
         /// all apostrophes in the expected JSON with double quotes. This basically makes the tests easier

+ 93 - 18
csharp/src/Google.Protobuf/JsonFormatter.cs

@@ -55,6 +55,12 @@ namespace Google.Protobuf
     /// </remarks>
     /// </remarks>
     public sealed class JsonFormatter
     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);
         private static JsonFormatter defaultInstance = new JsonFormatter(Settings.Default);
 
 
         /// <summary>
         /// <summary>
@@ -130,7 +136,7 @@ namespace Google.Protobuf
         /// <returns>The formatted message.</returns>
         /// <returns>The formatted message.</returns>
         public string Format(IMessage message)
         public string Format(IMessage message)
         {
         {
-            Preconditions.CheckNotNull(message, "message");
+            Preconditions.CheckNotNull(message, nameof(message));
             StringBuilder builder = new StringBuilder();
             StringBuilder builder = new StringBuilder();
             if (message.Descriptor.IsWellKnownType)
             if (message.Descriptor.IsWellKnownType)
             {
             {
@@ -151,13 +157,18 @@ namespace Google.Protobuf
                 return;
                 return;
             }
             }
             builder.Append("{ ");
             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;
             var fields = message.Descriptor.Fields;
-            bool first = true;
+            bool first = !assumeFirstFieldWritten;
             // First non-oneof fields
             // First non-oneof fields
             foreach (var field in fields.InFieldNumberOrder())
             foreach (var field in fields.InFieldNumberOrder())
             {
             {
                 var accessor = field.Accessor;
                 var accessor = field.Accessor;
-                // Oneofs are written later
                 if (field.ContainingOneof != null && field.ContainingOneof.Accessor.GetCaseFieldDescriptor(message) != field)
                 if (field.ContainingOneof != null && field.ContainingOneof.Accessor.GetCaseFieldDescriptor(message) != field)
                 {
                 {
                     continue;
                     continue;
@@ -178,14 +189,14 @@ namespace Google.Protobuf
                 // Okay, all tests complete: let's write the field value...
                 // Okay, all tests complete: let's write the field value...
                 if (!first)
                 if (!first)
                 {
                 {
-                    builder.Append(", ");
+                    builder.Append(PropertySeparator);
                 }
                 }
                 WriteString(builder, ToCamelCase(accessor.Descriptor.Name));
                 WriteString(builder, ToCamelCase(accessor.Descriptor.Name));
-                builder.Append(": ");
+                builder.Append(NameValueSeparator);
                 WriteValue(builder, value);
                 WriteValue(builder, value);
                 first = false;
                 first = false;
             }            
             }            
-            builder.Append(first ? "}" : " }");
+            return !first;
         }
         }
 
 
         // Converted from src/google/protobuf/util/internal/utility.cc ToCamelCase
         // Converted from src/google/protobuf/util/internal/utility.cc ToCamelCase
@@ -378,6 +389,8 @@ namespace Google.Protobuf
         /// </summary>
         /// </summary>
         private void WriteWellKnownTypeValue(StringBuilder builder, MessageDescriptor descriptor, object value, bool inField)
         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)
             if (value == null)
             {
             {
                 WriteNull(builder);
                 WriteNull(builder);
@@ -429,6 +442,11 @@ namespace Google.Protobuf
                 WriteStructFieldValue(builder, (IMessage) value);
                 WriteStructFieldValue(builder, (IMessage) value);
                 return;
                 return;
             }
             }
+            if (descriptor.FullName == Any.Descriptor.FullName)
+            {
+                WriteAny(builder, (IMessage) value);
+                return;
+            }
             WriteMessage(builder, (IMessage) value);
             WriteMessage(builder, (IMessage) value);
         }
         }
 
 
@@ -496,6 +514,46 @@ namespace Google.Protobuf
             AppendEscapedString(builder, string.Join(",", paths.Cast<string>().Select(ToCamelCase)));
             AppendEscapedString(builder, string.Join(",", paths.Cast<string>().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];
+        }
+
         /// <summary>
         /// <summary>
         /// Appends a number of nanoseconds to a StringBuilder. Either 0 digits are added (in which
         /// 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.
         /// case no "." is appended), or 3 6 or 9 digits.
@@ -537,10 +595,10 @@ namespace Google.Protobuf
 
 
                 if (!first)
                 if (!first)
                 {
                 {
-                    builder.Append(", ");
+                    builder.Append(PropertySeparator);
                 }
                 }
                 WriteString(builder, key);
                 WriteString(builder, key);
-                builder.Append(": ");
+                builder.Append(NameValueSeparator);
                 WriteStructFieldValue(builder, value);
                 WriteStructFieldValue(builder, value);
                 first = false;
                 first = false;
             }
             }
@@ -590,7 +648,7 @@ namespace Google.Protobuf
                 }
                 }
                 if (!first)
                 if (!first)
                 {
                 {
-                    builder.Append(", ");
+                    builder.Append(PropertySeparator);
                 }
                 }
                 WriteValue(builder, value);
                 WriteValue(builder, value);
                 first = false;
                 first = false;
@@ -611,7 +669,7 @@ namespace Google.Protobuf
                 }
                 }
                 if (!first)
                 if (!first)
                 {
                 {
-                    builder.Append(", ");
+                    builder.Append(PropertySeparator);
                 }
                 }
                 string keyText;
                 string keyText;
                 if (pair.Key is string)
                 if (pair.Key is string)
@@ -635,7 +693,7 @@ namespace Google.Protobuf
                     throw new ArgumentException("Unhandled dictionary key type: " + pair.Key.GetType());
                     throw new ArgumentException("Unhandled dictionary key type: " + pair.Key.GetType());
                 }
                 }
                 WriteString(builder, keyText);
                 WriteString(builder, keyText);
-                builder.Append(": ");
+                builder.Append(NameValueSeparator);
                 WriteValue(builder, pair.Value);
                 WriteValue(builder, pair.Value);
                 first = false;
                 first = false;
             }
             }
@@ -755,23 +813,40 @@ namespace Google.Protobuf
             /// <summary>
             /// <summary>
             /// Default settings, as used by <see cref="JsonFormatter.Default"/>
             /// Default settings, as used by <see cref="JsonFormatter.Default"/>
             /// </summary>
             /// </summary>
-            public static Settings Default { get { return defaultInstance; } }
-
-            private readonly bool formatDefaultValues;
+            public static Settings Default { get; } = new Settings(false);
 
 
             /// <summary>
             /// <summary>
             /// Whether fields whose values are the default for the field type (e.g. 0 for integers)
             /// Whether fields whose values are the default for the field type (e.g. 0 for integers)
             /// should be formatted (true) or omitted (false).
             /// should be formatted (true) or omitted (false).
             /// </summary>
             /// </summary>
-            public bool FormatDefaultValues { get { return formatDefaultValues; } }
+            public bool FormatDefaultValues { get; }
+
+            /// <summary>
+            /// The type registry used to format <see cref="Any"/> messages.
+            /// </summary>
+            public TypeRegistry TypeRegistry { get; }
+
+            // TODO: Work out how we're going to scale this to multiple settings. "WithXyz" methods?
+
+            /// <summary>
+            /// Creates a new <see cref="Settings"/> object with the specified formatting of default values
+            /// and an empty type registry.
+            /// </summary>
+            /// <param name="formatDefaultValues"><c>true</c> if default values (0, empty strings etc) should be formatted; <c>false</c> otherwise.</param>
+            public Settings(bool formatDefaultValues) : this(formatDefaultValues, TypeRegistry.Empty)
+            {
+            }
 
 
             /// <summary>
             /// <summary>
-            /// Creates a new <see cref="Settings"/> object with the specified formatting of default values.
+            /// Creates a new <see cref="Settings"/> object with the specified formatting of default values
+            /// and type registry.
             /// </summary>
             /// </summary>
             /// <param name="formatDefaultValues"><c>true</c> if default values (0, empty strings etc) should be formatted; <c>false</c> otherwise.</param>
             /// <param name="formatDefaultValues"><c>true</c> if default values (0, empty strings etc) should be formatted; <c>false</c> otherwise.</param>
-            public Settings(bool formatDefaultValues)
+            /// <param name="typeRegistry">The <see cref="TypeRegistry"/> to use when formatting <see cref="Any"/> messages.</param>
+            public Settings(bool formatDefaultValues, TypeRegistry typeRegistry)
             {
             {
-                this.formatDefaultValues = formatDefaultValues;
+                FormatDefaultValues = formatDefaultValues;
+                TypeRegistry = Preconditions.CheckNotNull(typeRegistry, nameof(typeRegistry));
             }
             }
         }
         }
     }
     }