diff options
Diffstat (limited to 'csharp/src/Google.Protobuf')
-rw-r--r-- | csharp/src/Google.Protobuf/JsonFormatter.cs | 111 |
1 files changed, 93 insertions, 18 deletions
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 /// </remarks> 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); /// <summary> @@ -130,7 +136,7 @@ namespace Google.Protobuf /// <returns>The formatted message.</returns> 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 /// </summary> 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<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> /// 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 /// <summary> /// Default settings, as used by <see cref="JsonFormatter.Default"/> /// </summary> - public static Settings Default { get { return defaultInstance; } } - - private readonly bool formatDefaultValues; + public static Settings Default { get; } = new Settings(false); /// <summary> /// Whether fields whose values are the default for the field type (e.g. 0 for integers) /// should be formatted (true) or omitted (false). /// </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> - /// 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> /// <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)); } } } |