From ea0aa5c143525dfb3447b8fbb4cbe9510e68d083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Tue, 5 Dec 2023 21:50:48 +0300 Subject: [PATCH] Fix #298 --- NiL.JS/Core/GlobalContext.cs | 27 ++++++++- NiL.JS/Core/Interop/DictionaryWrapper.cs | 73 +++++++++++++++++++++++- Tests/Core/Interop.cs | 58 +++++++++++++++++++ 3 files changed, 155 insertions(+), 3 deletions(-) diff --git a/NiL.JS/Core/GlobalContext.cs b/NiL.JS/Core/GlobalContext.cs index 0b8433d3f..ce433c740 100644 --- a/NiL.JS/Core/GlobalContext.cs +++ b/NiL.JS/Core/GlobalContext.cs @@ -29,6 +29,13 @@ public enum GlobalObjectsAssignMode DenyThrowException } + [Flags] + public enum MarshalinOptions + { + None = 0, + DictionaryAsObject = 1 << 0, + } + #if !NETCORE [Serializable] #endif @@ -47,6 +54,7 @@ public sealed class GlobalContext : Context public GlobalObjectsAssignMode GlobalObjectsAssignMode { get; set; } public JsonSerializersRegistry JsonSerializersRegistry { get; set; } public TimeZoneInfo CurrentTimeZone { get; set; } + public MarshalinOptions MarshalingOptions { get; set; } public GlobalContext() : this("") @@ -314,10 +322,10 @@ public Function GetGenericTypeSelector(IList types) return GetConstructor(type.MakeGenericType(parameters)); }); } - + public JSValue WrapValue(object value) { - if (value is null) + if (value is null) return JSValue.Null; return new ObjectWrapper(value, GetPrototype(value.GetType())); @@ -507,6 +515,21 @@ public JSValue ProxyValue(object value) _objectPrototype = GetPrototype(typeof(DictionaryWrapper)) }; } + else if ((MarshalingOptions & MarshalinOptions.DictionaryAsObject) != 0 + && value is IEnumerable + && (value is IDictionary || value.GetType().GetInterfaces().Any(x => x.IsConstructedGenericType && x.GetGenericTypeDefinition() == typeof(IDictionary<,>)))) + { + var dictionaryInterface = value.GetType().GetInterfaces().FirstOrDefault(x => x.IsConstructedGenericType && x.GetGenericTypeDefinition() == typeof(IDictionary<,>)); + if (dictionaryInterface != null) + { + var types = dictionaryInterface.GetGenericArguments(); + return DictionaryWrapper.Of(types[0], types[1], value); + } + else + { + return new DictionaryWrapper(new UntypedDictionaryWrapper(value as IDictionary)); + } + } else if (value is Task) { Task result; diff --git a/NiL.JS/Core/Interop/DictionaryWrapper.cs b/NiL.JS/Core/Interop/DictionaryWrapper.cs index f9a4b53ff..d8595f57d 100644 --- a/NiL.JS/Core/Interop/DictionaryWrapper.cs +++ b/NiL.JS/Core/Interop/DictionaryWrapper.cs @@ -1,15 +1,86 @@ -using System.Collections.Generic; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Reflection; using NiL.JS.Extensions; namespace NiL.JS.Core.Interop { public static class DictionaryWrapper { + internal static readonly MethodInfo OfMethod = typeof(DictionaryWrapper) + .GetMethod(nameof(Of)); + + internal static JSObject Of(Type keyType, Type valueType, object value) + { + return (JSObject)OfMethod + .MakeGenericMethod(keyType, valueType) + .Invoke(null, new[] { value })!; + } + public static DictionaryWrapper Of(IDictionary dictionary) => new DictionaryWrapper(dictionary); } + public sealed class UntypedDictionaryWrapper : IDictionary + { + private readonly IDictionary _target; + + public UntypedDictionaryWrapper(IDictionary target) + { + _target = target; + } + + public object this[object key] { get => _target[key]; set => _target[key] = value; } + + public ICollection Keys => _target.Keys.OfType().ToList(); + + public ICollection Values => _target.Values.OfType().ToList(); + + public int Count => _target.Count; + + public bool IsReadOnly => _target.IsReadOnly; + + public void Add(object key, object value) => _target.Add(key, value); + + public void Add(KeyValuePair item) => _target.Add(item.Key, item.Value); + + public void Clear() => _target.Clear(); + + public bool Contains(KeyValuePair item) => _target.Contains(item.Key) && _target[item.Key].Equals(item.Value); + + public bool ContainsKey(object key) => _target.Contains(key); + + public void CopyTo(KeyValuePair[] array, int arrayIndex) => _target.CopyTo(array, arrayIndex); + + public IEnumerator> GetEnumerator() + { + for (var e = _target.GetEnumerator(); e.MoveNext();) + yield return new KeyValuePair(e.Key, e.Value); + } + + public bool Remove(object key) + { + var contains = _target.Contains(key); + if (contains) + _target.Remove(key); + return contains; + } + + public bool Remove(KeyValuePair item) => Contains(item) && Remove(item.Key); + + public bool TryGetValue(object key, [MaybeNullWhen(false)] out object value) + { + var contains = _target.Contains(key); + value = _target[key]; + return contains; + } + + IEnumerator IEnumerable.GetEnumerator() => _target.GetEnumerator(); + } + public sealed class DictionaryWrapper : JSObject { private readonly IDictionary _target; diff --git a/Tests/Core/Interop.cs b/Tests/Core/Interop.cs index 43750b8e0..641ed1550 100644 --- a/Tests/Core/Interop.cs +++ b/Tests/Core/Interop.cs @@ -52,5 +52,63 @@ public void ImplementationOfClass() Assert.AreEqual(1, value0); Assert.AreEqual(2, value1); } + + [TestMethod] + public void DictionaryWrapperRaw() + { + var globalContext = new GlobalContext(); + globalContext.MarshalingOptions |= MarshalinOptions.DictionaryAsObject; + globalContext.ActivateInCurrentThread(); + + try + { + var dict = new Dictionary> { ["key0"] = new() { ["key1"] = 123 } }; + + var jsobj = globalContext.ProxyValue(dict); + + var test = jsobj["key0"]["key1"]; + + Assert.AreEqual(123, test.As()); + } + finally + { + globalContext.Deactivate(); + } + } + + [TestMethod] + public void DictionaryWrapperFromCode() + { + var globalContext = new GlobalContext(); + globalContext.MarshalingOptions |= MarshalinOptions.DictionaryAsObject; + + var dict = new Dictionary> { ["key0"] = new() { ["key1"] = 123 } }; + + var context = new Context(globalContext) + { + { "dict", globalContext.ProxyValue(dict) } + }; + + var test = context.Eval("dict.key0.key1"); + + Assert.AreEqual(123, test.As()); + } + + [TestMethod] + public void DictionaryWrapperDisabled() + { + var globalContext = new GlobalContext(); + + var dict = new Dictionary> { ["key0"] = new() { ["key1"] = 123 } }; + + var context = new Context(globalContext) + { + { "dict", globalContext.ProxyValue(dict) } + }; + + var test = context.Eval("dict.key0"); + + Assert.IsTrue(!test.Exists); + } } }