Ported FieldMaskUtil from Java to C# (#5045)

* Ported FieldMaskUtil from Java to C#

* Merged FieldMaskUtil into FieldMaskPartial

- Removed FieldMaskUtil
- Moved FieldMaskTree to root
- Updated tests

* Improved tests

- Removed internal method FieldMaskTree.GetFieldPaths
- Proof FieldMask.Paths only contains expected values

* Added FieldMaskTreeTest to Makefile

* Added FieldMaskTree to Makefile
This commit is contained in:
Benjamin Krämer 2018-10-08 12:54:05 -07:00 committed by Jie Luo
parent 6a51c03823
commit 80e530dabf
6 changed files with 1262 additions and 17 deletions

View File

@ -93,6 +93,7 @@ csharp_EXTRA_DIST= \
csharp/src/Google.Protobuf.Test/DeprecatedMemberTest.cs \
csharp/src/Google.Protobuf.Test/EqualityTester.cs \
csharp/src/Google.Protobuf.Test/FieldCodecTest.cs \
csharp/src/Google.Protobuf.Test/FieldMaskTreeTest.cs \
csharp/src/Google.Protobuf.Test/GeneratedMessageTest.cs \
csharp/src/Google.Protobuf.Test/Google.Protobuf.Test.csproj \
csharp/src/Google.Protobuf.Test/IssuesTest.cs \
@ -140,6 +141,7 @@ csharp_EXTRA_DIST= \
csharp/src/Google.Protobuf/Compatibility/StreamExtensions.cs \
csharp/src/Google.Protobuf/Compatibility/TypeExtensions.cs \
csharp/src/Google.Protobuf/FieldCodec.cs \
csharp/src/Google.Protobuf/FieldMaskTree.cs \
csharp/src/Google.Protobuf/FrameworkPortability.cs \
csharp/src/Google.Protobuf/Google.Protobuf.csproj \
csharp/src/Google.Protobuf/ICustomDiagnosticMessage.cs \

View File

@ -0,0 +1,436 @@
#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.Collections.Generic;
using Google.Protobuf.Collections;
using Google.Protobuf.TestProtos;
using NUnit.Framework;
using Google.Protobuf.WellKnownTypes;
namespace Google.Protobuf
{
public class FieldMaskTreeTest
{
[Test]
public void AddFieldPath()
{
FieldMaskTree tree = new FieldMaskTree();
RepeatedField<string> paths = tree.ToFieldMask().Paths;
Assert.AreEqual(0, paths.Count);
tree.AddFieldPath("");
paths = tree.ToFieldMask().Paths;
Assert.AreEqual(1, paths.Count);
Assert.Contains("", paths);
// New branch.
tree.AddFieldPath("foo");
paths = tree.ToFieldMask().Paths;
Assert.AreEqual(2, paths.Count);
Assert.Contains("foo", paths);
// Redundant path.
tree.AddFieldPath("foo");
paths = tree.ToFieldMask().Paths;
Assert.AreEqual(2, paths.Count);
// New branch.
tree.AddFieldPath("bar.baz");
paths = tree.ToFieldMask().Paths;
Assert.AreEqual(3, paths.Count);
Assert.Contains("bar.baz", paths);
// Redundant sub-path.
tree.AddFieldPath("foo.bar");
paths = tree.ToFieldMask().Paths;
Assert.AreEqual(3, paths.Count);
// New branch from a non-root node.
tree.AddFieldPath("bar.quz");
paths = tree.ToFieldMask().Paths;
Assert.AreEqual(4, paths.Count);
Assert.Contains("bar.quz", paths);
// A path that matches several existing sub-paths.
tree.AddFieldPath("bar");
paths = tree.ToFieldMask().Paths;
Assert.AreEqual(3, paths.Count);
Assert.Contains("foo", paths);
Assert.Contains("bar", paths);
}
[Test]
public void MergeFromFieldMask()
{
FieldMaskTree tree = new FieldMaskTree();
tree.MergeFromFieldMask(new FieldMask
{
Paths = {"foo", "bar.baz", "bar.quz"}
});
RepeatedField<string> paths = tree.ToFieldMask().Paths;
Assert.AreEqual(3, paths.Count);
Assert.Contains("foo", paths);
Assert.Contains("bar.baz", paths);
Assert.Contains("bar.quz", paths);
tree.MergeFromFieldMask(new FieldMask
{
Paths = {"foo.bar", "bar"}
});
paths = tree.ToFieldMask().Paths;
Assert.AreEqual(2, paths.Count);
Assert.Contains("foo", paths);
Assert.Contains("bar", paths);
}
[Test]
public void IntersectFieldPath()
{
FieldMaskTree tree = new FieldMaskTree();
FieldMaskTree result = new FieldMaskTree();
tree.MergeFromFieldMask(new FieldMask
{
Paths = {"foo", "bar.baz", "bar.quz"}
});
// Empty path.
tree.IntersectFieldPath("", result);
RepeatedField<string> paths = result.ToFieldMask().Paths;
Assert.AreEqual(0, paths.Count);
// Non-exist path.
tree.IntersectFieldPath("quz", result);
paths = result.ToFieldMask().Paths;
Assert.AreEqual(0, paths.Count);
// Sub-path of an existing leaf.
tree.IntersectFieldPath("foo.bar", result);
paths = result.ToFieldMask().Paths;
Assert.AreEqual(1, paths.Count);
Assert.Contains("foo.bar", paths);
// Match an existing leaf node.
tree.IntersectFieldPath("foo", result);
paths = result.ToFieldMask().Paths;
Assert.AreEqual(1, paths.Count);
Assert.Contains("foo", paths);
// Non-exist path.
tree.IntersectFieldPath("bar.foo", result);
paths = result.ToFieldMask().Paths;
Assert.AreEqual(1, paths.Count);
Assert.Contains("foo", paths);
// Match a non-leaf node.
tree.IntersectFieldPath("bar", result);
paths = result.ToFieldMask().Paths;
Assert.AreEqual(3, paths.Count);
Assert.Contains("foo", paths);
Assert.Contains("bar.baz", paths);
Assert.Contains("bar.quz", paths);
}
private void Merge(FieldMaskTree tree, IMessage source, IMessage destination, FieldMask.MergeOptions options, bool useDynamicMessage)
{
if (useDynamicMessage)
{
var newSource = source.Descriptor.Parser.CreateTemplate();
newSource.MergeFrom(source.ToByteString());
var newDestination = source.Descriptor.Parser.CreateTemplate();
newDestination.MergeFrom(destination.ToByteString());
tree.Merge(newSource, newDestination, options);
// Clear before merging:
foreach (var fieldDescriptor in destination.Descriptor.Fields.InFieldNumberOrder())
{
fieldDescriptor.Accessor.Clear(destination);
}
destination.MergeFrom(newDestination.ToByteString());
}
else
{
tree.Merge(source, destination, options);
}
}
[Test]
[TestCase(true)]
[TestCase(false)]
public void Merge(bool useDynamicMessage)
{
TestAllTypes value = new TestAllTypes
{
SingleInt32 = 1234,
SingleNestedMessage = new TestAllTypes.Types.NestedMessage {Bb = 5678},
RepeatedInt32 = {4321},
RepeatedNestedMessage = {new TestAllTypes.Types.NestedMessage {Bb = 8765}}
};
NestedTestAllTypes source = new NestedTestAllTypes
{
Payload = value,
Child = new NestedTestAllTypes {Payload = value}
};
// Now we have a message source with the following structure:
// [root] -+- payload -+- single_int32
// | +- single_nested_message
// | +- repeated_int32
// | +- repeated_nested_message
// |
// +- child --- payload -+- single_int32
// +- single_nested_message
// +- repeated_int32
// +- repeated_nested_message
FieldMask.MergeOptions options = new FieldMask.MergeOptions();
// Test merging each individual field.
NestedTestAllTypes destination = new NestedTestAllTypes();
Merge(new FieldMaskTree().AddFieldPath("payload.single_int32"),
source, destination, options, useDynamicMessage);
NestedTestAllTypes expected = new NestedTestAllTypes
{
Payload = new TestAllTypes
{
SingleInt32 = 1234
}
};
Assert.AreEqual(expected, destination);
destination = new NestedTestAllTypes();
Merge(new FieldMaskTree().AddFieldPath("payload.single_nested_message"),
source, destination, options, useDynamicMessage);
expected = new NestedTestAllTypes
{
Payload = new TestAllTypes
{
SingleNestedMessage = new TestAllTypes.Types.NestedMessage {Bb = 5678}
}
};
Assert.AreEqual(expected, destination);
destination = new NestedTestAllTypes();
Merge(new FieldMaskTree().AddFieldPath("payload.repeated_int32"),
source, destination, options, useDynamicMessage);
expected = new NestedTestAllTypes
{
Payload = new TestAllTypes
{
RepeatedInt32 = {4321}
}
};
Assert.AreEqual(expected, destination);
destination = new NestedTestAllTypes();
Merge(new FieldMaskTree().AddFieldPath("payload.repeated_nested_message"),
source, destination, options, useDynamicMessage);
expected = new NestedTestAllTypes
{
Payload = new TestAllTypes
{
RepeatedNestedMessage = {new TestAllTypes.Types.NestedMessage {Bb = 8765}}
}
};
Assert.AreEqual(expected, destination);
destination = new NestedTestAllTypes();
Merge(
new FieldMaskTree().AddFieldPath("child.payload.single_int32"),
source,
destination,
options,
useDynamicMessage);
expected = new NestedTestAllTypes
{
Child = new NestedTestAllTypes
{
Payload = new TestAllTypes
{
SingleInt32 = 1234
}
}
};
Assert.AreEqual(expected, destination);
destination = new NestedTestAllTypes();
Merge(
new FieldMaskTree().AddFieldPath("child.payload.single_nested_message"),
source,
destination,
options,
useDynamicMessage);
expected = new NestedTestAllTypes
{
Child = new NestedTestAllTypes
{
Payload = new TestAllTypes
{
SingleNestedMessage = new TestAllTypes.Types.NestedMessage {Bb = 5678}
}
}
};
Assert.AreEqual(expected, destination);
destination = new NestedTestAllTypes();
Merge(new FieldMaskTree().AddFieldPath("child.payload.repeated_int32"),
source, destination, options, useDynamicMessage);
expected = new NestedTestAllTypes
{
Child = new NestedTestAllTypes
{
Payload = new TestAllTypes
{
RepeatedInt32 = {4321}
}
}
};
Assert.AreEqual(expected, destination);
destination = new NestedTestAllTypes();
Merge(new FieldMaskTree().AddFieldPath("child.payload.repeated_nested_message"),
source, destination, options, useDynamicMessage);
expected = new NestedTestAllTypes
{
Child = new NestedTestAllTypes
{
Payload = new TestAllTypes
{
RepeatedNestedMessage = {new TestAllTypes.Types.NestedMessage {Bb = 8765}}
}
}
};
Assert.AreEqual(expected, destination);
destination = new NestedTestAllTypes();
Merge(new FieldMaskTree().AddFieldPath("child").AddFieldPath("payload"),
source, destination, options, useDynamicMessage);
Assert.AreEqual(source, destination);
// Test repeated options.
destination = new NestedTestAllTypes
{
Payload = new TestAllTypes
{
RepeatedInt32 = { 1000 }
}
};
Merge(new FieldMaskTree().AddFieldPath("payload.repeated_int32"),
source, destination, options, useDynamicMessage);
// Default behavior is to append repeated fields.
Assert.AreEqual(2, destination.Payload.RepeatedInt32.Count);
Assert.AreEqual(1000, destination.Payload.RepeatedInt32[0]);
Assert.AreEqual(4321, destination.Payload.RepeatedInt32[1]);
// Change to replace repeated fields.
options.ReplaceRepeatedFields = true;
Merge(new FieldMaskTree().AddFieldPath("payload.repeated_int32"),
source, destination, options, useDynamicMessage);
Assert.AreEqual(1, destination.Payload.RepeatedInt32.Count);
Assert.AreEqual(4321, destination.Payload.RepeatedInt32[0]);
// Test message options.
destination = new NestedTestAllTypes
{
Payload = new TestAllTypes
{
SingleInt32 = 1000,
SingleUint32 = 2000
}
};
Merge(new FieldMaskTree().AddFieldPath("payload"),
source, destination, options, useDynamicMessage);
// Default behavior is to merge message fields.
Assert.AreEqual(1234, destination.Payload.SingleInt32);
Assert.AreEqual(2000, destination.Payload.SingleUint32);
// Test merging unset message fields.
NestedTestAllTypes clearedSource = source.Clone();
clearedSource.Payload = null;
destination = new NestedTestAllTypes();
Merge(new FieldMaskTree().AddFieldPath("payload"),
clearedSource, destination, options, useDynamicMessage);
Assert.IsNull(destination.Payload);
// Skip a message field if they are unset in both source and target.
destination = new NestedTestAllTypes();
Merge(new FieldMaskTree().AddFieldPath("payload.single_int32"),
clearedSource, destination, options, useDynamicMessage);
Assert.IsNull(destination.Payload);
// Change to replace message fields.
options.ReplaceMessageFields = true;
destination = new NestedTestAllTypes
{
Payload = new TestAllTypes
{
SingleInt32 = 1000,
SingleUint32 = 2000
}
};
Merge(new FieldMaskTree().AddFieldPath("payload"),
source, destination, options, useDynamicMessage);
Assert.AreEqual(1234, destination.Payload.SingleInt32);
Assert.AreEqual(0, destination.Payload.SingleUint32);
// Test merging unset message fields.
destination = new NestedTestAllTypes
{
Payload = new TestAllTypes
{
SingleInt32 = 1000,
SingleUint32 = 2000
}
};
Merge(new FieldMaskTree().AddFieldPath("payload"),
clearedSource, destination, options, useDynamicMessage);
Assert.IsNull(destination.Payload);
// Test merging unset primitive fields.
destination = source.Clone();
destination.Payload.SingleInt32 = 0;
NestedTestAllTypes sourceWithPayloadInt32Unset = destination;
destination = source.Clone();
Merge(new FieldMaskTree().AddFieldPath("payload.single_int32"),
sourceWithPayloadInt32Unset, destination, options, useDynamicMessage);
Assert.AreEqual(0, destination.Payload.SingleInt32);
// Change to clear unset primitive fields.
options.ReplacePrimitiveFields = true;
destination = source.Clone();
Merge(new FieldMaskTree().AddFieldPath("payload.single_int32"),
sourceWithPayloadInt32Unset, destination, options, useDynamicMessage);
Assert.IsNotNull(destination.Payload);
}
}
}

View File

@ -30,7 +30,8 @@
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#endregion
using System;
using Google.Protobuf.TestProtos;
using NUnit.Framework;
namespace Google.Protobuf.WellKnownTypes
@ -58,5 +59,187 @@ namespace Google.Protobuf.WellKnownTypes
"{ \"@warning\": \"Invalid FieldMask\", \"paths\": [ \"x\", \"foo__bar\", \"x\\\\y\" ] }",
mask.ToString());
}
[Test]
public void IsValid()
{
Assert.IsTrue(FieldMask.IsValid<NestedTestAllTypes>("payload"));
Assert.IsFalse(FieldMask.IsValid<NestedTestAllTypes>("nonexist"));
Assert.IsTrue(FieldMask.IsValid<NestedTestAllTypes>("payload.single_int32"));
Assert.IsTrue(FieldMask.IsValid<NestedTestAllTypes>("payload.repeated_int32"));
Assert.IsTrue(FieldMask.IsValid<NestedTestAllTypes>("payload.single_nested_message"));
Assert.IsTrue(FieldMask.IsValid<NestedTestAllTypes>("payload.repeated_nested_message"));
Assert.IsFalse(FieldMask.IsValid<NestedTestAllTypes>("payload.nonexist"));
Assert.IsTrue(FieldMask.IsValid<NestedTestAllTypes>(FieldMask.FromString("payload")));
Assert.IsFalse(FieldMask.IsValid<NestedTestAllTypes>(FieldMask.FromString("nonexist")));
Assert.IsFalse(FieldMask.IsValid<NestedTestAllTypes>(FieldMask.FromString("payload,nonexist")));
Assert.IsTrue(FieldMask.IsValid(NestedTestAllTypes.Descriptor, "payload"));
Assert.IsFalse(FieldMask.IsValid(NestedTestAllTypes.Descriptor, "nonexist"));
Assert.IsTrue(FieldMask.IsValid(NestedTestAllTypes.Descriptor, FieldMask.FromString("payload")));
Assert.IsFalse(FieldMask.IsValid(NestedTestAllTypes.Descriptor, FieldMask.FromString("nonexist")));
Assert.IsTrue(FieldMask.IsValid<NestedTestAllTypes>("payload.single_nested_message.bb"));
// Repeated fields cannot have sub-paths.
Assert.IsFalse(FieldMask.IsValid<NestedTestAllTypes>("payload.repeated_nested_message.bb"));
// Non-message fields cannot have sub-paths.
Assert.IsFalse(FieldMask.IsValid<NestedTestAllTypes>("payload.single_int32.bb"));
}
[Test]
[TestCase(new string[] { }, "\"\"")]
[TestCase(new string[] { "foo" }, "\"foo\"")]
[TestCase(new string[] { "foo", "bar" }, "\"foo,bar\"")]
[TestCase(new string[] { "", "foo", "", "bar", "" }, "\",foo,,bar,\"")]
public void ToString(string[] input, string expectedOutput)
{
FieldMask mask = new FieldMask();
mask.Paths.AddRange(input);
Assert.AreEqual(expectedOutput, mask.ToString());
}
[Test]
[TestCase("", new string[] { })]
[TestCase("foo", new string[] { "foo" })]
[TestCase("foo,bar.baz", new string[] { "foo", "bar.baz" })]
[TestCase(",foo,,bar,", new string[] { "foo", "bar" })]
public void FromString(string input, string[] expectedOutput)
{
FieldMask mask = FieldMask.FromString(input);
Assert.AreEqual(expectedOutput.Length, mask.Paths.Count);
for (int i = 0; i < expectedOutput.Length; i++)
{
Assert.AreEqual(expectedOutput[i], mask.Paths[i]);
}
}
[Test]
public void FromString_Validated()
{
// Check whether the field paths are valid if a class parameter is provided.
Assert.DoesNotThrow(() => FieldMask.FromString<NestedTestAllTypes>(",payload"));
Assert.Throws<InvalidProtocolBufferException>(() => FieldMask.FromString<NestedTestAllTypes>("payload,nonexist"));
}
[Test]
[TestCase(new int[] { }, new string[] { })]
[TestCase(new int[] { TestAllTypes.SingleInt32FieldNumber }, new string[] { "single_int32" })]
[TestCase(new int[] { TestAllTypes.SingleInt32FieldNumber, TestAllTypes.SingleInt64FieldNumber }, new string[] { "single_int32", "single_int64" })]
public void FromFieldNumbers(int[] input, string[] expectedOutput)
{
FieldMask mask = FieldMask.FromFieldNumbers<TestAllTypes>(input);
Assert.AreEqual(expectedOutput.Length, mask.Paths.Count);
for (int i = 0; i < expectedOutput.Length; i++)
{
Assert.AreEqual(expectedOutput[i], mask.Paths[i]);
}
}
[Test]
public void FromFieldNumbers_Invalid()
{
Assert.Throws<ArgumentNullException>(() =>
{
int invalidFieldNumber = 1000;
FieldMask.FromFieldNumbers<TestAllTypes>(invalidFieldNumber);
});
}
[Test]
[TestCase(new string[] { }, "\"\"")]
[TestCase(new string[] { "foo" }, "\"foo\"")]
[TestCase(new string[] { "foo", "bar" }, "\"foo,bar\"")]
[TestCase(new string[] { "", "foo", "", "bar", "" }, "\",foo,bar\"")]
public void Normalize(string[] input, string expectedOutput)
{
FieldMask mask = new FieldMask();
mask.Paths.AddRange(input);
FieldMask result = mask.Normalize();
Assert.AreEqual(expectedOutput, result.ToString());
}
[Test]
public void Union()
{
// Only test a simple case here and expect
// {@link FieldMaskTreeTest#AddFieldPath} to cover all scenarios.
FieldMask mask1 = FieldMask.FromString("foo,bar.baz,bar.quz");
FieldMask mask2 = FieldMask.FromString("foo.bar,bar");
FieldMask result = mask1.Union(mask2);
Assert.AreEqual(2, result.Paths.Count);
Assert.Contains("bar", result.Paths);
Assert.Contains("foo", result.Paths);
Assert.That(result.Paths, Has.No.Member("bar.baz"));
Assert.That(result.Paths, Has.No.Member("bar.quz"));
Assert.That(result.Paths, Has.No.Member("foo.bar"));
}
[Test]
public void Union_UsingVarArgs()
{
FieldMask mask1 = FieldMask.FromString("foo");
FieldMask mask2 = FieldMask.FromString("foo.bar,bar.quz");
FieldMask mask3 = FieldMask.FromString("bar.quz");
FieldMask mask4 = FieldMask.FromString("bar");
FieldMask result = mask1.Union(mask2, mask3, mask4);
Assert.AreEqual(2, result.Paths.Count);
Assert.Contains("bar", result.Paths);
Assert.Contains("foo", result.Paths);
Assert.That(result.Paths, Has.No.Member("foo.bar"));
Assert.That(result.Paths, Has.No.Member("bar.quz"));
}
[Test]
public void Intersection()
{
// Only test a simple case here and expect
// {@link FieldMaskTreeTest#IntersectFieldPath} to cover all scenarios.
FieldMask mask1 = FieldMask.FromString("foo,bar.baz,bar.quz");
FieldMask mask2 = FieldMask.FromString("foo.bar,bar");
FieldMask result = mask1.Intersection(mask2);
Assert.AreEqual(3, result.Paths.Count);
Assert.Contains("foo.bar", result.Paths);
Assert.Contains("bar.baz", result.Paths);
Assert.Contains("bar.quz", result.Paths);
Assert.That(result.Paths, Has.No.Member("foo"));
Assert.That(result.Paths, Has.No.Member("bar"));
}
[Test]
public void Merge()
{
// Only test a simple case here and expect
// {@link FieldMaskTreeTest#Merge} to cover all scenarios.
FieldMask fieldMask = FieldMask.FromString("payload");
NestedTestAllTypes source = new NestedTestAllTypes
{
Payload = new TestAllTypes
{
SingleInt32 = 1234,
SingleFixed64 = 4321
}
};
NestedTestAllTypes destination = new NestedTestAllTypes();
fieldMask.Merge(source, destination);
Assert.AreEqual(1234, destination.Payload.SingleInt32);
Assert.AreEqual(4321, destination.Payload.SingleFixed64);
destination = new NestedTestAllTypes
{
Payload = new TestAllTypes
{
SingleInt32 = 4321,
SingleInt64 = 5678
}
};
fieldMask.Merge(source, destination);
Assert.AreEqual(1234, destination.Payload.SingleInt32);
Assert.AreEqual(5678, destination.Payload.SingleInt64);
Assert.AreEqual(4321, destination.Payload.SingleFixed64);
}
}
}

View File

@ -0,0 +1,364 @@
#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.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using Google.Protobuf.Reflection;
using Google.Protobuf.WellKnownTypes;
namespace Google.Protobuf
{
/// <summary>
/// <para>A tree representation of a FieldMask. Each leaf node in this tree represent
/// a field path in the FieldMask.</para>
///
/// <para>For example, FieldMask "foo.bar,foo.baz,bar.baz" as a tree will be:</para>
/// <code>
/// [root] -+- foo -+- bar
/// | |
/// | +- baz
/// |
/// +- bar --- baz
/// </code>
///
/// <para>By representing FieldMasks with this tree structure we can easily convert
/// a FieldMask to a canonical form, merge two FieldMasks, calculate the
/// intersection to two FieldMasks and traverse all fields specified by the
/// FieldMask in a message tree.</para>
/// </summary>
internal sealed class FieldMaskTree
{
private const char FIELD_PATH_SEPARATOR = '.';
internal sealed class Node
{
public Dictionary<string, Node> Children { get; } = new Dictionary<string, Node>();
}
private readonly Node root = new Node();
/// <summary>
/// Creates an empty FieldMaskTree.
/// </summary>
public FieldMaskTree()
{
}
/// <summary>
/// Creates a FieldMaskTree for a given FieldMask.
/// </summary>
public FieldMaskTree(FieldMask mask)
{
MergeFromFieldMask(mask);
}
public override string ToString()
{
return ToFieldMask().ToString();
}
/// <summary>
/// Adds a field path to the tree. In a FieldMask, every field path matches the
/// specified field as well as all its sub-fields. For example, a field path
/// "foo.bar" matches field "foo.bar" and also "foo.bar.baz", etc. When adding
/// a field path to the tree, redundant sub-paths will be removed. That is,
/// after adding "foo.bar" to the tree, "foo.bar.baz" will be removed if it
/// exists, which will turn the tree node for "foo.bar" to a leaf node.
/// Likewise, if the field path to add is a sub-path of an existing leaf node,
/// nothing will be changed in the tree.
/// </summary>
public FieldMaskTree AddFieldPath(string path)
{
var parts = path.Split(FIELD_PATH_SEPARATOR);
if (parts.Length == 0)
{
return this;
}
var node = root;
var createNewBranch = false;
// Find the matching node in the tree.
foreach (var part in parts)
{
// Check whether the path matches an existing leaf node.
if (!createNewBranch
&& node != root
&& node.Children.Count == 0)
{
// The path to add is a sub-path of an existing leaf node.
return this;
}
if (!node.Children.TryGetValue(part, out var childNode))
{
createNewBranch = true;
childNode = new Node();
node.Children.Add(part, childNode);
}
node = childNode;
}
// Turn the matching node into a leaf node (i.e., remove sub-paths).
node.Children.Clear();
return this;
}
/// <summary>
/// Merges all field paths in a FieldMask into this tree.
/// </summary>
public FieldMaskTree MergeFromFieldMask(FieldMask mask)
{
foreach (var path in mask.Paths)
{
AddFieldPath(path);
}
return this;
}
/// <summary>
/// Converts this tree to a FieldMask.
/// </summary>
public FieldMask ToFieldMask()
{
var mask = new FieldMask();
if (root.Children.Count != 0)
{
var paths = new List<string>();
GetFieldPaths(root, "", paths);
mask.Paths.AddRange(paths);
}
return mask;
}
/// <summary>
/// Gathers all field paths in a sub-tree.
/// </summary>
private void GetFieldPaths(Node node, string path, List<string> paths)
{
if (node.Children.Count == 0)
{
paths.Add(path);
return;
}
foreach (var entry in node.Children)
{
var childPath = path.Length == 0 ? entry.Key : path + "." + entry.Key;
GetFieldPaths(entry.Value, childPath, paths);
}
}
/// <summary>
/// Adds the intersection of this tree with the given <paramref name="path"/> to <paramref name="output"/>.
/// </summary>
public void IntersectFieldPath(string path, FieldMaskTree output)
{
if (root.Children.Count == 0)
{
return;
}
var parts = path.Split(FIELD_PATH_SEPARATOR);
if (parts.Length == 0)
{
return;
}
var node = root;
foreach (var part in parts)
{
if (node != root
&& node.Children.Count == 0)
{
// The given path is a sub-path of an existing leaf node in the tree.
output.AddFieldPath(path);
return;
}
if (!node.Children.TryGetValue(part, out node))
{
return;
}
}
// We found a matching node for the path. All leaf children of this matching
// node is in the intersection.
var paths = new List<string>();
GetFieldPaths(node, path, paths);
foreach (var value in paths)
{
output.AddFieldPath(value);
}
}
/// <summary>
/// Merges all fields specified by this FieldMaskTree from <paramref name="source"/> to <paramref name="destination"/>.
/// </summary>
public void Merge(IMessage source, IMessage destination, FieldMask.MergeOptions options)
{
if (source.Descriptor != destination.Descriptor)
{
throw new InvalidProtocolBufferException("Cannot merge messages of different types.");
}
if (root.Children.Count == 0)
{
return;
}
Merge(root, "", source, destination, options);
}
/// <summary>
/// Merges all fields specified by a sub-tree from <paramref name="source"/> to <paramref name="destination"/>.
/// </summary>
private void Merge(
Node node,
string path,
IMessage source,
IMessage destination,
FieldMask.MergeOptions options)
{
if (source.Descriptor != destination.Descriptor)
{
throw new InvalidProtocolBufferException($"source ({source.Descriptor}) and destination ({destination.Descriptor}) descriptor must be equal");
}
var descriptor = source.Descriptor;
foreach (var entry in node.Children)
{
var field = descriptor.FindFieldByName(entry.Key);
if (field == null)
{
Debug.WriteLine($"Cannot find field \"{entry.Key}\" in message type \"{descriptor.FullName}\"");
continue;
}
if (entry.Value.Children.Count != 0)
{
if (field.IsRepeated
|| field.FieldType != FieldType.Message)
{
Debug.WriteLine($"Field \"{field.FullName}\" is not a singular message field and cannot have sub-fields.");
continue;
}
var sourceField = field.Accessor.GetValue(source);
var destinationField = field.Accessor.GetValue(destination);
if (sourceField == null
&& destinationField == null)
{
// If the message field is not present in both source and destination, skip recursing
// so we don't create unnecessary empty messages.
continue;
}
if (destinationField == null)
{
// If we have to merge but the destination does not contain the field, create it.
destinationField = field.MessageType.Parser.CreateTemplate();
field.Accessor.SetValue(destination, destinationField);
}
var childPath = path.Length == 0 ? entry.Key : path + "." + entry.Key;
Merge(entry.Value, childPath, (IMessage)sourceField, (IMessage)destinationField, options);
continue;
}
if (field.IsRepeated)
{
if (options.ReplaceRepeatedFields)
{
field.Accessor.Clear(destination);
}
var sourceField = (IList)field.Accessor.GetValue(source);
var destinationField = (IList)field.Accessor.GetValue(destination);
foreach (var element in sourceField)
{
destinationField.Add(element);
}
}
else
{
var sourceField = field.Accessor.GetValue(source);
if (field.FieldType == FieldType.Message)
{
if (options.ReplaceMessageFields)
{
if (sourceField == null)
{
field.Accessor.Clear(destination);
}
else
{
field.Accessor.SetValue(destination, sourceField);
}
}
else
{
if (sourceField != null)
{
var sourceByteString = ((IMessage)sourceField).ToByteString();
var destinationValue = (IMessage)field.Accessor.GetValue(destination);
if (destinationValue != null)
{
destinationValue.MergeFrom(sourceByteString);
}
else
{
field.Accessor.SetValue(destination, field.MessageType.Parser.ParseFrom(sourceByteString));
}
}
}
}
else
{
if (sourceField != null
|| !options.ReplacePrimitiveFields)
{
field.Accessor.SetValue(destination, sourceField);
}
else
{
field.Accessor.Clear(destination);
}
}
}
}
}
}
}

View File

@ -271,7 +271,25 @@ namespace Google.Protobuf
}
return result.ToString();
}
internal static string FromJsonName(string name)
{
StringBuilder result = new StringBuilder(name.Length);
foreach (char ch in name)
{
if (char.IsUpper(ch))
{
result.Append('_');
result.Append(char.ToLowerInvariant(ch));
}
else
{
result.Append(ch);
}
}
return result.ToString();
}
private static void WriteNull(TextWriter writer)
{
writer.Write("null");

View File

@ -35,15 +35,18 @@ using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Google.Protobuf.Reflection;
namespace Google.Protobuf.WellKnownTypes
{
// Manually-written partial class for the FieldMask well-known type.
public partial class FieldMask : ICustomDiagnosticMessage
{
private const char FIELD_PATH_SEPARATOR = ',';
private const char FIELD_SEPARATOR_REGEX = '.';
/// <summary>
/// Converts a timestamp specified in seconds/nanoseconds to a string.
/// Converts a field mask specified by paths to a string.
/// </summary>
/// <remarks>
/// If the value is a normalized duration in the range described in <c>field_mask.proto</c>,
@ -55,7 +58,7 @@ namespace Google.Protobuf.WellKnownTypes
/// <exception cref="InvalidOperationException">The represented field mask is invalid, and <paramref name="diagnosticOnly"/> is <c>false</c>.</exception>
internal static string ToJson(IList<string> paths, bool diagnosticOnly)
{
var firstInvalid = paths.FirstOrDefault(p => !ValidatePath(p));
var firstInvalid = paths.FirstOrDefault(p => !IsPathValid(p));
if (firstInvalid == null)
{
var writer = new StringWriter();
@ -84,11 +87,103 @@ namespace Google.Protobuf.WellKnownTypes
}
}
/// <summary>
/// Returns a string representation of this <see cref="FieldMask"/> for diagnostic purposes.
/// </summary>
/// <remarks>
/// Normally the returned value will be a JSON string value (including leading and trailing quotes) but
/// when the value is non-normalized or out of range, a JSON object representation will be returned
/// instead, including a warning. This is to avoid exceptions being thrown when trying to
/// diagnose problems - the regular JSON formatter will still throw an exception for non-normalized
/// values.
/// </remarks>
/// <returns>A string representation of this value.</returns>
public string ToDiagnosticString()
{
return ToJson(Paths, true);
}
/// <summary>
/// Parses from a string to a FieldMask.
/// </summary>
public static FieldMask FromString(string value)
{
return FromStringEnumerable<Empty>(new List<string>(value.Split(FIELD_PATH_SEPARATOR)));
}
/// <summary>
/// Parses from a string to a FieldMask and validates all field paths.
/// </summary>
/// <typeparam name="T">The type to validate the field paths against.</typeparam>
public static FieldMask FromString<T>(string value) where T : IMessage
{
return FromStringEnumerable<T>(new List<string>(value.Split(FIELD_PATH_SEPARATOR)));
}
/// <summary>
/// Constructs a FieldMask for a list of field paths in a certain type.
/// </summary>
/// <typeparam name="T">The type to validate the field paths against.</typeparam>
public static FieldMask FromStringEnumerable<T>(IEnumerable<string> paths) where T : IMessage
{
var mask = new FieldMask();
foreach (var path in paths)
{
if (path.Length == 0)
{
// Ignore empty field paths.
continue;
}
if (typeof(T) != typeof(Empty)
&& !IsValid<T>(path))
{
throw new InvalidProtocolBufferException(path + " is not a valid path for " + typeof(T));
}
mask.Paths.Add(path);
}
return mask;
}
/// <summary>
/// Constructs a FieldMask from the passed field numbers.
/// </summary>
/// <typeparam name="T">The type to validate the field paths against.</typeparam>
public static FieldMask FromFieldNumbers<T>(params int[] fieldNumbers) where T : IMessage
{
return FromFieldNumbers<T>((IEnumerable<int>)fieldNumbers);
}
/// <summary>
/// Constructs a FieldMask from the passed field numbers.
/// </summary>
/// <typeparam name="T">The type to validate the field paths against.</typeparam>
public static FieldMask FromFieldNumbers<T>(IEnumerable<int> fieldNumbers) where T : IMessage
{
var descriptor = Activator.CreateInstance<T>().Descriptor;
var mask = new FieldMask();
foreach (var fieldNumber in fieldNumbers)
{
var field = descriptor.FindFieldByNumber(fieldNumber);
if (field == null)
{
throw new ArgumentNullException($"{fieldNumber} is not a valid field number for {descriptor.Name}");
}
mask.Paths.Add(field.Name);
}
return mask;
}
/// <summary>
/// Checks whether the given path is valid for a field mask.
/// </summary>
/// <returns>true if the path is valid; false otherwise</returns>
private static bool ValidatePath(string input)
private static bool IsPathValid(string input)
{
for (int i = 0; i < input.Length; i++)
{
@ -110,19 +205,166 @@ namespace Google.Protobuf.WellKnownTypes
}
/// <summary>
/// Returns a string representation of this <see cref="FieldMask"/> for diagnostic purposes.
/// Checks whether paths in a given fields mask are valid.
/// </summary>
/// <remarks>
/// Normally the returned value will be a JSON string value (including leading and trailing quotes) but
/// when the value is non-normalized or out of range, a JSON object representation will be returned
/// instead, including a warning. This is to avoid exceptions being thrown when trying to
/// diagnose problems - the regular JSON formatter will still throw an exception for non-normalized
/// values.
/// </remarks>
/// <returns>A string representation of this value.</returns>
public string ToDiagnosticString()
/// <typeparam name="T">The type to validate the field paths against.</typeparam>
public static bool IsValid<T>(FieldMask fieldMask) where T : IMessage
{
return ToJson(Paths, true);
var descriptor = Activator.CreateInstance<T>().Descriptor;
return IsValid(descriptor, fieldMask);
}
/// <summary>
/// Checks whether paths in a given fields mask are valid.
/// </summary>
public static bool IsValid(MessageDescriptor descriptor, FieldMask fieldMask)
{
foreach (var path in fieldMask.Paths)
{
if (!IsValid(descriptor, path))
{
return false;
}
}
return true;
}
/// <summary>
/// Checks whether a given field path is valid.
/// </summary>
/// <typeparam name="T">The type to validate the field paths against.</typeparam>
public static bool IsValid<T>(string path) where T : IMessage
{
var descriptor = Activator.CreateInstance<T>().Descriptor;
return IsValid(descriptor, path);
}
/// <summary>
/// Checks whether paths in a given fields mask are valid.
/// </summary>
public static bool IsValid(MessageDescriptor descriptor, string path)
{
var parts = path.Split(FIELD_SEPARATOR_REGEX);
if (parts.Length == 0)
{
return false;
}
foreach (var name in parts)
{
var field = descriptor?.FindFieldByName(name);
if (field == null)
{
return false;
}
if (!field.IsRepeated
&& field.FieldType == FieldType.Message)
{
descriptor = field.MessageType;
}
else
{
descriptor = null;
}
}
return true;
}
/// <summary>
/// Converts this FieldMask to its canonical form. In the canonical form of a
/// FieldMask, all field paths are sorted alphabetically and redundant field
/// paths are removed.
/// </summary>
public FieldMask Normalize()
{
return new FieldMaskTree(this).ToFieldMask();
}
/// <summary>
/// Creates a union of two or more FieldMasks.
/// </summary>
public FieldMask Union(params FieldMask[] otherMasks)
{
var maskTree = new FieldMaskTree(this);
foreach (var mask in otherMasks)
{
maskTree.MergeFromFieldMask(mask);
}
return maskTree.ToFieldMask();
}
/// <summary>
/// Calculates the intersection of two FieldMasks.
/// </summary>
public FieldMask Intersection(FieldMask additionalMask)
{
var tree = new FieldMaskTree(this);
var result = new FieldMaskTree();
foreach (var path in additionalMask.Paths)
{
tree.IntersectFieldPath(path, result);
}
return result.ToFieldMask();
}
/// <summary>
/// Merges fields specified by this FieldMask from one message to another with the
/// specified merge options.
/// </summary>
public void Merge(IMessage source, IMessage destination, MergeOptions options)
{
new FieldMaskTree(this).Merge(source, destination, options);
}
/// <summary>
/// Merges fields specified by this FieldMask from one message to another.
/// </summary>
public void Merge(IMessage source, IMessage destination)
{
Merge(source, destination, new MergeOptions());
}
/// <summary>
/// Options to customize merging behavior.
/// </summary>
public sealed class MergeOptions
{
/// <summary>
/// Whether to replace message fields(i.e., discard existing content in
/// destination message fields) when merging.
/// Default behavior is to merge the source message field into the
/// destination message field.
/// </summary>
public bool ReplaceMessageFields { get; set; } = false;
/// <summary>
/// Whether to replace repeated fields (i.e., discard existing content in
/// destination repeated fields) when merging.
/// Default behavior is to append elements from source repeated field to the
/// destination repeated field.
/// </summary>
public bool ReplaceRepeatedFields { get; set; } = false;
/// <summary>
/// Whether to replace primitive (non-repeated and non-message) fields in
/// destination message fields with the source primitive fields (i.e., if the
/// field is set in the source, the value is copied to the
/// destination; if the field is unset in the source, the field is cleared
/// from the destination) when merging.
///
/// Default behavior is to always set the value of the source primitive
/// field to the destination primitive field, and if the source field is
/// unset, the default value of the source field is copied to the
/// destination.
/// </summary>
public bool ReplacePrimitiveFields { get; set; } = false;
}
}
}