Browse Source

Improve stringify for JObject (#1009)

Marko Lahma 3 years ago
parent
commit
2d8f1ec2cb

+ 1 - 1
Jint.Tests/Runtime/Domain/ArrayConverterTestClass.cs

@@ -54,7 +54,7 @@ namespace Jint.Tests.Runtime.Domain
         #region NotImplemented
         public TypeCode GetTypeCode()
         {
-            throw new NotImplementedException();
+            return TypeCode.Object;
         }
 
         public bool ToBoolean(IFormatProvider provider)

+ 18 - 0
Jint.Tests/Runtime/InteropTests.NewtonsoftJson.cs

@@ -90,5 +90,23 @@ namespace Jint.Tests.Runtime
 
             Assert.Equal("{\"Current\":null}", result);
         }
+
+        [Fact]
+        public void EngineShouldStringifyJObjectFromObjectListWithValuesCorrectly()
+        {
+            var engine = new Engine();
+
+            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);
+        }
     }
 }

+ 86 - 28
Jint/Runtime/Interop/DefaultObjectConverter.cs

@@ -37,6 +37,7 @@ namespace Jint
 
         public static bool TryConvert(Engine engine, object value, out JsValue result)
         {
+            result = null;
             var valueType = value.GetType();
 
             var typeMappers = _typeMappers;
@@ -57,57 +58,114 @@ namespace Jint
                         }, typeMappers);
 
                     result = ConvertArray(engine, a);
+                    return result is not null;
+                }
+
+                if (value is IConvertible convertible)
+                {
+                    result = ConvertConvertible(engine, convertible);
+                    if (result is not null)
+                    {
+                        return true;
+                    }
+                }
+
+                if (value is Delegate d)
+                {
+                    result = new DelegateWrapper(engine, d);
                 }
                 else
                 {
-                    if (value is Delegate d)
+                    var t = value.GetType();
+
+                    if (!engine.Options.Interop.AllowSystemReflection
+                        && t.Namespace?.StartsWith("System.Reflection") == true)
                     {
-                        result = new DelegateWrapper(engine, d);
+                        const string message = "Cannot access System.Reflection namespace, check Engine's interop options";
+                        ExceptionHelper.ThrowInvalidOperationException(message);
                     }
-                    else
+
+                    if (t.IsEnum)
                     {
-                        var t = value.GetType();
+                        var ut = Enum.GetUnderlyingType(t);
 
-                        if (!engine.Options.Interop.AllowSystemReflection
-                            && t.Namespace?.StartsWith("System.Reflection") == true)
+                        if (ut == typeof(ulong))
                         {
-                            const string message = "Cannot access System.Reflection namespace, check Engine's interop options";
-                            ExceptionHelper.ThrowInvalidOperationException(message);
+                            result = JsNumber.Create(Convert.ToDouble(value));
                         }
-
-                        if (t.IsEnum)
+                        else
                         {
-                            var ut = Enum.GetUnderlyingType(t);
-
-                            if (ut == typeof(ulong))
+                            if (ut == typeof(uint) || ut == typeof(long))
                             {
-                                result = JsNumber.Create(Convert.ToDouble(value));
+                                result = JsNumber.Create(Convert.ToInt64(value));
                             }
                             else
                             {
-                                if (ut == typeof(uint) || ut == typeof(long))
-                                {
-                                    result = JsNumber.Create(Convert.ToInt64(value));
-                                }
-                                else
-                                {
-                                    result = JsNumber.Create(Convert.ToInt32(value));
-                                }
+                                result = JsNumber.Create(Convert.ToInt32(value));
                             }
                         }
-                        else
-                        {
-                            result = engine.Options.Interop.WrapObjectHandler.Invoke(engine, value);
-                        }
-
-                        // if no known type could be guessed, use the default of wrapping using using ObjectWrapper.
                     }
+                    else
+                    {
+                        result = engine.Options.Interop.WrapObjectHandler.Invoke(engine, value);
+                    }
+
+                    // if no known type could be guessed, use the default of wrapping using using ObjectWrapper.
                 }
             }
 
             return result is not null;
         }
 
+        private static JsValue ConvertConvertible(Engine engine, IConvertible convertible)
+        {
+            JsValue result = null;
+            switch (convertible.GetTypeCode())
+            {
+                case TypeCode.Boolean:
+                    result = convertible.ToBoolean(engine.Options.Culture) ? JsBoolean.True : JsBoolean.False;
+                    break;
+                case TypeCode.Byte:
+                    result = JsNumber.Create(convertible.ToByte(engine.Options.Culture));
+                    break;
+                case TypeCode.Char:
+                    result = JsString.Create(convertible.ToChar(engine.Options.Culture));
+                    break;
+                case TypeCode.Double:
+                    result = JsNumber.Create(convertible.ToDouble(engine.Options.Culture));
+                    break;
+                case TypeCode.SByte:
+                    result = JsNumber.Create(convertible.ToSByte(engine.Options.Culture));
+                    break;
+                case TypeCode.Int16:
+                    result = JsNumber.Create(convertible.ToInt16(engine.Options.Culture));
+                    break;
+                case TypeCode.Int32:
+                    result = JsNumber.Create(convertible.ToInt32(engine.Options.Culture));
+                    break;
+                case TypeCode.UInt16:
+                    result = JsNumber.Create(convertible.ToUInt16(engine.Options.Culture));
+                    break;
+                case TypeCode.Int64:
+                    result = JsNumber.Create(convertible.ToInt64(engine.Options.Culture));
+                    break;
+                case TypeCode.Single:
+                    result = JsNumber.Create(convertible.ToSingle(engine.Options.Culture));
+                    break;
+                case TypeCode.String:
+                    result = JsString.Create(convertible.ToString(engine.Options.Culture));
+                    break;
+                case TypeCode.UInt32:
+                    result = JsNumber.Create(convertible.ToUInt32(engine.Options.Culture));
+                    break;
+                case TypeCode.UInt64:
+                    result = JsNumber.Create(convertible.ToUInt64(engine.Options.Culture));
+                    break;
+            }
+
+            return result;
+        }
+
         private static JsValue ConvertArray(Engine e, object v)
         {
             var array = (Array)v;

+ 15 - 34
Jint/Runtime/Interop/ObjectWrapper.cs

@@ -98,37 +98,6 @@ namespace Jint.Runtime.Interop
                 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 @@ namespace Jint.Runtime.Interop
             var processed = basePropertyKeys.Count > 0 ? new HashSet<JsValue>() : null;
 
             var includeStrings = (types & Types.String) != 0;
-            if (includeStrings && Target is IDictionary<string, object> 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 @@ namespace Jint.Runtime.Interop
                 // 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 @@ namespace Jint.Runtime.Interop
             }
 
             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)
             {

+ 12 - 0
Jint/Runtime/Interop/TypeDescriptor.cs

@@ -13,6 +13,7 @@ namespace Jint.Runtime.Interop
         private static readonly Type _stringType = typeof(string);
 
         private readonly PropertyInfo _stringIndexer;
+        private readonly PropertyInfo _keysAccessor;
 
         private TypeDescriptor(Type type)
         {
@@ -24,6 +25,7 @@ namespace Jint.Runtime.Interop
                     && i.GenericTypeArguments[0] == _stringType)
                 {
                     _stringIndexer = i.GetProperty("Item");
+                    _keysAccessor = i.GetProperty("Keys");
                     break;
                 }
             }
@@ -99,5 +101,15 @@ namespace Jint.Runtime.Interop
                 return false;
             }
         }
+
+        public ICollection<string> GetKeys(object target)
+        {
+            if (!IsStringKeyedGenericDictionary)
+            {
+                ExceptionHelper.ThrowInvalidOperationException("Not a string-keyed dictionary");
+            }
+
+            return (ICollection<string>)_keysAccessor.GetValue(target);
+        }
     }
 }