diff options
Diffstat (limited to 'csharp/src/Google.Protobuf/JsonFormatter.cs')
-rw-r--r-- | csharp/src/Google.Protobuf/JsonFormatter.cs | 310 |
1 files changed, 198 insertions, 112 deletions
diff --git a/csharp/src/Google.Protobuf/JsonFormatter.cs b/csharp/src/Google.Protobuf/JsonFormatter.cs index 08e3142e..60f61fc8 100644 --- a/csharp/src/Google.Protobuf/JsonFormatter.cs +++ b/csharp/src/Google.Protobuf/JsonFormatter.cs @@ -37,6 +37,7 @@ using System.Text; using Google.Protobuf.Reflection; using Google.Protobuf.WellKnownTypes; using System.Linq; +using System.Collections.Generic; namespace Google.Protobuf { @@ -55,12 +56,20 @@ namespace Google.Protobuf /// </remarks> public sealed class JsonFormatter { - private static JsonFormatter defaultInstance = new JsonFormatter(Settings.Default); + internal const string AnyTypeUrlField = "@type"; + internal const string AnyDiagnosticValueField = "@value"; + internal const string AnyWellKnownTypeValueField = "value"; + private const string TypeUrlPrefix = "type.googleapis.com"; + private const string NameValueSeparator = ": "; + private const string PropertySeparator = ", "; /// <summary> /// Returns a formatter using the default settings. /// </summary> - public static JsonFormatter Default { get { return defaultInstance; } } + public static JsonFormatter Default { get; } = new JsonFormatter(Settings.Default); + + // A JSON formatter which *only* exists + private static readonly JsonFormatter diagnosticFormatter = new JsonFormatter(Settings.Default); /// <summary> /// The JSON representation of the first 160 characters of Unicode. @@ -114,6 +123,8 @@ namespace Google.Protobuf private readonly Settings settings; + private bool DiagnosticOnly => ReferenceEquals(this, diagnosticFormatter); + /// <summary> /// Creates a new formatted with the given settings. /// </summary> @@ -130,11 +141,11 @@ namespace Google.Protobuf /// <returns>The formatted message.</returns> public string Format(IMessage message) { - Preconditions.CheckNotNull(message, "message"); + ProtoPreconditions.CheckNotNull(message, nameof(message)); StringBuilder builder = new StringBuilder(); if (message.Descriptor.IsWellKnownType) { - WriteWellKnownTypeValue(builder, message.Descriptor, message, false); + WriteWellKnownTypeValue(builder, message.Descriptor, message); } else { @@ -143,6 +154,29 @@ namespace Google.Protobuf return builder.ToString(); } + /// <summary> + /// Converts a message to JSON for diagnostic purposes with no extra context. + /// </summary> + /// <remarks> + /// <para> + /// This differs from calling <see cref="Format(IMessage)"/> on the default JSON + /// formatter in its handling of <see cref="Any"/>. As no type registry is available + /// in <see cref="object.ToString"/> calls, the normal way of resolving the type of + /// an <c>Any</c> message cannot be applied. Instead, a JSON property named <c>@value</c> + /// is included with the base64 data from the <see cref="Any.Value"/> property of the message. + /// </para> + /// <para>The value returned by this method is only designed to be used for diagnostic + /// purposes. It may not be parsable by <see cref="JsonParser"/>, and may not be parsable + /// by other Protocol Buffer implementations.</para> + /// </remarks> + /// <param name="message">The message to format for diagnostic purposes.</param> + /// <returns>The diagnostic-only JSON representation of the message</returns> + public static string ToDiagnosticString(IMessage message) + { + ProtoPreconditions.CheckNotNull(message, nameof(message)); + return diagnosticFormatter.Format(message); + } + private void WriteMessage(StringBuilder builder, IMessage message) { if (message == null) @@ -150,14 +184,28 @@ namespace Google.Protobuf WriteNull(builder); return; } + if (DiagnosticOnly) + { + ICustomDiagnosticMessage customDiagnosticMessage = message as ICustomDiagnosticMessage; + if (customDiagnosticMessage != null) + { + builder.Append(customDiagnosticMessage.ToDiagnosticString()); + 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; @@ -169,23 +217,43 @@ namespace Google.Protobuf { continue; } - // Omit awkward (single) values such as unknown enum values - if (!field.IsRepeated && !field.IsMap && !CanWriteSingleValue(value)) - { - continue; - } // 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; + } + + /// <summary> + /// Camel-case converter with added strictness for field mask formatting. + /// </summary> + /// <exception cref="InvalidOperationException">The field mask is invalid for JSON representation</exception> + private static string ToCamelCaseForFieldMask(string input) + { + for (int i = 0; i < input.Length; i++) + { + char c = input[i]; + if (c >= 'A' && c <= 'Z') + { + throw new InvalidOperationException($"Invalid field mask to be converted to JSON: {input}"); + } + if (c == '_' && i < input.Length - 1) + { + char next = input[i + 1]; + if (next < 'a' || next > 'z') + { + throw new InvalidOperationException($"Invalid field mask to be converted to JSON: {input}"); + } + } + } + return ToCamelCase(input); } // Converted from src/google/protobuf/util/internal/utility.cc ToCamelCase @@ -336,7 +404,14 @@ namespace Google.Protobuf } else if (value is System.Enum) { - WriteString(builder, value.ToString()); + if (System.Enum.IsDefined(value.GetType(), value)) + { + WriteString(builder, value.ToString()); + } + else + { + WriteValue(builder, (int) value); + } } else if (value is float || value is double) { @@ -357,7 +432,7 @@ namespace Google.Protobuf IMessage message = (IMessage) value; if (message.Descriptor.IsWellKnownType) { - WriteWellKnownTypeValue(builder, message.Descriptor, value, true); + WriteWellKnownTypeValue(builder, message.Descriptor, value); } else { @@ -376,8 +451,10 @@ namespace Google.Protobuf /// values are using the embedded well-known types, in order to allow for dynamic messages /// in the future. /// </summary> - private void WriteWellKnownTypeValue(StringBuilder builder, MessageDescriptor descriptor, object value, bool inField) + private void WriteWellKnownTypeValue(StringBuilder builder, MessageDescriptor descriptor, object value) { + // 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); @@ -388,8 +465,7 @@ namespace Google.Protobuf // If it's the message form, we can extract the value first, which *will* be the (possibly boxed) native value, // and then proceed, writing it as if we were definitely in a field. (We never need to wrap it in an extra string... // WriteValue will do the right thing.) - // TODO: Detect this differently when we have dynamic messages. - if (descriptor.File == Int32Value.Descriptor.File) + if (descriptor.IsWrapperType) { if (value is IMessage) { @@ -401,17 +477,17 @@ namespace Google.Protobuf } if (descriptor.FullName == Timestamp.Descriptor.FullName) { - MaybeWrapInString(builder, value, WriteTimestamp, inField); + WriteTimestamp(builder, (IMessage) value); return; } if (descriptor.FullName == Duration.Descriptor.FullName) { - MaybeWrapInString(builder, value, WriteDuration, inField); + WriteDuration(builder, (IMessage) value); return; } if (descriptor.FullName == FieldMask.Descriptor.FullName) { - MaybeWrapInString(builder, value, WriteFieldMask, inField); + WriteFieldMask(builder, (IMessage) value); return; } if (descriptor.FullName == Struct.Descriptor.FullName) @@ -430,25 +506,12 @@ namespace Google.Protobuf WriteStructFieldValue(builder, (IMessage) value); return; } - WriteMessage(builder, (IMessage) value); - } - - /// <summary> - /// Some well-known types end up as string values... so they need wrapping in quotes, but only - /// when they're being used as fields within another message. - /// </summary> - private void MaybeWrapInString(StringBuilder builder, object value, Action<StringBuilder, IMessage> action, bool inField) - { - if (inField) + if (descriptor.FullName == Any.Descriptor.FullName) { - builder.Append('"'); - action(builder, (IMessage) value); - builder.Append('"'); - } - else - { - action(builder, (IMessage) value); + WriteAny(builder, (IMessage) value); + return; } + WriteMessage(builder, (IMessage) value); } private void WriteTimestamp(StringBuilder builder, IMessage value) @@ -459,15 +522,7 @@ namespace Google.Protobuf // it still works in that case. int nanos = (int) value.Descriptor.Fields[Timestamp.NanosFieldNumber].Accessor.GetValue(value); long seconds = (long) value.Descriptor.Fields[Timestamp.SecondsFieldNumber].Accessor.GetValue(value); - - // Even if the original message isn't using the built-in classes, we can still build one... and then - // rely on it being normalized. - Timestamp normalized = Timestamp.Normalize(seconds, nanos); - // Use .NET's formatting for the value down to the second, including an opening double quote (as it's a string value) - DateTime dateTime = normalized.ToDateTime(); - builder.Append(dateTime.ToString("yyyy'-'MM'-'dd'T'HH:mm:ss", CultureInfo.InvariantCulture)); - AppendNanoseconds(builder, Math.Abs(normalized.Nanos)); - builder.Append('Z'); + builder.Append(Timestamp.ToJson(seconds, nanos, DiagnosticOnly)); } private void WriteDuration(StringBuilder builder, IMessage value) @@ -475,51 +530,76 @@ namespace Google.Protobuf // TODO: Same as for WriteTimestamp int nanos = (int) value.Descriptor.Fields[Duration.NanosFieldNumber].Accessor.GetValue(value); long seconds = (long) value.Descriptor.Fields[Duration.SecondsFieldNumber].Accessor.GetValue(value); + builder.Append(Duration.ToJson(seconds, nanos, DiagnosticOnly)); + } - // Even if the original message isn't using the built-in classes, we can still build one... and then - // rely on it being normalized. - Duration normalized = Duration.Normalize(seconds, nanos); + private void WriteFieldMask(StringBuilder builder, IMessage value) + { + var paths = (IList<string>) value.Descriptor.Fields[FieldMask.PathsFieldNumber].Accessor.GetValue(value); + builder.Append(FieldMask.ToJson(paths, DiagnosticOnly)); + } + + private void WriteAny(StringBuilder builder, IMessage value) + { + if (DiagnosticOnly) + { + WriteDiagnosticOnlyAny(builder, value); + return; + } - // The seconds part will normally provide the minus sign if we need it, but not if it's 0... - if (normalized.Seconds == 0 && normalized.Nanos < 0) + 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) { - builder.Append('-'); + 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); - builder.Append(normalized.Seconds.ToString("d", CultureInfo.InvariantCulture)); - AppendNanoseconds(builder, Math.Abs(normalized.Nanos)); - builder.Append('s'); + if (descriptor.IsWellKnownType) + { + builder.Append(PropertySeparator); + WriteString(builder, AnyWellKnownTypeValueField); + builder.Append(NameValueSeparator); + WriteWellKnownTypeValue(builder, descriptor, message); + } + else + { + WriteMessageFields(builder, message, true); + } + builder.Append(" }"); } - private void WriteFieldMask(StringBuilder builder, IMessage value) + private void WriteDiagnosticOnlyAny(StringBuilder builder, IMessage value) { - IList paths = (IList) value.Descriptor.Fields[FieldMask.PathsFieldNumber].Accessor.GetValue(value); - AppendEscapedString(builder, string.Join(",", paths.Cast<string>().Select(ToCamelCase))); + string typeUrl = (string) value.Descriptor.Fields[Any.TypeUrlFieldNumber].Accessor.GetValue(value); + ByteString data = (ByteString) value.Descriptor.Fields[Any.ValueFieldNumber].Accessor.GetValue(value); + builder.Append("{ "); + WriteString(builder, AnyTypeUrlField); + builder.Append(NameValueSeparator); + WriteString(builder, typeUrl); + builder.Append(PropertySeparator); + WriteString(builder, AnyDiagnosticValueField); + builder.Append(NameValueSeparator); + builder.Append('"'); + builder.Append(data.ToBase64()); + builder.Append('"'); + builder.Append(" }"); } - /// <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. - /// </summary> - private static void AppendNanoseconds(StringBuilder builder, int nanos) + internal static string GetTypeName(String typeUrl) { - if (nanos != 0) + string[] parts = typeUrl.Split('/'); + if (parts.Length != 2 || parts[0] != TypeUrlPrefix) { - builder.Append('.'); - // Output to 3, 6 or 9 digits. - if (nanos % 1000000 == 0) - { - builder.Append((nanos / 1000000).ToString("d", CultureInfo.InvariantCulture)); - } - else if (nanos % 1000 == 0) - { - builder.Append((nanos / 1000).ToString("d", CultureInfo.InvariantCulture)); - } - else - { - builder.Append(nanos.ToString("d", CultureInfo.InvariantCulture)); - } + throw new InvalidProtocolBufferException($"Invalid type url: {typeUrl}"); } + return parts[1]; } private void WriteStruct(StringBuilder builder, IMessage message) @@ -538,10 +618,10 @@ namespace Google.Protobuf if (!first) { - builder.Append(", "); + builder.Append(PropertySeparator); } WriteString(builder, key); - builder.Append(": "); + builder.Append(NameValueSeparator); WriteStructFieldValue(builder, value); first = false; } @@ -569,7 +649,7 @@ namespace Google.Protobuf case Value.ListValueFieldNumber: // Structs and ListValues are nested messages, and already well-known types. var nestedMessage = (IMessage) specifiedField.Accessor.GetValue(message); - WriteWellKnownTypeValue(builder, nestedMessage.Descriptor, nestedMessage, true); + WriteWellKnownTypeValue(builder, nestedMessage.Descriptor, nestedMessage); return; case Value.NullValueFieldNumber: WriteNull(builder); @@ -585,13 +665,9 @@ namespace Google.Protobuf bool first = true; foreach (var value in list) { - if (!CanWriteSingleValue(value)) - { - continue; - } if (!first) { - builder.Append(", "); + builder.Append(PropertySeparator); } WriteValue(builder, value); first = false; @@ -606,13 +682,9 @@ namespace Google.Protobuf // This will box each pair. Could use IDictionaryEnumerator, but that's ugly in terms of disposal. foreach (DictionaryEntry pair in dictionary) { - if (!CanWriteSingleValue(pair.Value)) - { - continue; - } if (!first) { - builder.Append(", "); + builder.Append(PropertySeparator); } string keyText; if (pair.Key is string) @@ -636,7 +708,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; } @@ -663,18 +735,9 @@ namespace Google.Protobuf /// <remarks> /// Other than surrogate pair handling, this code is mostly taken from src/google/protobuf/util/internal/json_escaping.cc. /// </remarks> - private void WriteString(StringBuilder builder, string text) + internal static void WriteString(StringBuilder builder, string text) { builder.Append('"'); - AppendEscapedString(builder, text); - builder.Append('"'); - } - - /// <summary> - /// Appends the given text to the string builder, escaping as required. - /// </summary> - private void AppendEscapedString(StringBuilder builder, string text) - { for (int i = 0; i < text.Length; i++) { char c = text[i]; @@ -734,6 +797,7 @@ namespace Google.Protobuf break; } } + builder.Append('"'); } private const string Hex = "0123456789abcdef"; @@ -751,28 +815,50 @@ namespace Google.Protobuf /// </summary> public sealed class Settings { - private static readonly Settings defaultInstance = new Settings(false); - /// <summary> /// Default settings, as used by <see cref="JsonFormatter.Default"/> /// </summary> - public static Settings Default { get { return defaultInstance; } } + public static Settings Default { get; } - private readonly bool formatDefaultValues; + // Workaround for the Mono compiler complaining about XML comments not being on + // valid language elements. + static Settings() + { + Default = 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 = ProtoPreconditions.CheckNotNull(typeRegistry, nameof(typeRegistry)); } } } |