aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar Jon Skeet <jonskeet@google.com>2015-07-31 13:22:15 +0100
committerGravatar Jon Skeet <jonskeet@google.com>2015-08-03 09:26:04 +0100
commit16e272e0c4eebb1d01a2e71ef3c555ff57a22b07 (patch)
tree5b624cf24ba05ac067dc4bfa6f10b8fe4dd967c5
parent80f89b4ecfad8783c1453f1bf36ecda1c2c9c664 (diff)
Format JSON for Duration and Timestamp.
This is taking an approach of putting all the logic in JsonFormatter. That's helpful in terms of concealing the details of whether or not to wrap the value in quotes, but it does lack flexibility. I don't *think* we want to allow user-defined formatting of messages, so that much shouldn't be a problem.
-rw-r--r--csharp/src/Google.Protobuf.Test/JsonFormatterTest.cs61
-rw-r--r--csharp/src/Google.Protobuf/JsonFormatter.cs130
-rw-r--r--csharp/src/Google.Protobuf/WellKnownTypes/TimestampPartial.cs2
3 files changed, 182 insertions, 11 deletions
diff --git a/csharp/src/Google.Protobuf.Test/JsonFormatterTest.cs b/csharp/src/Google.Protobuf.Test/JsonFormatterTest.cs
index ac33a750..82094ea3 100644
--- a/csharp/src/Google.Protobuf.Test/JsonFormatterTest.cs
+++ b/csharp/src/Google.Protobuf.Test/JsonFormatterTest.cs
@@ -34,6 +34,7 @@ using System;
using Google.Protobuf.TestProtos;
using NUnit.Framework;
using UnitTest.Issues.TestProtos;
+using Google.Protobuf.WellKnownTypes;
namespace Google.Protobuf
{
@@ -310,6 +311,66 @@ namespace Google.Protobuf
AssertJson("{ 'plainString': 'plain', 'o1String': '', 'plainInt32': 10, 'o2Int32': 0 }", formatter.Format(message));
}
+ [Test]
+ public void TimestampStandalone()
+ {
+ Assert.AreEqual("1970-01-01T00:00:00Z", new Timestamp().ToString());
+ Assert.AreEqual("1970-01-01T00:00:00.100Z", new Timestamp { Nanos = 100000000 }.ToString());
+ Assert.AreEqual("1970-01-01T00:00:00.120Z", new Timestamp { Nanos = 120000000 }.ToString());
+ Assert.AreEqual("1970-01-01T00:00:00.123Z", new Timestamp { Nanos = 123000000 }.ToString());
+ Assert.AreEqual("1970-01-01T00:00:00.123400Z", new Timestamp { Nanos = 123400000 }.ToString());
+ Assert.AreEqual("1970-01-01T00:00:00.123450Z", new Timestamp { Nanos = 123450000 }.ToString());
+ Assert.AreEqual("1970-01-01T00:00:00.123456Z", new Timestamp { Nanos = 123456000 }.ToString());
+ Assert.AreEqual("1970-01-01T00:00:00.123456700Z", new Timestamp { Nanos = 123456700 }.ToString());
+ Assert.AreEqual("1970-01-01T00:00:00.123456780Z", new Timestamp { Nanos = 123456780 }.ToString());
+ Assert.AreEqual("1970-01-01T00:00:00.123456789Z", new Timestamp { Nanos = 123456789 }.ToString());
+
+ // One before and one after the Unix epoch
+ Assert.AreEqual("1673-06-19T12:34:56Z",
+ new DateTime(1673, 6, 19, 12, 34, 56, DateTimeKind.Utc).ToTimestamp().ToString());
+ Assert.AreEqual("2015-07-31T10:29:34Z",
+ new DateTime(2015, 7, 31, 10, 29, 34, DateTimeKind.Utc).ToTimestamp().ToString());
+ }
+
+ [Test]
+ public void TimestampField()
+ {
+ var message = new TestWellKnownTypes { TimestampField = new Timestamp() };
+ AssertJson("{ 'timestampField': '1970-01-01T00:00:00Z' }", JsonFormatter.Default.Format(message));
+ }
+
+ [Test]
+ [TestCase(0, 0, "0s")]
+ [TestCase(1, 0, "1s")]
+ [TestCase(-1, 0, "-1s")]
+ [TestCase(0, 100000000, "0.100s")]
+ [TestCase(0, 120000000, "0.120s")]
+ [TestCase(0, 123000000, "0.123s")]
+ [TestCase(0, 123400000, "0.123400s")]
+ [TestCase(0, 123450000, "0.123450s")]
+ [TestCase(0, 123456000, "0.123456s")]
+ [TestCase(0, 123456700, "0.123456700s")]
+ [TestCase(0, 123456780, "0.123456780s")]
+ [TestCase(0, 123456789, "0.123456789s")]
+ [TestCase(0, -100000000, "-0.100s")]
+ [TestCase(1, 100000000, "1.100s")]
+ [TestCase(-1, -100000000, "-1.100s")]
+ // Non-normalized examples
+ [TestCase(1, 2123456789, "3.123456789s")]
+ [TestCase(1, -100000000, "0.900s")]
+ public void DurationStandalone(long seconds, int nanoseconds, string expected)
+ {
+ Assert.AreEqual(expected, new Duration { Seconds = seconds, Nanos = nanoseconds }.ToString());
+ }
+
+ [Test]
+ public void DurationField()
+ {
+ var message = new TestWellKnownTypes { DurationField = new Duration() };
+ AssertJson("{ 'durationField': '0s' }", JsonFormatter.Default.Format(message));
+
+ }
+
/// <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
diff --git a/csharp/src/Google.Protobuf/JsonFormatter.cs b/csharp/src/Google.Protobuf/JsonFormatter.cs
index d9783fc4..29fc80fa 100644
--- a/csharp/src/Google.Protobuf/JsonFormatter.cs
+++ b/csharp/src/Google.Protobuf/JsonFormatter.cs
@@ -122,10 +122,14 @@ namespace Google.Protobuf
{
Preconditions.CheckNotNull(message, "message");
StringBuilder builder = new StringBuilder();
- // TODO(jonskeet): Handle well-known types here.
- // Our reflection support needs improving so that we can get at the descriptor
- // to find out whether *this* message is a well-known type.
- WriteMessage(builder, message);
+ if (message.Descriptor.IsWellKnownType)
+ {
+ WriteWellKnownTypeValue(builder, message.Descriptor, message, false);
+ }
+ else
+ {
+ WriteMessage(builder, message);
+ }
return builder.ToString();
}
@@ -356,7 +360,7 @@ namespace Google.Protobuf
case FieldType.Group: // Never expect to get this, but...
if (descriptor.MessageType.IsWellKnownType)
{
- WriteWellKnownTypeValue(builder, descriptor, value);
+ WriteWellKnownTypeValue(builder, descriptor.MessageType, value, true);
}
else
{
@@ -370,20 +374,126 @@ namespace Google.Protobuf
/// <summary>
/// Central interception point for well-known type formatting. Any well-known types which
- /// don't need special handling can fall back to WriteMessage.
+ /// don't need special handling can fall back to WriteMessage. We avoid assuming that the
+ /// values are using the embedded well-known types, in order to allow for dynamic messages
+ /// in the future.
/// </summary>
- private void WriteWellKnownTypeValue(StringBuilder builder, FieldDescriptor descriptor, object value)
+ private void WriteWellKnownTypeValue(StringBuilder builder, MessageDescriptor descriptor, object value, bool inField)
{
// For wrapper types, the value will be the (possibly boxed) "native" value,
// so we can write it as if we were unconditionally writing the Value field for the wrapper type.
- if (descriptor.MessageType.File == Int32Value.Descriptor.File && value != null)
+ if (descriptor.File == Int32Value.Descriptor.File && value != null)
{
- WriteSingleValue(builder, descriptor.MessageType.FindFieldByNumber(1), value);
+ WriteSingleValue(builder, descriptor.FindFieldByNumber(1), value);
+ return;
+ }
+ if (descriptor.FullName == Timestamp.Descriptor.FullName && value != null)
+ {
+ MaybeWrapInString(builder, value, WriteTimestamp, inField);
+ return;
+ }
+ if (descriptor.FullName == Duration.Descriptor.FullName && value != null)
+ {
+ MaybeWrapInString(builder, value, WriteDuration, inField);
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)
+ {
+ builder.Append('"');
+ action(builder, (IMessage) value);
+ builder.Append('"');
+ }
+ else
+ {
+ action(builder, (IMessage) value);
+ }
+ }
+
+ private void WriteTimestamp(StringBuilder builder, IMessage value)
+ {
+ // TODO: In the common case where this *is* using the built-in Timestamp type, we could
+ // avoid all the reflection at this point, by casting to Timestamp. In the interests of
+ // avoiding subtle bugs, don't do that until we've implemented DynamicMessage so that we can prove
+ // 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));
+ if (normalized.Nanos != 0)
+ {
+ builder.Append('.');
+ // Output to 3, 6 or 9 digits.
+ if (normalized.Nanos % 1000000 == 0)
+ {
+ builder.Append((normalized.Nanos / 1000000).ToString("d", CultureInfo.InvariantCulture));
+ }
+ else if (normalized.Nanos % 1000 == 0)
+ {
+ builder.Append((normalized.Nanos / 1000).ToString("d", CultureInfo.InvariantCulture));
+ }
+ else
+ {
+ builder.Append((normalized.Nanos).ToString("d", CultureInfo.InvariantCulture));
+ }
+ }
+ builder.Append('Z');
+ }
+
+ private void WriteDuration(StringBuilder builder, IMessage value)
+ {
+ // TODO: In the common case where this *is* using the built-in Timestamp type, we could
+ // avoid all the reflection at this point, by casting to Timestamp. In the interests of
+ // avoiding subtle bugs, don't do that until we've implemented DynamicMessage so that we can prove
+ // it still works in that case.
+ int nanos = (int) value.Descriptor.Fields[Duration.NanosFieldNumber].Accessor.GetValue(value);
+ long seconds = (long) value.Descriptor.Fields[Duration.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.
+ Duration normalized = Duration.Normalize(seconds, nanos);
+
+ // 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)
+ {
+ builder.Append('-');
+ }
+
+ builder.Append(normalized.Seconds.ToString("d", CultureInfo.InvariantCulture));
+ nanos = Math.Abs(normalized.Nanos);
+ if (nanos != 0)
+ {
+ builder.Append('.');
+ // Output to 3, 6 or 9 digits.
+ if (nanos % 1000000 == 0)
+ {
+ builder.Append((nanos / 1000000).ToString("d", CultureInfo.InvariantCulture));
+ }
+ else if (normalized.Nanos % 1000 == 0)
+ {
+ builder.Append((nanos / 1000).ToString("d", CultureInfo.InvariantCulture));
+ }
+ else
+ {
+ builder.Append(nanos.ToString("d", CultureInfo.InvariantCulture));
+ }
+ }
+ builder.Append('s');
+ }
+
private void WriteList(StringBuilder builder, IFieldAccessor accessor, IList list)
{
builder.Append("[ ");
@@ -537,7 +647,7 @@ namespace Google.Protobuf
}
}
builder.Append('"');
- }
+ }
private const string Hex = "0123456789abcdef";
private static void HexEncodeUtf16CodeUnit(StringBuilder builder, char c)
diff --git a/csharp/src/Google.Protobuf/WellKnownTypes/TimestampPartial.cs b/csharp/src/Google.Protobuf/WellKnownTypes/TimestampPartial.cs
index f682d093..1aa392c7 100644
--- a/csharp/src/Google.Protobuf/WellKnownTypes/TimestampPartial.cs
+++ b/csharp/src/Google.Protobuf/WellKnownTypes/TimestampPartial.cs
@@ -147,7 +147,7 @@ namespace Google.Protobuf.WellKnownTypes
return FromDateTime(dateTimeOffset.UtcDateTime);
}
- private static Timestamp Normalize(long seconds, int nanoseconds)
+ internal static Timestamp Normalize(long seconds, int nanoseconds)
{
int extraSeconds = nanoseconds / Duration.NanosecondsPerSecond;
seconds += extraSeconds;