Merge pull request #672 from jskeet/json-struct

Formatting of Struct as JSON
diff --git a/csharp/src/Google.Protobuf.Test/JsonFormatterTest.cs b/csharp/src/Google.Protobuf.Test/JsonFormatterTest.cs
index 82094ea..f6e6488 100644
--- a/csharp/src/Google.Protobuf.Test/JsonFormatterTest.cs
+++ b/csharp/src/Google.Protobuf.Test/JsonFormatterTest.cs
@@ -368,7 +368,24 @@
         {
             var message = new TestWellKnownTypes { DurationField = new Duration() };
             AssertJson("{ 'durationField': '0s' }", JsonFormatter.Default.Format(message));
+        }
 
+        [Test]
+        public void StructSample()
+        {
+            var message = new Struct
+            {
+                Fields =
+                {
+                    { "a", new Value { NullValue = new NullValue() } },
+                    { "b", new Value { BoolValue = false } },
+                    { "c", new Value { NumberValue = 10.5 } },
+                    { "d", new Value { StringValue = "text" } },
+                    { "e", new Value { ListValue = new ListValue { Values = { new Value { StringValue = "t1" }, new Value { NumberValue = 5 } } } } },
+                    { "f", new Value { StructValue = new Struct { Fields = { { "nested", new Value { StringValue = "value" } } } } } }
+                }
+            };
+            AssertJson("{ 'a': null, 'b': false, 'c': 10.5, 'd': 'text', 'e': [ 't1', 5 ], 'f': { 'nested': 'value' } }", message.ToString());
         }
 
         /// <summary>
diff --git a/csharp/src/Google.Protobuf/JsonFormatter.cs b/csharp/src/Google.Protobuf/JsonFormatter.cs
index 099fb6a..3b25beb 100644
--- a/csharp/src/Google.Protobuf/JsonFormatter.cs
+++ b/csharp/src/Google.Protobuf/JsonFormatter.cs
@@ -380,23 +380,44 @@
         /// </summary>
         private void WriteWellKnownTypeValue(StringBuilder builder, MessageDescriptor descriptor, object value, bool inField)
         {
+            if (value == null)
+            {
+                WriteNull(builder);
+                return;
+            }
             // 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.File == Int32Value.Descriptor.File && value != null)
+            if (descriptor.File == Int32Value.Descriptor.File)
             {
                 WriteSingleValue(builder, descriptor.FindFieldByNumber(1), value);
                 return;
             }
-            if (descriptor.FullName == Timestamp.Descriptor.FullName && value != null)
+            if (descriptor.FullName == Timestamp.Descriptor.FullName)
             {
                 MaybeWrapInString(builder, value, WriteTimestamp, inField);
                 return;
             }
-            if (descriptor.FullName == Duration.Descriptor.FullName && value != null)
+            if (descriptor.FullName == Duration.Descriptor.FullName)
             {
                 MaybeWrapInString(builder, value, WriteDuration, inField);
                 return;
             }
+            if (descriptor.FullName == Struct.Descriptor.FullName)
+            {
+                WriteStruct(builder, (IMessage) value);
+                return;
+            }
+            if (descriptor.FullName == ListValue.Descriptor.FullName)
+            {
+                var fieldAccessor = descriptor.Fields[ListValue.ValuesFieldNumber].Accessor;
+                WriteList(builder, fieldAccessor, (IList) fieldAccessor.GetValue(value));
+                return;
+            }
+            if (descriptor.FullName == Value.Descriptor.FullName)
+            {
+                WriteStructFieldValue(builder, (IMessage) value);
+                return;
+            }
             WriteMessage(builder, (IMessage) value);
         }
 
@@ -483,6 +504,63 @@
             }
         }
 
+        private void WriteStruct(StringBuilder builder, IMessage message)
+        {
+            builder.Append("{ ");
+            IDictionary fields = (IDictionary) message.Descriptor.Fields[Struct.FieldsFieldNumber].Accessor.GetValue(message);
+            bool first = true;
+            foreach (DictionaryEntry entry in fields)
+            {
+                string key = (string) entry.Key;
+                IMessage value = (IMessage) entry.Value;
+                if (string.IsNullOrEmpty(key) || value == null)
+                {
+                    throw new InvalidOperationException("Struct fields cannot have an empty key or a null value.");
+                }
+
+                if (!first)
+                {
+                    builder.Append(", ");
+                }
+                WriteString(builder, key);
+                builder.Append(": ");
+                WriteStructFieldValue(builder, value);
+                first = false;
+            }
+            builder.Append(first ? "}" : " }");
+        }
+
+        private void WriteStructFieldValue(StringBuilder builder, IMessage message)
+        {
+            var specifiedField = message.Descriptor.Oneofs[0].Accessor.GetCaseFieldDescriptor(message);
+            if (specifiedField == null)
+            {
+                throw new InvalidOperationException("Value message must contain a value for the oneof.");
+            }
+
+            object value = specifiedField.Accessor.GetValue(message);
+            
+            switch (specifiedField.FieldNumber)
+            {
+                case Value.BoolValueFieldNumber:
+                case Value.StringValueFieldNumber:
+                case Value.NumberValueFieldNumber:
+                    WriteSingleValue(builder, specifiedField, value);
+                    return;
+                case Value.StructValueFieldNumber:
+                case Value.ListValueFieldNumber:
+                    // Structs and ListValues are nested messages, and already well-known types.
+                    var nestedMessage = (IMessage) specifiedField.Accessor.GetValue(message);
+                    WriteWellKnownTypeValue(builder, nestedMessage.Descriptor, nestedMessage, true);
+                    return;
+                case Value.NullValueFieldNumber:
+                    WriteNull(builder);
+                    return;
+                default:
+                    throw new InvalidOperationException("Unexpected case in struct field: " + specifiedField.FieldNumber);
+            }
+        }
+
         private void WriteList(StringBuilder builder, IFieldAccessor accessor, IList list)
         {
             builder.Append("[ ");