Commit e4af879b authored by Jon Skeet's avatar Jon Skeet

Merge pull request #1000 from jskeet/any-format

JSON handling for Any
parents bdabaeb0 3de2fced
......@@ -35,6 +35,7 @@ using Google.Protobuf.TestProtos;
using NUnit.Framework;
using UnitTest.Issues.TestProtos;
using Google.Protobuf.WellKnownTypes;
using Google.Protobuf.Reflection;
namespace Google.Protobuf
{
......@@ -420,6 +421,47 @@ namespace Google.Protobuf
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>
/// 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
......
......@@ -30,6 +30,7 @@
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#endregion
using Google.Protobuf.Reflection;
using Google.Protobuf.TestProtos;
using Google.Protobuf.WellKnownTypes;
using NUnit.Framework;
......@@ -717,6 +718,55 @@ namespace Google.Protobuf
CollectionAssert.AreEqual(expectedPaths, parsed.Paths);
}
[Test]
public void Any_RegularMessage()
{
var registry = TypeRegistry.FromMessages(TestAllTypes.Descriptor);
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 original = Any.Pack(message);
var json = formatter.Format(original); // This is tested in JsonFormatterTest
var parser = new JsonParser(new JsonParser.Settings(10, registry));
Assert.AreEqual(original, parser.Parse<Any>(json));
string valueFirstJson = "{ \"singleInt32\": 10, \"singleNestedMessage\": { \"bb\": 20 }, \"@type\": \"type.googleapis.com/protobuf_unittest.TestAllTypes\" }";
Assert.AreEqual(original, parser.Parse<Any>(valueFirstJson));
}
[Test]
public void Any_UnknownType()
{
string json = "{ \"@type\": \"type.googleapis.com/bogus\" }";
Assert.Throws<InvalidOperationException>(() => Any.Parser.ParseJson(json));
}
[Test]
public void Any_WellKnownType()
{
var registry = TypeRegistry.FromMessages(Timestamp.Descriptor);
var formatter = new JsonFormatter(new JsonFormatter.Settings(false, registry));
var timestamp = new DateTime(1673, 6, 19, 12, 34, 56, DateTimeKind.Utc).ToTimestamp();
var original = Any.Pack(timestamp);
var json = formatter.Format(original); // This is tested in JsonFormatterTest
var parser = new JsonParser(new JsonParser.Settings(10, registry));
Assert.AreEqual(original, parser.Parse<Any>(json));
string valueFirstJson = "{ \"value\": \"1673-06-19T12:34:56Z\", \"@type\": \"type.googleapis.com/google.protobuf.Timestamp\" }";
Assert.AreEqual(original, parser.Parse<Any>(valueFirstJson));
}
[Test]
public void Any_Nested()
{
var registry = TypeRegistry.FromMessages(TestWellKnownTypes.Descriptor, TestAllTypes.Descriptor);
var formatter = new JsonFormatter(new JsonFormatter.Settings(false, registry));
var parser = new JsonParser(new JsonParser.Settings(10, registry));
var doubleNestedMessage = new TestAllTypes { SingleInt32 = 20 };
var nestedMessage = Any.Pack(doubleNestedMessage);
var message = new TestWellKnownTypes { AnyField = Any.Pack(nestedMessage) };
var json = formatter.Format(message);
// Use the descriptor-based parser just for a change.
Assert.AreEqual(message, parser.Parse(json, TestWellKnownTypes.Descriptor));
}
[Test]
public void DataAfterObject()
{
......
......@@ -85,7 +85,7 @@ namespace Google.Protobuf
public void ObjectDepth()
{
string json = "{ \"foo\": { \"x\": 1, \"y\": [ 0 ] } }";
var tokenizer = new JsonTokenizer(new StringReader(json));
var tokenizer = JsonTokenizer.FromTextReader(new StringReader(json));
// If we had more tests like this, I'd introduce a helper method... but for one test, it's not worth it.
Assert.AreEqual(0, tokenizer.ObjectDepth);
Assert.AreEqual(JsonToken.StartObject, tokenizer.Next());
......@@ -118,7 +118,7 @@ namespace Google.Protobuf
public void ObjectDepth_WithPushBack()
{
string json = "{}";
var tokenizer = new JsonTokenizer(new StringReader(json));
var tokenizer = JsonTokenizer.FromTextReader(new StringReader(json));
Assert.AreEqual(0, tokenizer.ObjectDepth);
var token = tokenizer.Next();
Assert.AreEqual(1, tokenizer.ObjectDepth);
......@@ -275,7 +275,7 @@ namespace Google.Protobuf
// Note: we don't test that the earlier tokens are exactly as expected,
// partly because that's hard to parameterize.
var reader = new StringReader(json.Replace('\'', '"'));
var tokenizer = new JsonTokenizer(reader);
var tokenizer = JsonTokenizer.FromTextReader(reader);
for (int i = 0; i < expectedValidTokens; i++)
{
Assert.IsNotNull(tokenizer.Next());
......@@ -334,7 +334,7 @@ namespace Google.Protobuf
[Test]
public void NextAfterEndDocumentThrows()
{
var tokenizer = new JsonTokenizer(new StringReader("null"));
var tokenizer = JsonTokenizer.FromTextReader(new StringReader("null"));
Assert.AreEqual(JsonToken.Null, tokenizer.Next());
Assert.AreEqual(JsonToken.EndDocument, tokenizer.Next());
Assert.Throws<InvalidOperationException>(() => tokenizer.Next());
......@@ -343,7 +343,7 @@ namespace Google.Protobuf
[Test]
public void CanPushBackEndDocument()
{
var tokenizer = new JsonTokenizer(new StringReader("null"));
var tokenizer = JsonTokenizer.FromTextReader(new StringReader("null"));
Assert.AreEqual(JsonToken.Null, tokenizer.Next());
Assert.AreEqual(JsonToken.EndDocument, tokenizer.Next());
tokenizer.PushBack(JsonToken.EndDocument);
......@@ -373,7 +373,7 @@ namespace Google.Protobuf
private static void AssertTokensNoReplacement(string json, params JsonToken[] expectedTokens)
{
var reader = new StringReader(json);
var tokenizer = new JsonTokenizer(reader);
var tokenizer = JsonTokenizer.FromTextReader(reader);
for (int i = 0; i < expectedTokens.Length; i++)
{
var actualToken = tokenizer.Next();
......@@ -393,7 +393,7 @@ namespace Google.Protobuf
private static void AssertThrowsAfter(string json, params JsonToken[] expectedTokens)
{
var reader = new StringReader(json);
var tokenizer = new JsonTokenizer(reader);
var tokenizer = JsonTokenizer.FromTextReader(reader);
for (int i = 0; i < expectedTokens.Length; i++)
{
var actualToken = tokenizer.Next();
......
......@@ -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;
}
......@@ -750,28 +808,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 = Preconditions.CheckNotNull(typeRegistry, nameof(typeRegistry));
}
}
}
......
This diff is collapsed.
This diff is collapsed.
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