Skip to content

Commit

Permalink
Improve stringify for JObject
Browse files Browse the repository at this point in the history
  • Loading branch information
lahma committed Nov 16, 2021
1 parent 0ba1090 commit 32e9d78
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 34 deletions.
64 changes: 64 additions & 0 deletions Jint.Tests/Runtime/InteropTests.NewtonsoftJson.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<int>());
break;
case JTokenType.Float:
result = JsNumber.Create(token.Value<double>());
break;
case JTokenType.String:
result = JsString.Create(token.Value<string>());
break;
case JTokenType.Boolean:
result = token.Value<bool>() ? 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<DateTime>());
break;
}

return !(result is null);
}
}
}
}
49 changes: 15 additions & 34 deletions Jint/Runtime/Interop/ObjectWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -152,9 +121,10 @@ private IEnumerable<JsValue> EnumerateOwnPropertyKeys(Types types)
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);
Expand All @@ -166,7 +136,9 @@ private IEnumerable<JsValue> 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);
Expand Down Expand Up @@ -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)
{
Expand Down
12 changes: 12 additions & 0 deletions Jint/Runtime/Interop/TypeDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -24,6 +25,7 @@ private TypeDescriptor(Type type)
&& i.GenericTypeArguments[0] == _stringType)
{
_stringIndexer = i.GetProperty("Item");
_keysAccessor = i.GetProperty("Keys");
break;
}
}
Expand Down Expand Up @@ -99,5 +101,15 @@ public bool TryGetValue(object target, string member, out object o)
return false;
}
}

public ICollection<string> GetKeys(object target)
{
if (!IsStringKeyedGenericDictionary)
{
ExceptionHelper.ThrowInvalidOperationException("Not a string-keyed dictionary");
}

return (ICollection<string>)_keysAccessor.GetValue(target);
}
}
}

0 comments on commit 32e9d78

Please sign in to comment.