From 9c22b889a06eaa80615643d086c7611f0dc2990c Mon Sep 17 00:00:00 2001 From: Quin Lynch Date: Tue, 18 Jul 2023 12:26:28 -0300 Subject: [PATCH 1/2] init --- .../Info/EdgeDBTypeDeserializerInfo.cs | 48 +++++++++++++++-- .../Binary/Builders/TypeBuilder.cs | 5 ++ .../Builders/Wrappers/FSharpOptionWrapper.cs | 54 +++++++++++++++++++ .../Binary/Builders/Wrappers/IWrapper.cs | 51 ++++++++++++++++++ .../Builders/Wrappers/NullableWrapper.cs | 31 +++++++++++ 5 files changed, 184 insertions(+), 5 deletions(-) create mode 100644 src/EdgeDB.Net.Driver/Binary/Builders/Wrappers/FSharpOptionWrapper.cs create mode 100644 src/EdgeDB.Net.Driver/Binary/Builders/Wrappers/IWrapper.cs create mode 100644 src/EdgeDB.Net.Driver/Binary/Builders/Wrappers/NullableWrapper.cs diff --git a/src/EdgeDB.Net.Driver/Binary/Builders/Info/EdgeDBTypeDeserializerInfo.cs b/src/EdgeDB.Net.Driver/Binary/Builders/Info/EdgeDBTypeDeserializerInfo.cs index 47518d3a..b4fedd9b 100644 --- a/src/EdgeDB.Net.Driver/Binary/Builders/Info/EdgeDBTypeDeserializerInfo.cs +++ b/src/EdgeDB.Net.Driver/Binary/Builders/Info/EdgeDBTypeDeserializerInfo.cs @@ -1,8 +1,11 @@ using EdgeDB.Binary; +using EdgeDB.Binary.Builders.Wrappers; using EdgeDB.DataTypes; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -45,22 +48,52 @@ private ObjectActivator? Activator private TypeDeserializerFactory _factory; + private IWrapper? _wrapper; + public EdgeDBTypeDeserializeInfo(Type type) { - _type = type; + IWrapper.TryGetWrapper(type, out _wrapper); + + _type = _wrapper?.GetInnerType(type) ?? type; - _factory = CreateDefaultFactory(); + var factory = CreateDefaultFactory(); EdgeDBTypeName = _type.GetCustomAttribute()?.Name ?? _type.Name; + + if(_wrapper is not null) + { + _factory = (ref ObjectEnumerator enumerator) => + { + var value = factory(ref enumerator); + return _wrapper.Wrap(type, value); + }; + } + else + { + _factory = factory; + } } public EdgeDBTypeDeserializeInfo(Type type, TypeDeserializerFactory factory) { - _type = type; + IWrapper.TryGetWrapper(type, out _wrapper); - _factory = factory; + _type = _wrapper?.GetInnerType(type) ?? type; EdgeDBTypeName = _type.GetCustomAttribute()?.Name ?? _type.Name; + + if (_wrapper is not null) + { + _factory = (ref ObjectEnumerator enumerator) => + { + var value = factory(ref enumerator); + return _wrapper.Wrap(type, value); + }; + } + else + { + _factory = factory; + } } private ObjectActivator? CreateActivator() @@ -71,7 +104,12 @@ public EdgeDBTypeDeserializeInfo(Type type, TypeDeserializerFactory factory) if (!ConstructorInfo.HasValue || ConstructorInfo.Value.EmptyConstructor is null) return null; - return Expression.Lambda(Expression.New(ConstructorInfo.Value.EmptyConstructor)).Compile(); + Expression newExp = Expression.New(ConstructorInfo.Value.EmptyConstructor); + + if (_type.IsValueType) + newExp = Expression.TypeAs(newExp, typeof(object)); + + return Expression.Lambda(newExp).Compile(); } public void AddOrUpdateChildren(EdgeDBTypeDeserializeInfo child) diff --git a/src/EdgeDB.Net.Driver/Binary/Builders/TypeBuilder.cs b/src/EdgeDB.Net.Driver/Binary/Builders/TypeBuilder.cs index b788a892..a3d8fd79 100644 --- a/src/EdgeDB.Net.Driver/Binary/Builders/TypeBuilder.cs +++ b/src/EdgeDB.Net.Driver/Binary/Builders/TypeBuilder.cs @@ -11,6 +11,7 @@ using System; using EdgeDB.Binary; using System.Diagnostics; +using EdgeDB.Binary.Builders.Wrappers; namespace EdgeDB { @@ -184,6 +185,10 @@ internal static bool IsValidObjectType(Type type) if (CodecBuilder.ContainsScalarCodec(type)) return false; + // if its a wrapping type we support, validate the inner type + if (IWrapper.TryGetWrapper(type, out var wrapper)) + return IsValidObjectType(wrapper.GetInnerType(type)); + if ( type.IsAssignableTo(typeof(IEnumerable)) && type.Assembly.GetName().Name!.StartsWith("System") && diff --git a/src/EdgeDB.Net.Driver/Binary/Builders/Wrappers/FSharpOptionWrapper.cs b/src/EdgeDB.Net.Driver/Binary/Builders/Wrappers/FSharpOptionWrapper.cs new file mode 100644 index 00000000..4a65ae56 --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Builders/Wrappers/FSharpOptionWrapper.cs @@ -0,0 +1,54 @@ +using EdgeDB.Utils.FSharp; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Builders.Wrappers +{ + internal sealed class FSharpOptionWrapper : IWrapper + { + private static ConstructorInfo? _valueOptionConstructor; + private static ConstructorInfo? _referenceOptionConstructor; + + public Type GetInnerType(Type wrapperType) + => wrapperType.GenericTypeArguments[0]; + + public bool IsWrapping(Type t) + => t.IsFSharpOption() || t.IsFSharpValueOption(); + + public object? Wrap(Type target, object? value) + { + if (target.IsFSharpValueOption()) + return WrapValueOption(target, value); + else if (target.IsFSharpOption()) + return WrapReferenceOption(target, value); + else + throw new NotSupportedException($"Unsupported wrapping type: {target}"); + } + + private static object? WrapReferenceOption(Type target, object? value) + { + if (value is null) + return null; + + return ( + _referenceOptionConstructor ??= target.GetConstructor(new Type[] { value.GetType() }) + ?? throw new EdgeDBException($"Failed to find constructor for {target}") + ).Invoke(new object?[] { value }); + } + + private static object? WrapValueOption(Type target, object? value) + { + if (value is null) + return ReflectionUtils.GetDefault(target); + + return ( + _valueOptionConstructor ??= target.GetConstructor(new Type[] { value.GetType() }) + ?? throw new EdgeDBException($"Failed to find constructor for {target}") + ).Invoke(new object?[] { value }); + } + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Builders/Wrappers/IWrapper.cs b/src/EdgeDB.Net.Driver/Binary/Builders/Wrappers/IWrapper.cs new file mode 100644 index 00000000..c2a2f50c --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Builders/Wrappers/IWrapper.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Builders.Wrappers +{ + internal interface IWrapper + { + static readonly ConcurrentBag _wrappers = new() + { + new FSharpOptionWrapper(), + new NullableWrapper() + }; + + static bool TryGetWrapper(Type t, [NotNullWhen(true)] out IWrapper? wrapper) + => (wrapper = _wrappers.FirstOrDefault(x => x.IsWrapping(t))) is not null; + + /// + /// Returns the inner (real) type that the wrapper wraps. For example: + /// <> would return ; + /// + /// The wrapper type to extract the inner type from. + /// The inner type. + Type GetInnerType(Type wrapperType); + + /// + /// Determines whether or not this wrapper can work with the provided + /// wrapping type. + /// + /// + /// The type that is checked for a wrapper that this instance can work with. + /// + /// + /// if the provided type matches the type this wrapper + /// can work with; otherwise . + /// + bool IsWrapping(Type t); + + /// + /// Wraps a given value into the target type. + /// + /// The target type of the wrap. + /// The value to wrap. + /// The wrapped value. + object? Wrap(Type target, object? value); + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Builders/Wrappers/NullableWrapper.cs b/src/EdgeDB.Net.Driver/Binary/Builders/Wrappers/NullableWrapper.cs new file mode 100644 index 00000000..f418a69d --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Builders/Wrappers/NullableWrapper.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Builders.Wrappers +{ + internal sealed class NullableWrapper : IWrapper + { + private ConstructorInfo? _constructor; + + public Type GetInnerType(Type wrapperType) + => wrapperType.GenericTypeArguments[0]; + + public bool IsWrapping(Type t) + => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>); + + public object? Wrap(Type target, object? value) + { + if (value is null) + return ReflectionUtils.GetDefault(target); + + return ( + _constructor ??= target.GetConstructor(new Type[] { value.GetType() }) + ?? throw new EdgeDBException($"Failed to find constructor for {target}") + ).Invoke(new object?[] { value }); + } + } +} From 1bc6d5a2fb46b07903c3cbe95a13f0b03e0c7ed8 Mon Sep 17 00:00:00 2001 From: Quin Lynch Date: Tue, 18 Jul 2023 12:49:07 -0300 Subject: [PATCH 2/2] Add test case for nullable returns --- tests/EdgeDB.Tests.Integration/ClientTests.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/EdgeDB.Tests.Integration/ClientTests.cs b/tests/EdgeDB.Tests.Integration/ClientTests.cs index 127b8f28..d45cbeb8 100644 --- a/tests/EdgeDB.Tests.Integration/ClientTests.cs +++ b/tests/EdgeDB.Tests.Integration/ClientTests.cs @@ -22,6 +22,19 @@ public ClientTests() _getToken = () => ClientProvider.GetTimeoutToken(); } + [TestMethod] + public async Task TestNullableReturns() + { + var result = await EdgeDB.QuerySingleAsync("select $arg", new { arg = 1L }); + + Assert.IsTrue(result.HasValue); + Assert.AreEqual(1L, result.Value); + + result = await EdgeDB.QuerySingleAsync("select $arg", new { arg = (long?)null }); + + Assert.IsFalse(result.HasValue); + } + [TestMethod] public async Task TestCommandLocks() {