Commit aabc6c41 authored by Jon Skeet's avatar Jon Skeet

Make ToString() valid without a type registry

This addresses issue #1008, by creating a JsonFormatter which is private and only different
to JsonFormatter.Default in terms of reference equality.

Other plausible designs:

- The same, but expose the diagnostic-only formatter
- Add something to settings to say "I don't have a type registry at all"
- Change the behaviour of JsonFormatter.Default (bad idea IMO, as we really *don't* want the result of this used as regular JSON to be parsed)

Note that just trying to find a separate fix to issue #933 and using that to override Any.ToString() differently wouldn't work for messages that *contain* an Any.

Generated code changes follow in the next commit.
parent dc633398
......@@ -62,5 +62,28 @@ namespace Google.Protobuf.WellKnownTypes
var unpacked = any.Unpack<TestAllTypes>();
Assert.AreEqual(message, unpacked);
}
[Test]
public void ToString_WithValues()
{
var message = SampleMessages.CreateFullTestAllTypes();
var any = Any.Pack(message);
var text = any.ToString();
Assert.That(text, Is.StringContaining("\"@value\": \"" + message.ToByteString().ToBase64() + "\""));
}
[Test]
public void ToString_Empty()
{
var any = new Any();
Assert.AreEqual("{ \"@type\": \"\", \"@value\": \"\" }", any.ToString());
}
[Test]
public void ToString_MessageContainingAny()
{
var message = new TestWellKnownTypes { AnyField = new Any() };
Assert.AreEqual("{ \"anyField\": { \"@type\": \"\", \"@value\": \"\" } }", message.ToString());
}
}
}
......@@ -56,17 +56,19 @@ namespace Google.Protobuf
public sealed class JsonFormatter
{
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 = ", ";
private static JsonFormatter defaultInstance = new JsonFormatter(Settings.Default);
/// <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.
......@@ -149,6 +151,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)
{
Preconditions.CheckNotNull(message, nameof(message));
return diagnosticFormatter.Format(message);
}
private void WriteMessage(StringBuilder builder, IMessage message)
{
if (message == null)
......@@ -516,6 +541,12 @@ namespace Google.Protobuf
private void WriteAny(StringBuilder builder, IMessage value)
{
if (ReferenceEquals(this, diagnosticFormatter))
{
WriteDiagnosticOnlyAny(builder, value);
return;
}
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);
......@@ -544,6 +575,23 @@ namespace Google.Protobuf
builder.Append(" }");
}
private void WriteDiagnosticOnlyAny(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);
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(" }");
}
internal static string GetTypeName(String typeUrl)
{
string[] parts = typeUrl.Split('/');
......
......@@ -353,7 +353,7 @@ void MessageGenerator::GenerateFrameworkMethods(io::Printer* printer) {
printer->Print(
"public override string ToString() {\n"
" return pb::JsonFormatter.Default.Format(this);\n"
" return pb::JsonFormatter.ToDiagnosticString(this);\n"
"}\n\n");
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment