aboutsummaryrefslogtreecommitdiff
path: root/csharp
diff options
context:
space:
mode:
authorJon Skeet <skeet@pobox.com>2015-08-03 09:31:19 +0100
committerJon Skeet <skeet@pobox.com>2015-08-03 09:31:19 +0100
commit115e6c735ec561d5260c134fa74ee0d5121a441b (patch)
treec75f7b2a51f388ccbf9aeb6561a8292c2d3b05ab /csharp
parent80f89b4ecfad8783c1453f1bf36ecda1c2c9c664 (diff)
parent801b169bcecb45b3dadc22ab9e97d2c6d3d2ab5d (diff)
downloadprotobuf-115e6c735ec561d5260c134fa74ee0d5121a441b.tar.gz
protobuf-115e6c735ec561d5260c134fa74ee0d5121a441b.tar.bz2
protobuf-115e6c735ec561d5260c134fa74ee0d5121a441b.zip
Merge pull request #671 from jskeet/json-time
JSON formatting for Timestamp and Duration
Diffstat (limited to 'csharp')
-rw-r--r--csharp/protos/extest/unittest_issues.proto5
-rw-r--r--csharp/src/Google.Protobuf.Test/JsonFormatterTest.cs61
-rw-r--r--csharp/src/Google.Protobuf/JsonFormatter.cs117
-rw-r--r--csharp/src/Google.Protobuf/WellKnownTypes/TimestampPartial.cs2
4 files changed, 174 insertions, 11 deletions
diff --git a/csharp/protos/extest/unittest_issues.proto b/csharp/protos/extest/unittest_issues.proto
index b66da471..376eee84 100644
--- a/csharp/protos/extest/unittest_issues.proto
+++ b/csharp/protos/extest/unittest_issues.proto
@@ -97,7 +97,10 @@ message TestJsonFieldOrdering {
// ordering.
// TestFieldOrderings in unittest_proto3.proto is similar,
// but doesn't include oneofs.
- // TODO: Consider adding
+ // TODO: Consider adding oneofs to TestFieldOrderings, although
+ // that will require fixing other tests in multiple platforms.
+ // Alternatively, consider just adding this to
+ // unittest_proto3.proto if multiple platforms want it.
int32 plain_int32 = 4;
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..099fb6a1 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,115 @@ 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.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)
{
- WriteSingleValue(builder, descriptor.MessageType.FindFieldByNumber(1), value);
+ 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));
+ AppendNanoseconds(builder, Math.Abs(normalized.Nanos));
+ builder.Append('Z');
+ }
+
+ private void WriteDuration(StringBuilder builder, IMessage value)
+ {
+ // 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);
+
+ // 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));
+ AppendNanoseconds(builder, Math.Abs(normalized.Nanos));
+ builder.Append('s');
+ }
+
+ /// <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)
+ {
+ 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 (nanos % 1000 == 0)
+ {
+ builder.Append((nanos / 1000).ToString("d", CultureInfo.InvariantCulture));
+ }
+ else
+ {
+ builder.Append(nanos.ToString("d", CultureInfo.InvariantCulture));
+ }
+ }
+ }
+
private void WriteList(StringBuilder builder, IFieldAccessor accessor, IList list)
{
builder.Append("[ ");
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;