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
diff --git a/Makefile.am b/Makefile.am
index 6bafd10..0c86b93 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -93,6 +93,7 @@
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/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 \
diff --git a/csharp/src/Google.Protobuf.Test/FieldMaskTreeTest.cs b/csharp/src/Google.Protobuf.Test/FieldMaskTreeTest.cs
new file mode 100644
index 0000000..b0caab9
--- /dev/null
+++ b/csharp/src/Google.Protobuf.Test/FieldMaskTreeTest.cs
@@ -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);
+ }
+
+ }
+}
diff --git a/csharp/src/Google.Protobuf.Test/WellKnownTypes/FieldMaskTest.cs b/csharp/src/Google.Protobuf.Test/WellKnownTypes/FieldMaskTest.cs
index 1d9908b..5dc5035 100644
--- a/csharp/src/Google.Protobuf.Test/WellKnownTypes/FieldMaskTest.cs
+++ b/csharp/src/Google.Protobuf.Test/WellKnownTypes/FieldMaskTest.cs
@@ -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 @@
"{ \"@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);
+ }
}
}
diff --git a/csharp/src/Google.Protobuf/FieldMaskTree.cs b/csharp/src/Google.Protobuf/FieldMaskTree.cs
new file mode 100644
index 0000000..36c823b
--- /dev/null
+++ b/csharp/src/Google.Protobuf/FieldMaskTree.cs
@@ -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);
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/csharp/src/Google.Protobuf/JsonFormatter.cs b/csharp/src/Google.Protobuf/JsonFormatter.cs
index 4ae10d8..31fd887 100644
--- a/csharp/src/Google.Protobuf/JsonFormatter.cs
+++ b/csharp/src/Google.Protobuf/JsonFormatter.cs
@@ -271,7 +271,25 @@
}
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");
diff --git a/csharp/src/Google.Protobuf/WellKnownTypes/FieldMaskPartial.cs b/csharp/src/Google.Protobuf/WellKnownTypes/FieldMaskPartial.cs
index 4b0670f..27e513c 100644
--- a/csharp/src/Google.Protobuf/WellKnownTypes/FieldMaskPartial.cs
+++ b/csharp/src/Google.Protobuf/WellKnownTypes/FieldMaskPartial.cs
@@ -35,15 +35,18 @@
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 @@
/// <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();
@@ -85,10 +88,102 @@
}
/// <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 @@
}
/// <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;
}
}
}