Browse Source

Basic support for DynamicObject (#842)

Marko Lahma 4 years ago
parent
commit
cef5b55dde

+ 38 - 0
Jint.Tests/Runtime/InteropTests.cs

@@ -2318,5 +2318,43 @@ namespace Jint.Tests.Runtime
             Assert.True(_engine.Execute("animals[0].Type == 'Cat'").GetCompletionValue().AsBoolean());
             Assert.True(_engine.Execute("animals[0].Id == 1").GetCompletionValue().AsBoolean());
         }
+
+        [Fact]
+        public void CanAccessDynamicObject()
+        {
+            var test = new DynamicClass();
+            var engine = new Engine();
+
+            engine.SetValue("test", test);
+
+            Assert.Equal("a", engine.Execute("test.a").GetCompletionValue().AsString());
+            Assert.Equal("b", engine.Execute("test.b").GetCompletionValue().AsString());
+
+            engine.Execute("test.a = 5; test.b = 10;");
+
+            Assert.Equal(5, engine.Execute("test.a").GetCompletionValue().AsNumber());
+            Assert.Equal(10, engine.Execute("test.b").GetCompletionValue().AsNumber());
+        }
+        
+        private class DynamicClass : DynamicObject
+        {
+            private readonly Dictionary<string, object> _properties = new Dictionary<string, object>();
+            
+            public override bool TryGetMember(GetMemberBinder binder, out object result)
+            {
+                result = binder.Name;
+                if (_properties.TryGetValue(binder.Name, out var value))
+                {
+                    result = value;
+                }
+                return true;
+            }
+        
+            public override bool TrySetMember(SetMemberBinder binder, object value)
+            {
+                _properties[binder.Name] = value;
+                return true;
+            }
+        }
     }
 }

+ 6 - 0
Jint/Runtime/Interop/ObjectWrapper.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Collections;
 using System.Collections.Generic;
+using System.Dynamic;
 using System.Globalization;
 using System.Reflection;
 using System.Threading;
@@ -265,6 +266,11 @@ namespace Jint.Runtime.Interop
 
         private static ReflectionAccessor ResolvePropertyDescriptorFactory(Engine engine, Type type, string memberName)
         {
+            if (typeof(DynamicObject).IsAssignableFrom(type))
+            {
+                return new DynamicObjectAccessor(typeof(void), memberName);
+            }
+            
             var isNumber = uint.TryParse(memberName, out _);
 
             // we can always check indexer if there's one, and then fall back to properties if indexer returns null

+ 80 - 0
Jint/Runtime/Interop/Reflection/DynamicObjectAccessor.cs

@@ -0,0 +1,80 @@
+using System;
+using System.Dynamic;
+using System.Reflection;
+using Jint.Native;
+
+namespace Jint.Runtime.Interop.Reflection
+{
+    internal sealed class DynamicObjectAccessor : ReflectionAccessor
+    {
+        private readonly string _memberName;
+        private JintSetMemberBinder _setter;
+        private JintGetMemberBinder _getter;
+
+        public DynamicObjectAccessor(
+            Type memberType,
+            string memberName,
+            PropertyInfo indexer = null) : base(memberType, memberName, indexer)
+        {
+            _memberName = memberName;
+        }
+
+        public override bool Writable => true;
+
+        protected override object DoGetValue(object target)
+        {
+            var dynamicObject = (DynamicObject) target;
+            var getter = _getter ??= new JintGetMemberBinder(_memberName, ignoreCase: true);
+            dynamicObject.TryGetMember(getter, out var result);
+            return result;
+        }
+
+        protected override void DoSetValue(object target, object value)
+        {
+            var dynamicObject = (DynamicObject) target;
+            var setter = _setter ??= new JintSetMemberBinder(_memberName, ignoreCase: true);
+            dynamicObject.TrySetMember(setter, value);
+        }
+
+        protected override object ConvertValueToSet(Engine engine, object value)
+        {
+            // we expect value to be generally CLR type, convert when possible
+            return value switch
+            {
+                JsBoolean jsBoolean => jsBoolean._value ? JsBoolean.BoxedTrue : JsBoolean.BoxedFalse,
+                JsString jsString => jsString.ToString(),
+                JsNumber jsNumber => jsNumber._value,
+                JsNull => null,
+                JsUndefined => null,
+                _ => value
+            };
+        }
+
+        private sealed class JintGetMemberBinder : GetMemberBinder
+        {
+            public JintGetMemberBinder(string name, bool ignoreCase) : base(name, ignoreCase)
+            {
+            }
+
+            public override DynamicMetaObject FallbackGetMember(DynamicMetaObject target, DynamicMetaObject errorSuggestion)
+            {
+                throw new NotImplementedException(nameof(FallbackGetMember) + " not implemented");
+            }
+        }
+
+        private sealed class JintSetMemberBinder : SetMemberBinder
+        {
+            public JintSetMemberBinder(string name, bool ignoreCase) : base(name, ignoreCase)
+            {
+            }
+
+            public override DynamicMetaObject FallbackSetMember(
+                DynamicMetaObject target,
+                DynamicMetaObject value,
+                DynamicMetaObject errorSuggestion)
+            {
+                throw new NotImplementedException(nameof(FallbackSetMember) + " not implemented");
+            }
+        }
+    }
+}

+ 6 - 1
Jint/Runtime/Interop/Reflection/ReflectionAccessor.cs

@@ -101,7 +101,7 @@ namespace Jint.Runtime.Interop.Reflection
                 converted = value.ToObject();
                 if (converted != null && converted.GetType() != _memberType)
                 {
-                    converted = engine.ClrTypeConverter.Convert(converted, _memberType, CultureInfo.InvariantCulture);
+                    converted = ConvertValueToSet(engine, converted);
                 }
             }
 
@@ -115,6 +115,11 @@ namespace Jint.Runtime.Interop.Reflection
             }
         }
 
+        protected virtual object ConvertValueToSet(Engine engine, object value)
+        {
+            return engine.ClrTypeConverter.Convert(value, _memberType, CultureInfo.InvariantCulture);
+        }
+
         public virtual PropertyDescriptor CreatePropertyDescriptor(Engine engine, object target)
         {
             return new ReflectionDescriptor(engine, this, target);