|
@@ -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));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|