diff options
author | Jon Skeet <jonskeet@google.com> | 2015-06-24 17:21:55 +0100 |
---|---|---|
committer | Jon Skeet <jonskeet@google.com> | 2015-06-25 09:39:28 +0100 |
commit | 0d684d34209f8405106e580af854c45fb7c3f16d (patch) | |
tree | 225fcaa27fdc29b3f454b7a4b29b5b843ee711df /csharp | |
parent | 0698aa973740d2a666fed8281c84a884a4227aa4 (diff) | |
download | protobuf-0d684d34209f8405106e580af854c45fb7c3f16d.tar.gz protobuf-0d684d34209f8405106e580af854c45fb7c3f16d.tar.bz2 protobuf-0d684d34209f8405106e580af854c45fb7c3f16d.zip |
First pass at map support.
More tests required. Generated code in next commit.
Diffstat (limited to 'csharp')
-rw-r--r-- | csharp/src/ProtocolBuffers.Test/GeneratedMessageTest.cs | 61 | ||||
-rw-r--r-- | csharp/src/ProtocolBuffers/CodedOutputStream.cs | 8 | ||||
-rw-r--r-- | csharp/src/ProtocolBuffers/Collections/MapField.cs | 400 | ||||
-rw-r--r-- | csharp/src/ProtocolBuffers/FieldCodec.cs | 178 | ||||
-rw-r--r-- | csharp/src/ProtocolBuffers/ProtocolBuffers.csproj | 2 |
5 files changed, 649 insertions, 0 deletions
diff --git a/csharp/src/ProtocolBuffers.Test/GeneratedMessageTest.cs b/csharp/src/ProtocolBuffers.Test/GeneratedMessageTest.cs index 26165428..81e35940 100644 --- a/csharp/src/ProtocolBuffers.Test/GeneratedMessageTest.cs +++ b/csharp/src/ProtocolBuffers.Test/GeneratedMessageTest.cs @@ -1,4 +1,6 @@ using System;
+using System.Configuration;
+using System.IO;
using Google.Protobuf.TestProtos;
using NUnit.Framework;
@@ -147,6 +149,65 @@ namespace Google.Protobuf }
[Test]
+ public void RoundTrip_Maps()
+ {
+ var message = new TestAllTypes
+ {
+ MapBoolToEnum = {
+ { false, TestAllTypes.Types.NestedEnum.BAR},
+ { true, TestAllTypes.Types.NestedEnum.BAZ}
+ },
+ MapInt32ToBytes = {
+ { 5, ByteString.CopyFrom(6, 7, 8) },
+ { 25, ByteString.CopyFrom(1, 2, 3, 4, 5) },
+ { 10, ByteString.Empty }
+ },
+ MapStringToNestedMessage = {
+ { "", new TestAllTypes.Types.NestedMessage { Bb = 10 } },
+ { "null value", null },
+ }
+ };
+
+ byte[] bytes = message.ToByteArray();
+ TestAllTypes parsed = TestAllTypes.Parser.ParseFrom(bytes);
+ Assert.AreEqual(message, parsed);
+ }
+
+ [Test]
+ public void MapWithEmptyEntry()
+ {
+ var message = new TestAllTypes
+ {
+ MapInt32ToBytes = { { 0, ByteString.Empty } }
+ };
+
+ byte[] bytes = message.ToByteArray();
+ Assert.AreEqual(3, bytes.Length); // Tag for field entry (2 bytes), length of entry (0; 1 byte)
+
+ var parsed = TestAllTypes.Parser.ParseFrom(bytes);
+ Assert.AreEqual(1, parsed.MapInt32ToBytes.Count);
+ Assert.AreEqual(ByteString.Empty, parsed.MapInt32ToBytes[0]);
+ }
+
+ [Test]
+ public void MapWithOnlyValue()
+ {
+ // Hand-craft the stream to contain a single entry with just a value.
+ var memoryStream = new MemoryStream();
+ var output = CodedOutputStream.CreateInstance(memoryStream);
+ output.WriteTag(TestAllTypes.MapStringToNestedMessageFieldNumber, WireFormat.WireType.LengthDelimited);
+ var nestedMessage = new TestAllTypes.Types.NestedMessage { Bb = 20 };
+ // Size of the entry (tag, size written by WriteMessage, data written by WriteMessage)
+ output.WriteRawVarint32((uint)(nestedMessage.CalculateSize() + 3));
+ output.WriteTag(2, WireFormat.WireType.LengthDelimited);
+ output.WriteMessage(nestedMessage);
+ output.Flush();
+
+ var parsed = TestAllTypes.Parser.ParseFrom(memoryStream.ToArray());
+ Assert.AreEqual(nestedMessage, parsed.MapStringToNestedMessage[""]);
+ }
+
+ [Test]
public void CloneSingleNonMessageValues()
{
var original = new TestAllTypes
diff --git a/csharp/src/ProtocolBuffers/CodedOutputStream.cs b/csharp/src/ProtocolBuffers/CodedOutputStream.cs index e56ce789..def874c0 100644 --- a/csharp/src/ProtocolBuffers/CodedOutputStream.cs +++ b/csharp/src/ProtocolBuffers/CodedOutputStream.cs @@ -476,6 +476,14 @@ namespace Google.Protobuf }
/// <summary>
+ /// Writes an already-encoded tag.
+ /// </summary>
+ public void WriteTag(uint tag)
+ {
+ WriteRawVarint32(tag);
+ }
+
+ /// <summary>
/// Writes the given single-byte tag directly to the stream.
/// </summary>
public void WriteRawTag(byte b1)
diff --git a/csharp/src/ProtocolBuffers/Collections/MapField.cs b/csharp/src/ProtocolBuffers/Collections/MapField.cs new file mode 100644 index 00000000..2f5a741d --- /dev/null +++ b/csharp/src/ProtocolBuffers/Collections/MapField.cs @@ -0,0 +1,400 @@ +#region Copyright notice and license +// Protocol Buffers - Google's data interchange format +// Copyright 2015 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Google.Protobuf.Collections +{ + /// <summary> + /// Representation of a map field in a Protocol Buffer message. + /// </summary> + /// <remarks> + /// 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"/>. + /// </remarks> + /// <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> + public sealed class MapField<TKey, TValue> : IDeepCloneable<MapField<TKey, TValue>>, IFreezable, IDictionary<TKey, TValue>, IEquatable<MapField<TKey, TValue>> + { + // TODO: Don't create the map/list until we have an entry. (Assume many maps will be empty.) + private bool frozen; + private readonly Dictionary<TKey, LinkedListNode<KeyValuePair<TKey, TValue>>> map = + new Dictionary<TKey, LinkedListNode<KeyValuePair<TKey, TValue>>>(); + private readonly LinkedList<KeyValuePair<TKey, TValue>> list = new LinkedList<KeyValuePair<TKey, TValue>>(); + + public MapField<TKey, TValue> Clone() + { + var clone = new MapField<TKey, TValue>(); + // Keys are never cloneable. Values might be. + if (typeof(IDeepCloneable<TValue>).IsAssignableFrom(typeof(TValue))) + { + foreach (var pair in list) + { + clone.Add(pair.Key, pair.Value == null ? pair.Value : ((IDeepCloneable<TValue>) pair.Value).Clone()); + } + } + else + { + // Nothing is cloneable, so we don't need to worry. + clone.Add(this); + } + return clone; + } + + public void Add(TKey key, TValue value) + { + ThrowHelper.ThrowIfNull(key, "key"); + this.CheckMutable(); + if (ContainsKey(key)) + { + throw new ArgumentException("Key already exists in map", "key"); + } + this[key] = value; + } + + public bool ContainsKey(TKey key) + { + return map.ContainsKey(key); + } + + public bool Remove(TKey key) + { + this.CheckMutable(); + LinkedListNode<KeyValuePair<TKey, TValue>> node; + if (map.TryGetValue(key, out node)) + { + map.Remove(key); + node.List.Remove(node); + return true; + } + else + { + return false; + } + } + + public bool TryGetValue(TKey key, out TValue value) + { + LinkedListNode<KeyValuePair<TKey, TValue>> node; + if (map.TryGetValue(key, out node)) + { + value = node.Value.Value; + return true; + } + else + { + value = default(TValue); + return false; + } + } + + public TValue this[TKey key] + { + get + { + TValue value; + if (TryGetValue(key, out value)) + { + return value; + } + throw new KeyNotFoundException(); + } + set + { + this.CheckMutable(); + LinkedListNode<KeyValuePair<TKey, TValue>> node; + var pair = new KeyValuePair<TKey, TValue>(key, value); + if (map.TryGetValue(key, out node)) + { + node.Value = pair; + } + else + { + node = list.AddLast(pair); + map[key] = node; + } + } + } + + // TODO: Make these views? + public ICollection<TKey> Keys { get { return list.Select(t => t.Key).ToList(); } } + public ICollection<TValue> Values { get { return list.Select(t => t.Value).ToList(); } } + + public void Add(IDictionary<TKey, TValue> entries) + { + foreach (var pair in entries) + { + Add(pair); + } + } + + public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() + { + return list.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public void Add(KeyValuePair<TKey, TValue> item) + { + this.CheckMutable(); + Add(item.Key, item.Value); + } + + public void Clear() + { + this.CheckMutable(); + list.Clear(); + map.Clear(); + } + + public bool Contains(KeyValuePair<TKey, TValue> item) + { + TValue value; + return TryGetValue(item.Key, out value) + && EqualityComparer<TValue>.Default.Equals(item.Value, value); + } + + public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex) + { + list.CopyTo(array, arrayIndex); + } + + public bool Remove(KeyValuePair<TKey, TValue> item) + { + this.CheckMutable(); + return Remove(item.Key); + } + + public int Count { get { return list.Count; } } + public bool IsReadOnly { get { return frozen; } } + + public void Freeze() + { + if (IsFrozen) + { + return; + } + frozen = true; + // Only values can be frozen, as all the key types are simple. + // Everything can be done in-place, as we're just freezing objects. + if (typeof(IFreezable).IsAssignableFrom(typeof(TValue))) + { + for (var node = list.First; node != null; node = node.Next) + { + var pair = node.Value; + IFreezable freezableValue = pair.Value as IFreezable; + if (freezableValue != null) + { + freezableValue.Freeze(); + } + } + } + } + + public bool IsFrozen { get { return frozen; } } + + public override bool Equals(object other) + { + return Equals(other as MapField<TKey, TValue>); + } + + public override int GetHashCode() + { + var valueComparer = EqualityComparer<TValue>.Default; + int hash = 0; + foreach (var pair in list) + { + hash ^= pair.Key.GetHashCode() * 31 + valueComparer.GetHashCode(pair.Value); + } + return hash; + } + + public bool Equals(MapField<TKey, TValue> other) + { + if (other == null) + { + return false; + } + if (other == this) + { + return true; + } + if (other.Count != this.Count) + { + return false; + } + var valueComparer = EqualityComparer<TValue>.Default; + foreach (var pair in this) + { + TValue value; + if (!other.TryGetValue(pair.Key, out value)) + { + return false; + } + if (!valueComparer.Equals(value, pair.Value)) + { + return false; + } + } + return true; + } + + public void AddEntriesFrom(CodedInputStream input, Codec codec) + { + // TODO: Peek at the next tag and see if it's the same. If it is, we can reuse the entry object... + var adapter = new Codec.MessageAdapter(codec); + adapter.Reset(); + input.ReadMessage(adapter); + this[adapter.Key] = adapter.Value; + } + + public void WriteTo(CodedOutputStream output, Codec codec) + { + var message = new Codec.MessageAdapter(codec); + foreach (var entry in list) + { + message.Key = entry.Key; + message.Value = entry.Value; + output.WriteTag(codec.MapTag); + output.WriteMessage(message); + } + } + + public int CalculateSize(Codec codec) + { + var message = new Codec.MessageAdapter(codec); + int size = 0; + foreach (var entry in list) + { + message.Key = entry.Key; + message.Value = entry.Value; + size += CodedOutputStream.ComputeRawVarint32Size(codec.MapTag); + size += CodedOutputStream.ComputeMessageSize(message); + } + return size; + } + + /// <summary> + /// A codec for a specific map field. This contains all the information required to encoded and + /// decode the nested messages. + /// </summary> + public sealed class Codec + { + private readonly FieldCodec<TKey> keyCodec; + private readonly FieldCodec<TValue> valueCodec; + private readonly uint mapTag; + + public Codec(FieldCodec<TKey> keyCodec, FieldCodec<TValue> valueCodec, uint mapTag) + { + this.keyCodec = keyCodec; + this.valueCodec = valueCodec; + this.mapTag = mapTag; + } + + /// <summary> + /// The tag used in the enclosing message to indicate map entries. + /// </summary> + internal uint MapTag { get { return mapTag; } } + + /// <summary> + /// A mutable message class, used for parsing and serializing. This + /// delegates the work to a codec, but implements the <see cref="IMessage"/> interface + /// for interop with <see cref="CodedInputStream"/> and <see cref="CodedOutputStream"/>. + /// This is nested inside Codec as it's tightly coupled to the associated codec, + /// and it's simpler if it has direct access to all its fields. + /// </summary> + internal class MessageAdapter : IMessage + { + private readonly Codec codec; + internal TKey Key { get; set; } + internal TValue Value { get; set; } + internal int Size { get; set; } + + internal MessageAdapter(Codec codec) + { + this.codec = codec; + } + + internal void Reset() + { + Key = codec.keyCodec.DefaultValue; + Value = codec.valueCodec.DefaultValue; + } + + public void MergeFrom(CodedInputStream input) + { + uint tag; + while (input.ReadTag(out tag)) + { + if (tag == 0) + { + throw InvalidProtocolBufferException.InvalidTag(); + } + if (tag == codec.keyCodec.Tag) + { + Key = codec.keyCodec.Read(input); + } + else if (tag == codec.valueCodec.Tag) + { + Value = codec.valueCodec.Read(input); + } + else if (WireFormat.IsEndGroupTag(tag)) + { + // TODO(jonskeet): Do we need this? (Given that we don't support groups...) + return; + } + } + } + + public void WriteTo(CodedOutputStream output) + { + codec.keyCodec.Write(output, Key); + codec.valueCodec.Write(output, Value); + } + + public int CalculateSize() + { + return codec.keyCodec.CalculateSize(Key) + codec.valueCodec.CalculateSize(Value); + } + } + } + } +} diff --git a/csharp/src/ProtocolBuffers/FieldCodec.cs b/csharp/src/ProtocolBuffers/FieldCodec.cs new file mode 100644 index 00000000..931b54d3 --- /dev/null +++ b/csharp/src/ProtocolBuffers/FieldCodec.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; + +namespace Google.Protobuf +{ + /// <summary> + /// Factory methods for <see cref="FieldCodec{T}"/>. + /// </summary> + public static class FieldCodec + { + public static FieldCodec<string> ForString(uint tag) + { + return new FieldCodec<string>(input => input.ReadString(), (output, value) => output.WriteString(value), CodedOutputStream.ComputeStringSize, tag); + } + + public static FieldCodec<ByteString> ForBytes(uint tag) + { + return new FieldCodec<ByteString>(input => input.ReadBytes(), (output, value) => output.WriteBytes(value), CodedOutputStream.ComputeBytesSize, tag); + } + + public static FieldCodec<bool> ForBool(uint tag) + { + return new FieldCodec<bool>(input => input.ReadBool(), (output, value) => output.WriteBool(value), CodedOutputStream.ComputeBoolSize, tag); + } + + public static FieldCodec<int> ForInt32(uint tag) + { + return new FieldCodec<int>(input => input.ReadInt32(), (output, value) => output.WriteInt32(value), CodedOutputStream.ComputeInt32Size, tag); + } + + public static FieldCodec<int> ForSInt32(uint tag) + { + return new FieldCodec<int>(input => input.ReadSInt32(), (output, value) => output.WriteSInt32(value), CodedOutputStream.ComputeSInt32Size, tag); + } + + public static FieldCodec<uint> ForFixedInt32(uint tag) + { + return new FieldCodec<uint>(input => input.ReadFixed32(), (output, value) => output.WriteFixed32(value), CodedOutputStream.ComputeFixed32Size, tag); + } + + public static FieldCodec<uint> ForUInt32(uint tag) + { + return new FieldCodec<uint>(input => input.ReadUInt32(), (output, value) => output.WriteUInt32(value), CodedOutputStream.ComputeUInt32Size, tag); + } + + public static FieldCodec<long> ForInt64(uint tag) + { + return new FieldCodec<long>(input => input.ReadInt64(), (output, value) => output.WriteInt64(value), CodedOutputStream.ComputeInt64Size, tag); + } + + public static FieldCodec<long> ForSInt64(uint tag) + { + return new FieldCodec<long>(input => input.ReadSInt64(), (output, value) => output.WriteSInt64(value), CodedOutputStream.ComputeSInt64Size, tag); + } + + public static FieldCodec<ulong> ForFixedInt64(uint tag) + { + return new FieldCodec<ulong>(input => input.ReadFixed64(), (output, value) => output.WriteFixed64(value), CodedOutputStream.ComputeFixed64Size, tag); + } + + public static FieldCodec<ulong> ForUInt64(uint tag) + { + return new FieldCodec<ulong>(input => input.ReadUInt64(), (output, value) => output.WriteUInt64(value), CodedOutputStream.ComputeUInt64Size, tag); + } + + public static FieldCodec<float> ForFloat(uint tag) + { + return new FieldCodec<float>(input => input.ReadFloat(), (output, value) => output.WriteFloat(value), CodedOutputStream.ComputeFloatSize, tag); + } + + public static FieldCodec<double> ForDouble(uint tag) + { + return new FieldCodec<double>(input => input.ReadFloat(), (output, value) => output.WriteDouble(value), CodedOutputStream.ComputeDoubleSize, tag); + } + + // Enums are tricky. We can probably use expression trees to build these delegates automatically, + // but it's easy to generate the code fdor it. + public static FieldCodec<T> ForEnum<T>(uint tag, Func<T, int> toInt32, Func<int, T> fromInt32) + { + return new FieldCodec<T>(input => fromInt32( + input.ReadEnum()), + (output, value) => output.WriteEnum(toInt32(value)), + value => CodedOutputStream.ComputeEnumSize(toInt32(value)), tag); + } + + public static FieldCodec<T> ForMessage<T>(uint tag, MessageParser<T> parser) where T : IMessage<T> + { + return new FieldCodec<T>(input => { T message = parser.CreateTemplate(); input.ReadMessage(message); return message; }, + (output, value) => output.WriteMessage(value), message => CodedOutputStream.ComputeMessageSize(message), tag); + } + } + + /// <summary> + /// An encode/decode pair for a single field. This effectively encapsulates + /// all the information needed to read or write the field value from/to a coded + /// stream. + /// </summary> + /// <remarks> + /// This never writes default values to the stream, and is not currently designed + /// to play well with packed arrays. + /// </remarks> + public sealed class FieldCodec<T> + { + private static readonly Func<T, bool> IsDefault; + private static readonly T Default; + + static FieldCodec() + { + if (typeof(T) == typeof(string)) + { + Default = (T)(object)""; + IsDefault = CreateDefaultValueCheck<string>(x => x.Length == 0); + } + else if (typeof(T) == typeof(ByteString)) + { + Default = (T)(object)ByteString.Empty; + IsDefault = CreateDefaultValueCheck<ByteString>(x => x.Length == 0); + } + else if (!typeof(T).IsValueType) + { + // Default default + IsDefault = CreateDefaultValueCheck<T>(x => x == null); + } + else + { + // Default default + IsDefault = CreateDefaultValueCheck<T>(x => EqualityComparer<T>.Default.Equals(x, default(T))); + } + } + + private static Func<T, bool> CreateDefaultValueCheck<TTmp>(Func<TTmp, bool> check) + { + return (Func<T, bool>)(object)check; + } + + private readonly Func<CodedInputStream, T> reader; + private readonly Action<CodedOutputStream, T> writer; + private readonly Func<T, int> sizeComputer; + private readonly uint tag; + private readonly int tagSize; + + internal FieldCodec( + Func<CodedInputStream, T> reader, + Action<CodedOutputStream, T> writer, + Func<T, int> sizeComputer, + uint tag) + { + this.reader = reader; + this.writer = writer; + this.sizeComputer = sizeComputer; + this.tag = tag; + tagSize = CodedOutputStream.ComputeRawVarint32Size(tag); + } + + public uint Tag { get { return tag; } } + + public T DefaultValue { get { return Default; } } + + public void Write(CodedOutputStream output, T value) + { + if (!IsDefault(value)) + { + output.WriteTag(tag); + writer(output, value); + } + } + + public T Read(CodedInputStream input) + { + return reader(input); + } + + public int CalculateSize(T value) + { + return IsDefault(value) ? 0 : sizeComputer(value) + CodedOutputStream.ComputeRawVarint32Size(tag); + } + } +} diff --git a/csharp/src/ProtocolBuffers/ProtocolBuffers.csproj b/csharp/src/ProtocolBuffers/ProtocolBuffers.csproj index d1551148..5edeff70 100644 --- a/csharp/src/ProtocolBuffers/ProtocolBuffers.csproj +++ b/csharp/src/ProtocolBuffers/ProtocolBuffers.csproj @@ -60,6 +60,7 @@ <Compile Include="CodedOutputStream.cs" />
<Compile Include="Collections\Dictionaries.cs" />
<Compile Include="Collections\Lists.cs" />
+ <Compile Include="Collections\MapField.cs" />
<Compile Include="Collections\ReadOnlyDictionary.cs" />
<Compile Include="Collections\RepeatedField.cs" />
<Compile Include="Collections\RepeatedFieldExtensions.cs" />
@@ -84,6 +85,7 @@ <Compile Include="Descriptors\MethodDescriptor.cs" />
<Compile Include="Descriptors\PackageDescriptor.cs" />
<Compile Include="Descriptors\ServiceDescriptor.cs" />
+ <Compile Include="FieldCodec.cs" />
<Compile Include="FrameworkPortability.cs" />
<Compile Include="Freezable.cs" />
<Compile Include="MessageExtensions.cs" />
|