From b905a8f7ec0a1734f5d1c9b28c40c5f4cb24f570 Mon Sep 17 00:00:00 2001 From: Shravan Rajinikanth Date: Sun, 26 Mar 2017 13:12:33 -0500 Subject: [PATCH] Added dynamic property binding for VObject - Borrowed a few MIT licensed utils from Json.Net. --- Gameloop.Vdf/Gameloop.Vdf.csproj | 5 + Gameloop.Vdf/Utilities/CollectionUtils.cs | 68 +++ Gameloop.Vdf/Utilities/DynamicProxy.cs | 106 +++++ .../Utilities/DynamicProxyMetaObject.cs | 388 ++++++++++++++++++ Gameloop.Vdf/Utilities/ReflectionUtils.cs | 47 +++ Gameloop.Vdf/Utilities/TypeExtensions.cs | 60 +++ Gameloop.Vdf/VObject.cs | 37 +- Gameloop.Vdf/VToken.cs | 17 +- 8 files changed, 725 insertions(+), 3 deletions(-) create mode 100644 Gameloop.Vdf/Utilities/CollectionUtils.cs create mode 100644 Gameloop.Vdf/Utilities/DynamicProxy.cs create mode 100644 Gameloop.Vdf/Utilities/DynamicProxyMetaObject.cs create mode 100644 Gameloop.Vdf/Utilities/ReflectionUtils.cs create mode 100644 Gameloop.Vdf/Utilities/TypeExtensions.cs diff --git a/Gameloop.Vdf/Gameloop.Vdf.csproj b/Gameloop.Vdf/Gameloop.Vdf.csproj index bc7a1b4..4b7b2e2 100644 --- a/Gameloop.Vdf/Gameloop.Vdf.csproj +++ b/Gameloop.Vdf/Gameloop.Vdf.csproj @@ -40,6 +40,11 @@ + + + + + diff --git a/Gameloop.Vdf/Utilities/CollectionUtils.cs b/Gameloop.Vdf/Utilities/CollectionUtils.cs new file mode 100644 index 0000000..15cdd57 --- /dev/null +++ b/Gameloop.Vdf/Utilities/CollectionUtils.cs @@ -0,0 +1,68 @@ +#region License +// Copyright (c) 2007 James Newton-King +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +#endregion + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Gameloop.Vdf.Utilities +{ + internal static class CollectionUtils + { + /// + /// Adds the elements of the specified collection to the specified generic . + /// + /// The list to add to. + /// The collection of elements to add. + public static void AddRange(this IList initial, IEnumerable collection) + { + if (initial == null) + { + throw new ArgumentNullException(nameof(initial)); + } + + if (collection == null) + { + return; + } + + foreach (T value in collection) + { + initial.Add(value); + } + } + + public static T[] ArrayEmpty() + { + T[] array = Enumerable.Empty() as T[]; + Debug.Assert(array != null); + // Defensively guard against a version of Linq where Enumerable.Empty doesn't + // return an array, but throw in debug versions so a better strategy can be + // used if that ever happens. + return array ?? new T[0]; + } + } +} diff --git a/Gameloop.Vdf/Utilities/DynamicProxy.cs b/Gameloop.Vdf/Utilities/DynamicProxy.cs new file mode 100644 index 0000000..8b6928a --- /dev/null +++ b/Gameloop.Vdf/Utilities/DynamicProxy.cs @@ -0,0 +1,106 @@ +#region License +// Copyright (c) 2007 James Newton-King +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +#endregion + +using System.Collections.Generic; +using System.Dynamic; + +namespace Gameloop.Vdf.Utilities +{ + internal class DynamicProxy + { + public virtual IEnumerable GetDynamicMemberNames(T instance) + { + return CollectionUtils.ArrayEmpty(); + } + + public virtual bool TryBinaryOperation(T instance, BinaryOperationBinder binder, object arg, out object result) + { + result = null; + return false; + } + + public virtual bool TryConvert(T instance, ConvertBinder binder, out object result) + { + result = null; + return false; + } + + public virtual bool TryCreateInstance(T instance, CreateInstanceBinder binder, object[] args, out object result) + { + result = null; + return false; + } + + public virtual bool TryDeleteIndex(T instance, DeleteIndexBinder binder, object[] indexes) + { + return false; + } + + public virtual bool TryDeleteMember(T instance, DeleteMemberBinder binder) + { + return false; + } + + public virtual bool TryGetIndex(T instance, GetIndexBinder binder, object[] indexes, out object result) + { + result = null; + return false; + } + + public virtual bool TryGetMember(T instance, GetMemberBinder binder, out object result) + { + result = null; + return false; + } + + public virtual bool TryInvoke(T instance, InvokeBinder binder, object[] args, out object result) + { + result = null; + return false; + } + + public virtual bool TryInvokeMember(T instance, InvokeMemberBinder binder, object[] args, out object result) + { + result = null; + return false; + } + + public virtual bool TrySetIndex(T instance, SetIndexBinder binder, object[] indexes, object value) + { + return false; + } + + public virtual bool TrySetMember(T instance, SetMemberBinder binder, object value) + { + return false; + } + + public virtual bool TryUnaryOperation(T instance, UnaryOperationBinder binder, out object result) + { + result = null; + return false; + } + } +} diff --git a/Gameloop.Vdf/Utilities/DynamicProxyMetaObject.cs b/Gameloop.Vdf/Utilities/DynamicProxyMetaObject.cs new file mode 100644 index 0000000..0d999ed --- /dev/null +++ b/Gameloop.Vdf/Utilities/DynamicProxyMetaObject.cs @@ -0,0 +1,388 @@ +#region License +// Copyright (c) 2007 James Newton-King +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +#endregion + +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Linq; +using System.Linq.Expressions; + +namespace Gameloop.Vdf.Utilities +{ + internal sealed class DynamicProxyMetaObject : DynamicMetaObject + { + private readonly DynamicProxy _proxy; + + internal DynamicProxyMetaObject(Expression expression, T value, DynamicProxy proxy) + : base(expression, BindingRestrictions.Empty, value) + { + _proxy = proxy; + } + + private bool IsOverridden(string method) + { + return ReflectionUtils.IsMethodOverridden(_proxy.GetType(), typeof(DynamicProxy), method); + } + + public override DynamicMetaObject BindGetMember(GetMemberBinder binder) + { + return IsOverridden(nameof(DynamicProxy.TryGetMember)) + ? CallMethodWithResult(nameof(DynamicProxy.TryGetMember), binder, NoArgs, e => binder.FallbackGetMember(this, e)) + : base.BindGetMember(binder); + } + + public override DynamicMetaObject BindSetMember(SetMemberBinder binder, DynamicMetaObject value) + { + return IsOverridden(nameof(DynamicProxy.TrySetMember)) + ? CallMethodReturnLast(nameof(DynamicProxy.TrySetMember), binder, GetArgs(value), e => binder.FallbackSetMember(this, value, e)) + : base.BindSetMember(binder, value); + } + + public override DynamicMetaObject BindDeleteMember(DeleteMemberBinder binder) + { + return IsOverridden(nameof(DynamicProxy.TryDeleteMember)) + ? CallMethodNoResult(nameof(DynamicProxy.TryDeleteMember), binder, NoArgs, e => binder.FallbackDeleteMember(this, e)) + : base.BindDeleteMember(binder); + } + + public override DynamicMetaObject BindConvert(ConvertBinder binder) + { + return IsOverridden(nameof(DynamicProxy.TryConvert)) + ? CallMethodWithResult(nameof(DynamicProxy.TryConvert), binder, NoArgs, e => binder.FallbackConvert(this, e)) + : base.BindConvert(binder); + } + + public override DynamicMetaObject BindInvokeMember(InvokeMemberBinder binder, DynamicMetaObject[] args) + { + if (!IsOverridden(nameof(DynamicProxy.TryInvokeMember))) + { + return base.BindInvokeMember(binder, args); + } + + // + // Generate a tree like: + // + // { + // object result; + // TryInvokeMember(payload, out result) + // ? result + // : TryGetMember(payload, out result) + // ? FallbackInvoke(result) + // : fallbackResult + // } + // + // Then it calls FallbackInvokeMember with this tree as the + // "error", giving the language the option of using this + // tree or doing .NET binding. + // + Fallback fallback = e => binder.FallbackInvokeMember(this, args, e); + + return BuildCallMethodWithResult( + nameof(DynamicProxy.TryInvokeMember), + binder, + GetArgArray(args), + BuildCallMethodWithResult( + nameof(DynamicProxy.TryGetMember), + new GetBinderAdapter(binder), + NoArgs, + fallback(null), + e => binder.FallbackInvoke(e, args, null) + ), + null + ); + } + + public override DynamicMetaObject BindCreateInstance(CreateInstanceBinder binder, DynamicMetaObject[] args) + { + return IsOverridden(nameof(DynamicProxy.TryCreateInstance)) + ? CallMethodWithResult(nameof(DynamicProxy.TryCreateInstance), binder, GetArgArray(args), e => binder.FallbackCreateInstance(this, args, e)) + : base.BindCreateInstance(binder, args); + } + + public override DynamicMetaObject BindInvoke(InvokeBinder binder, DynamicMetaObject[] args) + { + return IsOverridden(nameof(DynamicProxy.TryInvoke)) + ? CallMethodWithResult(nameof(DynamicProxy.TryInvoke), binder, GetArgArray(args), e => binder.FallbackInvoke(this, args, e)) + : base.BindInvoke(binder, args); + } + + public override DynamicMetaObject BindBinaryOperation(BinaryOperationBinder binder, DynamicMetaObject arg) + { + return IsOverridden(nameof(DynamicProxy.TryBinaryOperation)) + ? CallMethodWithResult(nameof(DynamicProxy.TryBinaryOperation), binder, GetArgs(arg), e => binder.FallbackBinaryOperation(this, arg, e)) + : base.BindBinaryOperation(binder, arg); + } + + public override DynamicMetaObject BindUnaryOperation(UnaryOperationBinder binder) + { + return IsOverridden(nameof(DynamicProxy.TryUnaryOperation)) + ? CallMethodWithResult(nameof(DynamicProxy.TryUnaryOperation), binder, NoArgs, e => binder.FallbackUnaryOperation(this, e)) + : base.BindUnaryOperation(binder); + } + + public override DynamicMetaObject BindGetIndex(GetIndexBinder binder, DynamicMetaObject[] indexes) + { + return IsOverridden(nameof(DynamicProxy.TryGetIndex)) + ? CallMethodWithResult(nameof(DynamicProxy.TryGetIndex), binder, GetArgArray(indexes), e => binder.FallbackGetIndex(this, indexes, e)) + : base.BindGetIndex(binder, indexes); + } + + public override DynamicMetaObject BindSetIndex(SetIndexBinder binder, DynamicMetaObject[] indexes, DynamicMetaObject value) + { + return IsOverridden(nameof(DynamicProxy.TrySetIndex)) + ? CallMethodReturnLast(nameof(DynamicProxy.TrySetIndex), binder, GetArgArray(indexes, value), e => binder.FallbackSetIndex(this, indexes, value, e)) + : base.BindSetIndex(binder, indexes, value); + } + + public override DynamicMetaObject BindDeleteIndex(DeleteIndexBinder binder, DynamicMetaObject[] indexes) + { + return IsOverridden(nameof(DynamicProxy.TryDeleteIndex)) + ? CallMethodNoResult(nameof(DynamicProxy.TryDeleteIndex), binder, GetArgArray(indexes), e => binder.FallbackDeleteIndex(this, indexes, e)) + : base.BindDeleteIndex(binder, indexes); + } + + private delegate DynamicMetaObject Fallback(DynamicMetaObject errorSuggestion); + + private static Expression[] NoArgs => CollectionUtils.ArrayEmpty(); + + private static IEnumerable GetArgs(params DynamicMetaObject[] args) + { + return args.Select(arg => + { + Expression exp = arg.Expression; + return exp.Type.IsValueType() ? Expression.Convert(exp, typeof(object)) : exp; + }); + } + + private static Expression[] GetArgArray(DynamicMetaObject[] args) + { + return new[] { Expression.NewArrayInit(typeof(object), GetArgs(args)) }; + } + + private static Expression[] GetArgArray(DynamicMetaObject[] args, DynamicMetaObject value) + { + var exp = value.Expression; + return new[] + { + Expression.NewArrayInit(typeof(object), GetArgs(args)), + exp.Type.IsValueType() ? Expression.Convert(exp, typeof(object)) : exp + }; + } + + private static ConstantExpression Constant(DynamicMetaObjectBinder binder) + { + Type t = binder.GetType(); + while (!t.IsVisible()) + { + t = t.BaseType(); + } + return Expression.Constant(binder, t); + } + + /// + /// Helper method for generating a MetaObject which calls a + /// specific method on Dynamic that returns a result + /// + private DynamicMetaObject CallMethodWithResult(string methodName, DynamicMetaObjectBinder binder, IEnumerable args, Fallback fallback, Fallback fallbackInvoke = null) + { + // + // First, call fallback to do default binding + // This produces either an error or a call to a .NET member + // + DynamicMetaObject fallbackResult = fallback(null); + + return BuildCallMethodWithResult(methodName, binder, args, fallbackResult, fallbackInvoke); + } + + private DynamicMetaObject BuildCallMethodWithResult(string methodName, DynamicMetaObjectBinder binder, IEnumerable args, DynamicMetaObject fallbackResult, Fallback fallbackInvoke) + { + // + // Build a new expression like: + // { + // object result; + // TryGetMember(payload, out result) ? fallbackInvoke(result) : fallbackResult + // } + // + ParameterExpression result = Expression.Parameter(typeof(object), null); + + IList callArgs = new List(); + callArgs.Add(Expression.Convert(Expression, typeof(T))); + callArgs.Add(Constant(binder)); + callArgs.AddRange(args); + callArgs.Add(result); + + DynamicMetaObject resultMetaObject = new DynamicMetaObject(result, BindingRestrictions.Empty); + + // Need to add a conversion if calling TryConvert + if (binder.ReturnType != typeof(object)) + { + UnaryExpression convert = Expression.Convert(resultMetaObject.Expression, binder.ReturnType); + // will always be a cast or unbox + + resultMetaObject = new DynamicMetaObject(convert, resultMetaObject.Restrictions); + } + + if (fallbackInvoke != null) + { + resultMetaObject = fallbackInvoke(resultMetaObject); + } + + DynamicMetaObject callDynamic = new DynamicMetaObject( + Expression.Block( + new[] { result }, + Expression.Condition( + Expression.Call( + Expression.Constant(_proxy), + typeof(DynamicProxy).GetMethod(methodName), + callArgs + ), + resultMetaObject.Expression, + fallbackResult.Expression, + binder.ReturnType + ) + ), + GetRestrictions().Merge(resultMetaObject.Restrictions).Merge(fallbackResult.Restrictions) + ); + + return callDynamic; + } + + /// + /// Helper method for generating a MetaObject which calls a + /// specific method on Dynamic, but uses one of the arguments for + /// the result. + /// + private DynamicMetaObject CallMethodReturnLast(string methodName, DynamicMetaObjectBinder binder, IEnumerable args, Fallback fallback) + { + // + // First, call fallback to do default binding + // This produces either an error or a call to a .NET member + // + DynamicMetaObject fallbackResult = fallback(null); + + // + // Build a new expression like: + // { + // object result; + // TrySetMember(payload, result = value) ? result : fallbackResult + // } + // + ParameterExpression result = Expression.Parameter(typeof(object), null); + + IList callArgs = new List(); + callArgs.Add(Expression.Convert(Expression, typeof(T))); + callArgs.Add(Constant(binder)); + callArgs.AddRange(args); + callArgs[callArgs.Count - 1] = Expression.Assign(result, callArgs[callArgs.Count - 1]); + + return new DynamicMetaObject( + Expression.Block( + new[] { result }, + Expression.Condition( + Expression.Call( + Expression.Constant(_proxy), + typeof(DynamicProxy).GetMethod(methodName), + callArgs + ), + result, + fallbackResult.Expression, + typeof(object) + ) + ), + GetRestrictions().Merge(fallbackResult.Restrictions) + ); + } + + /// + /// Helper method for generating a MetaObject which calls a + /// specific method on Dynamic, but uses one of the arguments for + /// the result. + /// + private DynamicMetaObject CallMethodNoResult(string methodName, DynamicMetaObjectBinder binder, Expression[] args, Fallback fallback) + { + // + // First, call fallback to do default binding + // This produces either an error or a call to a .NET member + // + DynamicMetaObject fallbackResult = fallback(null); + + IList callArgs = new List(); + callArgs.Add(Expression.Convert(Expression, typeof(T))); + callArgs.Add(Constant(binder)); + callArgs.AddRange(args); + + // + // Build a new expression like: + // if (TryDeleteMember(payload)) { } else { fallbackResult } + // + return new DynamicMetaObject( + Expression.Condition( + Expression.Call( + Expression.Constant(_proxy), + typeof(DynamicProxy).GetMethod(methodName), + callArgs + ), + Expression.Empty(), + fallbackResult.Expression, + typeof(void) + ), + GetRestrictions().Merge(fallbackResult.Restrictions) + ); + } + + /// + /// Returns a Restrictions object which includes our current restrictions merged + /// with a restriction limiting our type + /// + private BindingRestrictions GetRestrictions() + { + return (Value == null && HasValue) + ? BindingRestrictions.GetInstanceRestriction(Expression, null) + : BindingRestrictions.GetTypeRestriction(Expression, LimitType); + } + + public override IEnumerable GetDynamicMemberNames() + { + return _proxy.GetDynamicMemberNames((T)Value); + } + + // It is okay to throw NotSupported from this binder. This object + // is only used by DynamicObject.GetMember--it is not expected to + // (and cannot) implement binding semantics. It is just so the DO + // can use the Name and IgnoreCase properties. + private sealed class GetBinderAdapter : GetMemberBinder + { + internal GetBinderAdapter(InvokeMemberBinder binder) : + base(binder.Name, binder.IgnoreCase) + { + } + + public override DynamicMetaObject FallbackGetMember(DynamicMetaObject target, DynamicMetaObject errorSuggestion) + { + throw new NotSupportedException(); + } + } + } +} diff --git a/Gameloop.Vdf/Utilities/ReflectionUtils.cs b/Gameloop.Vdf/Utilities/ReflectionUtils.cs new file mode 100644 index 0000000..f98f5b7 --- /dev/null +++ b/Gameloop.Vdf/Utilities/ReflectionUtils.cs @@ -0,0 +1,47 @@ +#region License +// Copyright (c) 2007 James Newton-King +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +#endregion + +using System; +using System.Linq; +using System.Reflection; + +namespace Gameloop.Vdf.Utilities +{ + internal static class ReflectionUtils + { + public static bool IsMethodOverridden(Type currentType, Type methodDeclaringType, string method) + { + bool isMethodOverriden = currentType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .Any(info => + info.Name == method && + // check that the method overrides the original on DynamicObjectProxy + info.DeclaringType != methodDeclaringType + && info.GetBaseDefinition().DeclaringType == methodDeclaringType + ); + + return isMethodOverriden; + } + } +} diff --git a/Gameloop.Vdf/Utilities/TypeExtensions.cs b/Gameloop.Vdf/Utilities/TypeExtensions.cs new file mode 100644 index 0000000..b5258f7 --- /dev/null +++ b/Gameloop.Vdf/Utilities/TypeExtensions.cs @@ -0,0 +1,60 @@ +#region License +// Copyright (c) 2007 James Newton-King +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +#endregion + +using System; +using System.Reflection; + +namespace Gameloop.Vdf.Utilities +{ + internal static class TypeExtensions + { + public static Type BaseType(this Type type) + { +#if HAVE_FULL_REFLECTION + return type.BaseType; +#else + return type.GetTypeInfo().BaseType; +#endif + } + + public static bool IsVisible(this Type type) + { +#if HAVE_FULL_REFLECTION + return type.IsVisible; +#else + return type.GetTypeInfo().IsVisible; +#endif + } + + public static bool IsValueType(this Type type) + { +#if HAVE_FULL_REFLECTION + return type.IsValueType; +#else + return type.GetTypeInfo().IsValueType; +#endif + } + } +} \ No newline at end of file diff --git a/Gameloop.Vdf/VObject.cs b/Gameloop.Vdf/VObject.cs index b419eb5..657cb7c 100644 --- a/Gameloop.Vdf/VObject.cs +++ b/Gameloop.Vdf/VObject.cs @@ -1,7 +1,10 @@ -using System; +using Gameloop.Vdf.Utilities; +using System; using System.Collections.Generic; using System.ComponentModel; +using System.Dynamic; using System.Linq; +using System.Linq.Expressions; namespace Gameloop.Vdf { @@ -187,5 +190,37 @@ public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listA } #endregion + + protected override DynamicMetaObject GetMetaObject(Expression parameter) + { + return new DynamicProxyMetaObject(parameter, this, new VObjectDynamicProxy()); + } + + private class VObjectDynamicProxy : DynamicProxy + { + public override bool TryGetMember(VObject instance, GetMemberBinder binder, out object result) + { + // result can be null + result = instance[binder.Name]; + return true; + } + + public override bool TrySetMember(VObject instance, SetMemberBinder binder, object value) + { + VToken v = value as VToken; + + // this can throw an error if value isn't a valid for a JValue + if (v == null) + v = new VValue(value); + + instance[binder.Name] = v; + return true; + } + + public override IEnumerable GetDynamicMemberNames(VObject instance) + { + return instance.Children().Select(p => p.Key); + } + } } } diff --git a/Gameloop.Vdf/VToken.cs b/Gameloop.Vdf/VToken.cs index 80ca376..8389d2e 100644 --- a/Gameloop.Vdf/VToken.cs +++ b/Gameloop.Vdf/VToken.cs @@ -1,9 +1,12 @@ -using System.Globalization; +using Gameloop.Vdf.Utilities; +using System.Dynamic; +using System.Globalization; using System.IO; +using System.Linq.Expressions; namespace Gameloop.Vdf { - public abstract class VToken + public abstract class VToken : IDynamicMetaObjectProvider { public VToken Parent { get; set; } public VToken Previous { get; set; } @@ -11,6 +14,16 @@ public abstract class VToken public abstract void WriteTo(VdfWriter writer); + protected virtual DynamicMetaObject GetMetaObject(Expression parameter) + { + return new DynamicProxyMetaObject(parameter, this, new DynamicProxy()); + } + + DynamicMetaObject IDynamicMetaObjectProvider.GetMetaObject(Expression parameter) + { + return GetMetaObject(parameter); + } + public override string ToString() { using (StringWriter stringWriter = new StringWriter(CultureInfo.InvariantCulture))