Commit c34ed5c9 authored by Jon Skeet's avatar Jon Skeet

Merge pull request #846 from jskeet/tostring

Support ToString in RepeatedField and MapField.
parents 2842568f 9ed6d4da
......@@ -562,6 +562,20 @@ namespace Google.Protobuf.Collections
Assert.IsFalse(values.Contains(null));
}
[Test]
public void ToString_StringToString()
{
var map = new MapField<string, string> { { "foo", "bar" }, { "x", "y" } };
Assert.AreEqual("{ \"foo\": \"bar\", \"x\": \"y\" }", map.ToString());
}
[Test]
public void ToString_UnsupportedKeyType()
{
var map = new MapField<byte, string> { { 10, "foo" } };
Assert.Throws<ArgumentException>(() => map.ToString());
}
private static KeyValuePair<TKey, TValue> NewKeyValuePair<TKey, TValue>(TKey key, TValue value)
{
return new KeyValuePair<TKey, TValue>(key, value);
......
......@@ -37,6 +37,7 @@ using System.IO;
using System.Linq;
using System.Text;
using Google.Protobuf.TestProtos;
using Google.Protobuf.WellKnownTypes;
using NUnit.Framework;
namespace Google.Protobuf.Collections
......@@ -599,5 +600,61 @@ namespace Google.Protobuf.Collections
list.Insert(1, "middle");
CollectionAssert.AreEqual(new[] { "first", "middle", "second" }, list);
}
[Test]
public void ToString_Integers()
{
var list = new RepeatedField<int> { 5, 10, 20 };
var text = list.ToString();
Assert.AreEqual("[ 5, 10, 20 ]", text);
}
[Test]
public void ToString_Strings()
{
var list = new RepeatedField<string> { "x", "y", "z" };
var text = list.ToString();
Assert.AreEqual("[ \"x\", \"y\", \"z\" ]", text);
}
[Test]
public void ToString_Messages()
{
var list = new RepeatedField<TestAllTypes> { new TestAllTypes { SingleDouble = 1.5 }, new TestAllTypes { SingleInt32 = 10 } };
var text = list.ToString();
Assert.AreEqual("[ { \"singleDouble\": 1.5 }, { \"singleInt32\": 10 } ]", text);
}
[Test]
public void ToString_Empty()
{
var list = new RepeatedField<TestAllTypes> { };
var text = list.ToString();
Assert.AreEqual("[ ]", text);
}
[Test]
public void ToString_InvalidElementType()
{
var list = new RepeatedField<decimal> { 15m };
Assert.Throws<ArgumentException>(() => list.ToString());
}
[Test]
public void ToString_Timestamp()
{
var list = new RepeatedField<Timestamp> { Timestamp.FromDateTime(new DateTime(2015, 10, 1, 12, 34, 56, DateTimeKind.Utc)) };
var text = list.ToString();
Assert.AreEqual("[ \"2015-10-01T12:34:56Z\" ]", text);
}
[Test]
public void ToString_Struct()
{
var message = new Struct { Fields = { { "foo", new Value { NumberValue = 20 } } } };
var list = new RepeatedField<Struct> { message };
var text = list.ToString();
Assert.AreEqual(text, "[ { \"foo\": 20 } ]", message.ToString());
}
}
}
......@@ -35,6 +35,7 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Google.Protobuf.Compatibility;
namespace Google.Protobuf.Collections
......@@ -45,10 +46,17 @@ namespace Google.Protobuf.Collections
/// <typeparam name="TKey">Key type in the map. Must be a type supported by Protocol Buffer map keys.</typeparam>
/// <typeparam name="TValue">Value type in the map. Must be a type supported by Protocol Buffers.</typeparam>
/// <remarks>
/// <para>
/// This implementation preserves insertion order for simplicity of testing
/// code using maps fields. Overwriting an existing entry does not change the
/// position of that entry within the map. Equality is not order-sensitive.
/// For string keys, the equality comparison is provided by <see cref="StringComparer.Ordinal" />.
/// </para>
/// <para>
/// This implementation does not generally prohibit the use of key/value types which are not
/// supported by Protocol Buffers (e.g. using a key type of <code>byte</code>) but nor does it guarantee
/// that all operations will work in such cases.
/// </para>
/// </remarks>
public sealed class MapField<TKey, TValue> : IDeepCloneable<MapField<TKey, TValue>>, IDictionary<TKey, TValue>, IEquatable<MapField<TKey, TValue>>, IDictionary
{
......@@ -482,6 +490,17 @@ namespace Google.Protobuf.Collections
return size;
}
/// <summary>
/// Returns a string representation of this repeated field, in the same
/// way as it would be represented by the default JSON formatter.
/// </summary>
public override string ToString()
{
var builder = new StringBuilder();
JsonFormatter.Default.WriteDictionary(builder, this);
return builder.ToString();
}
#region IDictionary explicit interface implementation
void IDictionary.Add(object key, object value)
{
......
......@@ -33,6 +33,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using Google.Protobuf.Compatibility;
namespace Google.Protobuf.Collections
......@@ -41,6 +42,10 @@ namespace Google.Protobuf.Collections
/// The contents of a repeated field: essentially, a collection with some extra
/// restrictions (no null values) and capabilities (deep cloning).
/// </summary>
/// <remarks>
/// This implementation does not generally prohibit the use of types which are not
/// supported by Protocol Buffers but nor does it guarantee that all operations will work in such cases.
/// </remarks>
/// <typeparam name="T">The element type of the repeated field.</typeparam>
public sealed class RepeatedField<T> : IList<T>, IList, IDeepCloneable<RepeatedField<T>>, IEquatable<RepeatedField<T>>
{
......@@ -464,6 +469,17 @@ namespace Google.Protobuf.Collections
array[count] = default(T);
}
/// <summary>
/// Returns a string representation of this repeated field, in the same
/// way as it would be represented by the default JSON formatter.
/// </summary>
public override string ToString()
{
var builder = new StringBuilder();
JsonFormatter.Default.WriteList(builder, this);
return builder.ToString();
}
/// <summary>
/// Gets or sets the item at the specified index.
/// </summary>
......
......@@ -170,7 +170,7 @@ namespace Google.Protobuf
continue;
}
// Omit awkward (single) values such as unknown enum values
if (!field.IsRepeated && !field.IsMap && !CanWriteSingleValue(accessor.Descriptor, value))
if (!field.IsRepeated && !field.IsMap && !CanWriteSingleValue(value))
{
continue;
}
......@@ -182,7 +182,7 @@ namespace Google.Protobuf
}
WriteString(builder, ToCamelCase(accessor.Descriptor.Name));
builder.Append(": ");
WriteValue(builder, accessor, value);
WriteValue(builder, value);
first = false;
}
builder.Append(first ? "}" : " }");
......@@ -291,93 +291,81 @@ namespace Google.Protobuf
throw new ArgumentException("Invalid field type");
}
}
private void WriteValue(StringBuilder builder, IFieldAccessor accessor, object value)
private void WriteValue(StringBuilder builder, object value)
{
if (accessor.Descriptor.IsMap)
if (value == null)
{
WriteDictionary(builder, accessor, (IDictionary) value);
WriteNull(builder);
}
else if (accessor.Descriptor.IsRepeated)
else if (value is bool)
{
WriteList(builder, accessor, (IList) value);
builder.Append((bool) value ? "true" : "false");
}
else
else if (value is ByteString)
{
WriteSingleValue(builder, accessor.Descriptor, value);
// Nothing in Base64 needs escaping
builder.Append('"');
builder.Append(((ByteString) value).ToBase64());
builder.Append('"');
}
}
private void WriteSingleValue(StringBuilder builder, FieldDescriptor descriptor, object value)
{
switch (descriptor.FieldType)
else if (value is string)
{
case FieldType.Bool:
builder.Append((bool) value ? "true" : "false");
break;
case FieldType.Bytes:
// Nothing in Base64 needs escaping
WriteString(builder, (string) value);
}
else if (value is IDictionary)
{
WriteDictionary(builder, (IDictionary) value);
}
else if (value is IList)
{
WriteList(builder, (IList) value);
}
else if (value is int || value is uint)
{
IFormattable formattable = (IFormattable) value;
builder.Append(formattable.ToString("d", CultureInfo.InvariantCulture));
}
else if (value is long || value is ulong)
{
builder.Append('"');
IFormattable formattable = (IFormattable) value;
builder.Append(formattable.ToString("d", CultureInfo.InvariantCulture));
builder.Append('"');
}
else if (value is System.Enum)
{
WriteString(builder, value.ToString());
}
else if (value is float || value is double)
{
string text = ((IFormattable) value).ToString("r", CultureInfo.InvariantCulture);
if (text == "NaN" || text == "Infinity" || text == "-Infinity")
{
builder.Append('"');
builder.Append(((ByteString) value).ToBase64());
builder.Append(text);
builder.Append('"');
break;
case FieldType.String:
WriteString(builder, (string) value);
break;
case FieldType.Fixed32:
case FieldType.UInt32:
case FieldType.SInt32:
case FieldType.Int32:
case FieldType.SFixed32:
{
IFormattable formattable = (IFormattable) value;
builder.Append(formattable.ToString("d", CultureInfo.InvariantCulture));
break;
}
case FieldType.Enum:
EnumValueDescriptor enumValue = descriptor.EnumType.FindValueByNumber((int) value);
// We will already have validated that this is a known value.
WriteString(builder, enumValue.Name);
break;
case FieldType.Fixed64:
case FieldType.UInt64:
case FieldType.SFixed64:
case FieldType.Int64:
case FieldType.SInt64:
{
builder.Append('"');
IFormattable formattable = (IFormattable) value;
builder.Append(formattable.ToString("d", CultureInfo.InvariantCulture));
builder.Append('"');
break;
}
case FieldType.Double:
case FieldType.Float:
string text = ((IFormattable) value).ToString("r", CultureInfo.InvariantCulture);
if (text == "NaN" || text == "Infinity" || text == "-Infinity")
{
builder.Append('"');
builder.Append(text);
builder.Append('"');
}
else
{
builder.Append(text);
}
break;
case FieldType.Message:
case FieldType.Group: // Never expect to get this, but...
if (descriptor.MessageType.IsWellKnownType)
{
WriteWellKnownTypeValue(builder, descriptor.MessageType, value, true);
}
else
{
WriteMessage(builder, (IMessage) value);
}
break;
default:
throw new ArgumentException("Invalid field type: " + descriptor.FieldType);
}
else
{
builder.Append(text);
}
}
else if (value is IMessage)
{
IMessage message = (IMessage) value;
if (message.Descriptor.IsWellKnownType)
{
WriteWellKnownTypeValue(builder, message.Descriptor, value, true);
}
else
{
WriteMessage(builder, (IMessage) value);
}
}
else
{
throw new ArgumentException("Unable to format value of type " + value.GetType());
}
}
......@@ -398,7 +386,7 @@ namespace Google.Protobuf
// so we can write it as if we were unconditionally writing the Value field for the wrapper type.
if (descriptor.File == Int32Value.Descriptor.File)
{
WriteSingleValue(builder, descriptor.FindFieldByNumber(1), value);
WriteValue(builder, value);
return;
}
if (descriptor.FullName == Timestamp.Descriptor.FullName)
......@@ -424,7 +412,7 @@ namespace Google.Protobuf
if (descriptor.FullName == ListValue.Descriptor.FullName)
{
var fieldAccessor = descriptor.Fields[ListValue.ValuesFieldNumber].Accessor;
WriteList(builder, fieldAccessor, (IList) fieldAccessor.GetValue((IMessage) value));
WriteList(builder, (IList) fieldAccessor.GetValue((IMessage) value));
return;
}
if (descriptor.FullName == Value.Descriptor.FullName)
......@@ -565,7 +553,7 @@ namespace Google.Protobuf
case Value.BoolValueFieldNumber:
case Value.StringValueFieldNumber:
case Value.NumberValueFieldNumber:
WriteSingleValue(builder, specifiedField, value);
WriteValue(builder, value);
return;
case Value.StructValueFieldNumber:
case Value.ListValueFieldNumber:
......@@ -581,13 +569,13 @@ namespace Google.Protobuf
}
}
private void WriteList(StringBuilder builder, IFieldAccessor accessor, IList list)
internal void WriteList(StringBuilder builder, IList list)
{
builder.Append("[ ");
bool first = true;
foreach (var value in list)
{
if (!CanWriteSingleValue(accessor.Descriptor, value))
if (!CanWriteSingleValue(value))
{
continue;
}
......@@ -595,22 +583,20 @@ namespace Google.Protobuf
{
builder.Append(", ");
}
WriteSingleValue(builder, accessor.Descriptor, value);
WriteValue(builder, value);
first = false;
}
builder.Append(first ? "]" : " ]");
}
private void WriteDictionary(StringBuilder builder, IFieldAccessor accessor, IDictionary dictionary)
internal void WriteDictionary(StringBuilder builder, IDictionary dictionary)
{
builder.Append("{ ");
bool first = true;
FieldDescriptor keyType = accessor.Descriptor.MessageType.FindFieldByNumber(1);
FieldDescriptor valueType = accessor.Descriptor.MessageType.FindFieldByNumber(2);
// This will box each pair. Could use IDictionaryEnumerator, but that's ugly in terms of disposal.
foreach (DictionaryEntry pair in dictionary)
{
if (!CanWriteSingleValue(valueType, pair.Value))
if (!CanWriteSingleValue(pair.Value))
{
continue;
}
......@@ -619,32 +605,29 @@ namespace Google.Protobuf
builder.Append(", ");
}
string keyText;
switch (keyType.FieldType)
if (pair.Key is string)
{
case FieldType.String:
keyText = (string) pair.Key;
break;
case FieldType.Bool:
keyText = (bool) pair.Key ? "true" : "false";
break;
case FieldType.Fixed32:
case FieldType.Fixed64:
case FieldType.SFixed32:
case FieldType.SFixed64:
case FieldType.Int32:
case FieldType.Int64:
case FieldType.SInt32:
case FieldType.SInt64:
case FieldType.UInt32:
case FieldType.UInt64:
keyText = ((IFormattable) pair.Key).ToString("d", CultureInfo.InvariantCulture);
break;
default:
throw new ArgumentException("Invalid key type: " + keyType.FieldType);
keyText = (string) pair.Key;
}
else if (pair.Key is bool)
{
keyText = (bool) pair.Key ? "true" : "false";
}
else if (pair.Key is int || pair.Key is uint | pair.Key is long || pair.Key is ulong)
{
keyText = ((IFormattable) pair.Key).ToString("d", CultureInfo.InvariantCulture);
}
else
{
if (pair.Key == null)
{
throw new ArgumentException("Dictionary has entry with null key");
}
throw new ArgumentException("Unhandled dictionary key type: " + pair.Key.GetType());
}
WriteString(builder, keyText);
builder.Append(": ");
WriteSingleValue(builder, valueType, pair.Value);
WriteValue(builder, pair.Value);
first = false;
}
builder.Append(first ? "}" : " }");
......@@ -655,12 +638,11 @@ namespace Google.Protobuf
/// Currently only relevant for enums, where unknown values can't be represented.
/// For repeated/map fields, this always returns true.
/// </summary>
private bool CanWriteSingleValue(FieldDescriptor descriptor, object value)
private bool CanWriteSingleValue(object value)
{
if (descriptor.FieldType == FieldType.Enum)
if (value is System.Enum)
{
EnumValueDescriptor enumValue = descriptor.EnumType.FindValueByNumber((int) value);
return enumValue != null;
return System.Enum.IsDefined(value.GetType(), value);
}
return true;
}
......
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