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:
parent
6a51c03823
commit
80e530dabf
@ -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 \
|
||||
|
436
csharp/src/Google.Protobuf.Test/FieldMaskTreeTest.cs
Normal file
436
csharp/src/Google.Protobuf.Test/FieldMaskTreeTest.cs
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
364
csharp/src/Google.Protobuf/FieldMaskTree.cs
Normal file
364
csharp/src/Google.Protobuf/FieldMaskTree.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user