From 32e9d78c4d278c231f42690e743822bd75811ceb Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Tue, 16 Nov 2021 17:44:52 +0200 Subject: [PATCH] Improve stringify for JObject --- .../Runtime/InteropTests.NewtonsoftJson.cs | 64 +++++++++++++++++++ Jint/Runtime/Interop/ObjectWrapper.cs | 49 +++++--------- Jint/Runtime/Interop/TypeDescriptor.cs | 12 ++++ 3 files changed, 91 insertions(+), 34 deletions(-) diff --git a/Jint.Tests/Runtime/InteropTests.NewtonsoftJson.cs b/Jint.Tests/Runtime/InteropTests.NewtonsoftJson.cs index 39b501a35c..1743df4921 100644 --- a/Jint.Tests/Runtime/InteropTests.NewtonsoftJson.cs +++ b/Jint.Tests/Runtime/InteropTests.NewtonsoftJson.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Jint.Native; using Jint.Runtime; using Jint.Runtime.Interop; using Newtonsoft.Json.Linq; @@ -90,5 +91,68 @@ public void EngineShouldStringifyAnJObjectListWithValuesCorrectly() Assert.Equal("{\"Current\":null}", result); } + + [Fact] + public void EngineShouldStringifyJObjectFromObjectListWithValuesCorrectly() + { + var engine = new Engine(options => + { + options.AddObjectConverter(JTokenConverter.Instance); + }); + + var source = new dynamic[] + { + new { Text = "Text1", Value = 1 }, + new { Text = "Text2", Value = 2 } + }; + + engine.SetValue("testSubject", source.Select(x => JObject.FromObject(x)).ToList()); + var fromEngine = engine.Evaluate("return JSON.stringify(testSubject);"); + var result = fromEngine.ToString(); + + Assert.Equal("[{\"Text\":\"Text1\",\"Value\":1},{\"Text\":\"Text2\",\"Value\":2}]", result); + } + + private sealed class JTokenConverter : IObjectConverter + { + public static readonly IObjectConverter Instance = new JTokenConverter(); + + public bool TryConvert(Engine engine, object value, out JsValue result) + { + result = null; + + if (!(value is JToken token)) + { + return false; + } + + switch (token.Type) + { + case JTokenType.Integer: + result = JsNumber.Create(token.Value()); + break; + case JTokenType.Float: + result = JsNumber.Create(token.Value()); + break; + case JTokenType.String: + result = JsString.Create(token.Value()); + break; + case JTokenType.Boolean: + result = token.Value() ? JsBoolean.True : JsBoolean.False; + break; + case JTokenType.Null: + result = JsValue.Null; + break; + case JTokenType.Undefined: + result = JsValue.Undefined; + break; + case JTokenType.Date: + result = engine.Realm.Intrinsics.Date.Construct(token.Value()); + break; + } + + return !(result is null); + } + } } } \ No newline at end of file diff --git a/Jint/Runtime/Interop/ObjectWrapper.cs b/Jint/Runtime/Interop/ObjectWrapper.cs index c9a8cdcd47..24eca7a833 100644 --- a/Jint/Runtime/Interop/ObjectWrapper.cs +++ b/Jint/Runtime/Interop/ObjectWrapper.cs @@ -98,37 +98,6 @@ public override JsValue Get(JsValue property, JsValue receiver) return Undefined; } - if (property is JsString stringKey) - { - var member = stringKey.ToString(); - - // expando object for instance - if (_typeDescriptor.IsStringKeyedGenericDictionary) - { - if (_typeDescriptor.TryGetValue(Target, member, out var value)) - { - return FromObject(_engine, value); - } - } - - var result = Engine.Options.Interop.MemberAccessor?.Invoke(Engine, Target, member); - if (result is not null) - { - return result; - } - - if (_properties is null || !_properties.ContainsKey(member)) - { - // can try utilize fast path - var accessor = _engine.Options.Interop.TypeResolver.GetAccessor(_engine, Target.GetType(), member); - var value = accessor.GetValue(_engine, Target); - if (value is not null) - { - return FromObject(_engine, value); - } - } - } - return base.Get(property, receiver); } @@ -152,9 +121,10 @@ private IEnumerable EnumerateOwnPropertyKeys(Types types) var processed = basePropertyKeys.Count > 0 ? new HashSet() : null; var includeStrings = (types & Types.String) != 0; - if (includeStrings && Target is IDictionary stringKeyedDictionary) // expando object for instance + if (includeStrings && _typeDescriptor.IsStringKeyedGenericDictionary) // expando object for instance { - foreach (var key in stringKeyedDictionary.Keys) + var keys = _typeDescriptor.GetKeys(Target); + foreach (var key in keys) { var jsString = JsString.Create(key); processed?.Add(jsString); @@ -166,7 +136,9 @@ private IEnumerable EnumerateOwnPropertyKeys(Types types) // we take values exposed as dictionary keys only foreach (var key in dictionary.Keys) { - if (_engine.ClrTypeConverter.TryConvert(key, typeof(string), CultureInfo.InvariantCulture, out var stringKey)) + object stringKey = key as string; + if (stringKey is not null + || _engine.ClrTypeConverter.TryConvert(key, typeof(string), CultureInfo.InvariantCulture, out stringKey)) { var jsString = JsString.Create((string) stringKey); processed?.Add(jsString); @@ -234,6 +206,15 @@ public override PropertyDescriptor GetOwnProperty(JsValue property) } var member = property.ToString(); + + if (_typeDescriptor.IsStringKeyedGenericDictionary) + { + if (_typeDescriptor.TryGetValue(Target, member, out var value)) + { + return new PropertyDescriptor(FromObject(_engine, value), PropertyFlag.OnlyEnumerable); + } + } + var result = Engine.Options.Interop.MemberAccessor(Engine, Target, member); if (result is not null) { diff --git a/Jint/Runtime/Interop/TypeDescriptor.cs b/Jint/Runtime/Interop/TypeDescriptor.cs index 1184e41988..260f1c7390 100644 --- a/Jint/Runtime/Interop/TypeDescriptor.cs +++ b/Jint/Runtime/Interop/TypeDescriptor.cs @@ -13,6 +13,7 @@ internal sealed class TypeDescriptor private static readonly Type _stringType = typeof(string); private readonly PropertyInfo _stringIndexer; + private readonly PropertyInfo _keysAccessor; private TypeDescriptor(Type type) { @@ -24,6 +25,7 @@ private TypeDescriptor(Type type) && i.GenericTypeArguments[0] == _stringType) { _stringIndexer = i.GetProperty("Item"); + _keysAccessor = i.GetProperty("Keys"); break; } } @@ -99,5 +101,15 @@ public bool TryGetValue(object target, string member, out object o) return false; } } + + public ICollection GetKeys(object target) + { + if (!IsStringKeyedGenericDictionary) + { + ExceptionHelper.ThrowInvalidOperationException("Not a string-keyed dictionary"); + } + + return (ICollection)_keysAccessor.GetValue(target); + } } } \ No newline at end of file