Add reflection support for proto3 optional fields
This is more involved than might be expected because the synthetic oneofs don't generate the properties we would usually expect to see.
This commit is contained in:
parent
bad9d753ae
commit
97737072f9
@ -32,6 +32,7 @@
|
||||
|
||||
using Google.Protobuf.TestProtos;
|
||||
using NUnit.Framework;
|
||||
using ProtobufUnittest;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -247,6 +248,7 @@ namespace Google.Protobuf.Reflection
|
||||
FieldDescriptor enumField = testAllTypesDescriptor.FindDescriptor<FieldDescriptor>("single_nested_enum");
|
||||
FieldDescriptor foreignMessageField = testAllTypesDescriptor.FindDescriptor<FieldDescriptor>("single_foreign_message");
|
||||
FieldDescriptor importMessageField = testAllTypesDescriptor.FindDescriptor<FieldDescriptor>("single_import_message");
|
||||
FieldDescriptor fieldInOneof = testAllTypesDescriptor.FindDescriptor<FieldDescriptor>("oneof_string");
|
||||
|
||||
Assert.AreEqual("single_int32", primitiveField.Name);
|
||||
Assert.AreEqual("protobuf_unittest3.TestAllTypes.single_int32",
|
||||
@ -268,6 +270,10 @@ namespace Google.Protobuf.Reflection
|
||||
Assert.AreEqual("single_import_message", importMessageField.Name);
|
||||
Assert.AreEqual(FieldType.Message, importMessageField.FieldType);
|
||||
Assert.AreEqual(importMessageDescriptor, importMessageField.MessageType);
|
||||
|
||||
// For a field in a regular onoef, ContainingOneof and RealContainingOneof should be the same.
|
||||
Assert.AreEqual("oneof_field", fieldInOneof.ContainingOneof.Name);
|
||||
Assert.AreSame(fieldInOneof.ContainingOneof, fieldInOneof.RealContainingOneof);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -318,6 +324,7 @@ namespace Google.Protobuf.Reflection
|
||||
public void OneofDescriptor()
|
||||
{
|
||||
OneofDescriptor descriptor = TestAllTypes.Descriptor.FindDescriptor<OneofDescriptor>("oneof_field");
|
||||
Assert.IsFalse(descriptor.IsSynthetic);
|
||||
Assert.AreEqual("oneof_field", descriptor.Name);
|
||||
Assert.AreEqual("protobuf_unittest3.TestAllTypes.oneof_field", descriptor.FullName);
|
||||
|
||||
@ -383,5 +390,48 @@ namespace Google.Protobuf.Reflection
|
||||
var importingDescriptor = TestProtos.OldGenerator.OldExtensions1Reflection.Descriptor;
|
||||
Assert.NotNull(importingDescriptor);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Proto3OptionalDescriptors()
|
||||
{
|
||||
var descriptor = TestProto3Optional.Descriptor;
|
||||
var field = descriptor.Fields[TestProto3Optional.OptionalInt32FieldNumber];
|
||||
Assert.NotNull(field.ContainingOneof);
|
||||
Assert.IsTrue(field.ContainingOneof.IsSynthetic);
|
||||
Assert.Null(field.RealContainingOneof);
|
||||
}
|
||||
|
||||
|
||||
[Test]
|
||||
public void SyntheticOneofReflection()
|
||||
{
|
||||
// Expect every oneof in TestProto3Optional to be synthetic
|
||||
var proto3OptionalDescriptor = TestProto3Optional.Descriptor;
|
||||
Assert.AreEqual(0, proto3OptionalDescriptor.RealOneofCount);
|
||||
foreach (var oneof in proto3OptionalDescriptor.Oneofs)
|
||||
{
|
||||
Assert.True(oneof.IsSynthetic);
|
||||
}
|
||||
|
||||
// Expect no oneof in the original proto3 unit test file to be synthetic.
|
||||
foreach (var descriptor in ProtobufTestMessages.Proto3.TestMessagesProto3Reflection.Descriptor.MessageTypes)
|
||||
{
|
||||
Assert.AreEqual(descriptor.Oneofs.Count, descriptor.RealOneofCount);
|
||||
foreach (var oneof in descriptor.Oneofs)
|
||||
{
|
||||
Assert.False(oneof.IsSynthetic);
|
||||
}
|
||||
}
|
||||
|
||||
// Expect no oneof in the original proto2 unit test file to be synthetic.
|
||||
foreach (var descriptor in ProtobufTestMessages.Proto2.TestMessagesProto2Reflection.Descriptor.MessageTypes)
|
||||
{
|
||||
Assert.AreEqual(descriptor.Oneofs.Count, descriptor.RealOneofCount);
|
||||
foreach (var oneof in descriptor.Oneofs)
|
||||
{
|
||||
Assert.False(oneof.IsSynthetic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using static Google.Protobuf.TestProtos.Proto2.UnittestExtensions;
|
||||
using ProtobufUnittest;
|
||||
|
||||
namespace Google.Protobuf.Reflection
|
||||
{
|
||||
@ -104,6 +105,21 @@ namespace Google.Protobuf.Reflection
|
||||
Assert.Throws<InvalidOperationException>(() => fields[TestProtos.TestAllTypes.SingleBoolFieldNumber].Accessor.HasValue(message));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HasValue_Proto3Optional()
|
||||
{
|
||||
IMessage message = new TestProto3Optional
|
||||
{
|
||||
OptionalInt32 = 0,
|
||||
LazyNestedMessage = new TestProto3Optional.Types.NestedMessage()
|
||||
};
|
||||
var fields = message.Descriptor.Fields;
|
||||
Assert.IsFalse(fields[TestProto3Optional.OptionalInt64FieldNumber].Accessor.HasValue(message));
|
||||
Assert.IsFalse(fields[TestProto3Optional.OptionalNestedMessageFieldNumber].Accessor.HasValue(message));
|
||||
Assert.IsTrue(fields[TestProto3Optional.LazyNestedMessageFieldNumber].Accessor.HasValue(message));
|
||||
Assert.IsTrue(fields[TestProto3Optional.OptionalInt32FieldNumber].Accessor.HasValue(message));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HasValue()
|
||||
{
|
||||
@ -225,6 +241,27 @@ namespace Google.Protobuf.Reflection
|
||||
Assert.AreEqual(0, mapMessage.MapStringString.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Clear_Proto3Optional()
|
||||
{
|
||||
TestProto3Optional message = new TestProto3Optional
|
||||
{
|
||||
OptionalInt32 = 0,
|
||||
OptionalNestedMessage = new TestProto3Optional.Types.NestedMessage()
|
||||
};
|
||||
var primitiveField = TestProto3Optional.Descriptor.Fields[TestProto3Optional.OptionalInt32FieldNumber];
|
||||
var messageField = TestProto3Optional.Descriptor.Fields[TestProto3Optional.OptionalNestedMessageFieldNumber];
|
||||
|
||||
Assert.True(message.HasOptionalInt32);
|
||||
Assert.NotNull(message.OptionalNestedMessage);
|
||||
|
||||
primitiveField.Accessor.Clear(message);
|
||||
messageField.Accessor.Clear(message);
|
||||
|
||||
Assert.False(message.HasOptionalInt32);
|
||||
Assert.Null(message.OptionalNestedMessage);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FieldDescriptor_ByName()
|
||||
{
|
||||
|
@ -58,6 +58,12 @@ namespace Google.Protobuf.Reflection
|
||||
/// </summary>
|
||||
public OneofDescriptor ContainingOneof { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the oneof containing this field if it's a "real" oneof, or <c>null</c> if either this
|
||||
/// field is not part of a oneof, or the oneof is synthetic.
|
||||
/// </summary>
|
||||
public OneofDescriptor RealContainingOneof => ContainingOneof?.IsSynthetic == false ? ContainingOneof : null;
|
||||
|
||||
/// <summary>
|
||||
/// The effective JSON name for this field. This is usually the lower-camel-cased form of the field name,
|
||||
/// but can be overridden using the <c>json_name</c> option in the .proto file.
|
||||
|
@ -59,7 +59,8 @@ namespace Google.Protobuf.Reflection
|
||||
object GetValue(IMessage message);
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the field in the specified message is set. For proto3 fields, this throws an <see cref="InvalidOperationException"/>
|
||||
/// Indicates whether the field in the specified message is set.
|
||||
/// For proto3 fields that aren't explicitly optional, this throws an <see cref="InvalidOperationException"/>
|
||||
/// </summary>
|
||||
bool HasValue(IMessage message);
|
||||
|
||||
|
@ -80,6 +80,20 @@ namespace Google.Protobuf.Reflection
|
||||
(oneof, index) =>
|
||||
new OneofDescriptor(oneof, file, this, index, generatedCodeInfo?.OneofNames[index]));
|
||||
|
||||
int syntheticOneofCount = 0;
|
||||
foreach (var oneof in Oneofs)
|
||||
{
|
||||
if (oneof.IsSynthetic)
|
||||
{
|
||||
syntheticOneofCount++;
|
||||
}
|
||||
else if (syntheticOneofCount != 0)
|
||||
{
|
||||
throw new ArgumentException("All synthetic oneofs should come after real oneofs");
|
||||
}
|
||||
}
|
||||
RealOneofCount = Oneofs.Count - syntheticOneofCount;
|
||||
|
||||
NestedTypes = DescriptorUtil.ConvertAndMakeReadOnly(
|
||||
proto.NestedType,
|
||||
(type, index) =>
|
||||
@ -234,9 +248,19 @@ namespace Google.Protobuf.Reflection
|
||||
|
||||
/// <value>
|
||||
/// An unmodifiable list of the "oneof" field collections in this message type.
|
||||
/// All "real" oneofs (where <see cref="OneofDescriptor.IsSynthetic"/> returns false)
|
||||
/// come before synthetic ones.
|
||||
/// </value>
|
||||
public IList<OneofDescriptor> Oneofs { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of real "oneof" descriptors in this message type. Every element in <see cref="Oneofs"/>
|
||||
/// with an index less than this will have a <see cref="OneofDescriptor.IsSynthetic"/> property value
|
||||
/// of <c>false</c>; every element with an index greater than or equal to this will have a
|
||||
/// <see cref="OneofDescriptor.IsSynthetic"/> property value of <c>true</c>.
|
||||
/// </summary>
|
||||
public int RealOneofCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Finds a field by field name.
|
||||
/// </summary>
|
||||
|
@ -43,19 +43,31 @@ namespace Google.Protobuf.Reflection
|
||||
{
|
||||
private readonly Func<IMessage, int> caseDelegate;
|
||||
private readonly Action<IMessage> clearDelegate;
|
||||
private OneofDescriptor descriptor;
|
||||
|
||||
internal OneofAccessor(PropertyInfo caseProperty, MethodInfo clearMethod, OneofDescriptor descriptor)
|
||||
private OneofAccessor(OneofDescriptor descriptor, Func<IMessage, int> caseDelegate, Action<IMessage> clearDelegate)
|
||||
{
|
||||
if (!caseProperty.CanRead)
|
||||
{
|
||||
throw new ArgumentException("Cannot read from property");
|
||||
Descriptor = descriptor;
|
||||
this.caseDelegate = caseDelegate;
|
||||
this.clearDelegate = clearDelegate;
|
||||
}
|
||||
this.descriptor = descriptor;
|
||||
caseDelegate = ReflectionUtil.CreateFuncIMessageInt32(caseProperty.GetGetMethod());
|
||||
|
||||
this.descriptor = descriptor;
|
||||
clearDelegate = ReflectionUtil.CreateActionIMessage(clearMethod);
|
||||
internal static OneofAccessor ForRegularOneof(
|
||||
OneofDescriptor descriptor,
|
||||
PropertyInfo caseProperty,
|
||||
MethodInfo clearMethod) =>
|
||||
new OneofAccessor(
|
||||
descriptor,
|
||||
ReflectionUtil.CreateFuncIMessageInt32(caseProperty.GetGetMethod()),
|
||||
ReflectionUtil.CreateActionIMessage(clearMethod));
|
||||
|
||||
internal static OneofAccessor ForSyntheticOneof(OneofDescriptor descriptor)
|
||||
{
|
||||
// Note: descriptor.Fields will be null when this method is called, because we haven't
|
||||
// cross-linked yet. But by the time the delgates are called by user code, all will be
|
||||
// well. (That's why we capture the descriptor itself rather than a field.)
|
||||
return new OneofAccessor(descriptor,
|
||||
message => descriptor.Fields[0].Accessor.HasValue(message) ? descriptor.Fields[0].FieldNumber : 0,
|
||||
message => descriptor.Fields[0].Accessor.Clear(message));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -64,15 +76,12 @@ namespace Google.Protobuf.Reflection
|
||||
/// <value>
|
||||
/// The descriptor of the oneof.
|
||||
/// </value>
|
||||
public OneofDescriptor Descriptor { get { return descriptor; } }
|
||||
public OneofDescriptor Descriptor { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Clears the oneof in the specified message.
|
||||
/// </summary>
|
||||
public void Clear(IMessage message)
|
||||
{
|
||||
clearDelegate(message);
|
||||
}
|
||||
public void Clear(IMessage message) => clearDelegate(message);
|
||||
|
||||
/// <summary>
|
||||
/// Indicates which field in the oneof is set for specified message
|
||||
@ -80,11 +89,9 @@ namespace Google.Protobuf.Reflection
|
||||
public FieldDescriptor GetCaseFieldDescriptor(IMessage message)
|
||||
{
|
||||
int fieldNumber = caseDelegate(message);
|
||||
if (fieldNumber > 0)
|
||||
{
|
||||
return descriptor.ContainingType.FindFieldByNumber(fieldNumber);
|
||||
}
|
||||
return null;
|
||||
return fieldNumber > 0
|
||||
? Descriptor.ContainingType.FindFieldByNumber(fieldNumber)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Google.Protobuf.Collections;
|
||||
using Google.Protobuf.Compatibility;
|
||||
|
||||
@ -54,8 +55,13 @@ namespace Google.Protobuf.Reflection
|
||||
{
|
||||
this.proto = proto;
|
||||
containingType = parent;
|
||||
|
||||
file.DescriptorPool.AddSymbol(this);
|
||||
|
||||
// It's useful to determine whether or not this is a synthetic oneof before cross-linking. That means
|
||||
// diving into the proto directly rather than using FieldDescriptor, but that's okay.
|
||||
var firstFieldInOneof = parent.Proto.Field.FirstOrDefault(fieldProto => fieldProto.OneofIndex == index);
|
||||
IsSynthetic = firstFieldInOneof?.Proto3Optional ?? false;
|
||||
|
||||
accessor = CreateAccessor(clrName);
|
||||
}
|
||||
|
||||
@ -83,6 +89,12 @@ namespace Google.Protobuf.Reflection
|
||||
/// </value>
|
||||
public IList<FieldDescriptor> Fields { get { return fields; } }
|
||||
|
||||
/// <summary>
|
||||
/// Returns <c>true</c> if this oneof is a synthetic oneof containing a proto3 optional field;
|
||||
/// <c>false</c> otherwise.
|
||||
/// </summary>
|
||||
public bool IsSynthetic { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets an accessor for reflective access to the values associated with the oneof
|
||||
/// in a particular message.
|
||||
@ -146,18 +158,28 @@ namespace Google.Protobuf.Reflection
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (IsSynthetic)
|
||||
{
|
||||
return OneofAccessor.ForSyntheticOneof(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
var caseProperty = containingType.ClrType.GetProperty(clrName + "Case");
|
||||
if (caseProperty == null)
|
||||
{
|
||||
throw new DescriptorValidationException(this, $"Property {clrName}Case not found in {containingType.ClrType}");
|
||||
}
|
||||
if (!caseProperty.CanRead)
|
||||
{
|
||||
throw new ArgumentException($"Cannot read from property {clrName}Case in {containingType.ClrType}");
|
||||
}
|
||||
var clearMethod = containingType.ClrType.GetMethod("Clear" + clrName);
|
||||
if (clearMethod == null)
|
||||
{
|
||||
throw new DescriptorValidationException(this, $"Method Clear{clrName} not found in {containingType.ClrType}");
|
||||
}
|
||||
|
||||
return new OneofAccessor(caseProperty, clearMethod, this);
|
||||
return OneofAccessor.ForRegularOneof(this, caseProperty, clearMethod);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -57,10 +57,11 @@ namespace Google.Protobuf.Reflection
|
||||
throw new ArgumentException("Not all required properties/methods available");
|
||||
}
|
||||
setValueDelegate = ReflectionUtil.CreateActionIMessageObject(property.GetSetMethod());
|
||||
if (descriptor.File.Syntax == Syntax.Proto3)
|
||||
if (descriptor.File.Syntax == Syntax.Proto3 && !descriptor.Proto.Proto3Optional)
|
||||
{
|
||||
hasDelegate = message => {
|
||||
throw new InvalidOperationException("HasValue is not implemented for proto3 fields");
|
||||
hasDelegate = message =>
|
||||
{
|
||||
throw new InvalidOperationException("HasValue is not implemented for non-optional proto3 fields");
|
||||
};
|
||||
var clrType = property.PropertyType;
|
||||
|
||||
@ -73,19 +74,32 @@ namespace Google.Protobuf.Reflection
|
||||
clearDelegate = message => SetValue(message, defaultValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
// For message fields, just compare with null and set to null.
|
||||
// For primitive fields, use the Has/Clear methods.
|
||||
|
||||
if (descriptor.FieldType == FieldType.Message)
|
||||
{
|
||||
hasDelegate = message => GetValue(message) != null;
|
||||
clearDelegate = message => SetValue(message, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
MethodInfo hasMethod = property.DeclaringType.GetRuntimeProperty("Has" + property.Name).GetMethod;
|
||||
if (hasMethod == null) {
|
||||
if (hasMethod == null)
|
||||
{
|
||||
throw new ArgumentException("Not all required properties/methods are available");
|
||||
}
|
||||
hasDelegate = ReflectionUtil.CreateFuncIMessageBool(hasMethod);
|
||||
MethodInfo clearMethod = property.DeclaringType.GetRuntimeMethod("Clear" + property.Name, ReflectionUtil.EmptyTypes);
|
||||
if (clearMethod == null) {
|
||||
if (clearMethod == null)
|
||||
{
|
||||
throw new ArgumentException("Not all required properties/methods are available");
|
||||
}
|
||||
clearDelegate = ReflectionUtil.CreateActionIMessage(clearMethod);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Clear(IMessage message)
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user