Skip to content

Commit

Permalink
Improve support for array operations under interop (#1828)
Browse files Browse the repository at this point in the history
  • Loading branch information
ejsmith authored Apr 5, 2024
1 parent 3209ab3 commit 45e0e7b
Show file tree
Hide file tree
Showing 10 changed files with 602 additions and 273 deletions.
17 changes: 1 addition & 16 deletions Jint.Benchmark/ListInteropBenchmark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,22 +44,7 @@ private static bool IsArrayLike(Type type)
[GlobalSetup]
public void Setup()
{
_engine = new Engine(options =>
{
options
.SetWrapObjectHandler((engine, target, type) =>
{
var instance = ObjectWrapper.Create(engine, target);
var isArrayLike = IsArrayLike(target.GetType());
if (isArrayLike)
{
instance.Prototype = engine.Intrinsics.Array.PrototypeObject;
}
return instance;
})
;
});
_engine = new Engine();

_properties = new JsValue[Count];
var input = new List<Data>(Count);
Expand Down
26 changes: 26 additions & 0 deletions Jint.Tests.PublicInterface/InteropTests.NewtonsoftJson.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,5 +129,31 @@ public void ShouldBeAbleToChangePropertyWithNameValue()

Assert.Equal("CHANGED", result);
}

[Fact]
public void ArraysShouldPassThroughCorrectly()
{
var engine = new Engine();

const string Json = """
{
'entries': [
{ 'id': 1, 'name': 'One' },
{ 'id': 2, 'name': 'Two' },
{ 'id': 3, 'name': 'Three' }
]
}
""";

var obj = JObject.Parse(Json);
engine.SetValue("o", obj);

var names = engine.Evaluate("o.entries.map(e => e.name)").AsArray();

Assert.Equal((uint) 3, names.Length);
Assert.Equal("One", names[0]);
Assert.Equal("Two", names[1]);
Assert.Equal("Three", names[2]);
}
}
}
258 changes: 136 additions & 122 deletions Jint.Tests.PublicInterface/InteropTests.SystemTextJson.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,148 +5,99 @@

namespace Jint.Tests.PublicInterface;

public sealed class SystemTextJsonValueConverter : IObjectConverter
public partial class InteropTests
{
public static readonly SystemTextJsonValueConverter Instance = new();
[Fact]
public void ArrayPrototypeFindWithInteropJsonArray()
{
var engine = GetEngine();

private SystemTextJsonValueConverter()
var array = new JsonArray { "A", "B", "C" };
engine.SetValue("array", array);

Assert.Equal(1, engine.Evaluate("array.findIndex((x) => x === 'B')"));
Assert.Equal('B', engine.Evaluate("array.find((x) => x === 'B')"));
}

[Fact]
public void ArrayPrototypePushWithInteropJsonArray()
{
var engine = GetEngine();

var array = new JsonArray { "A", "B", "C" };
engine.SetValue("array", array);

engine.Evaluate("array.push('D')");
Assert.Equal(4, array.Count);
Assert.Equal("D", array[3]?.ToString());
Assert.Equal(3, engine.Evaluate("array.lastIndexOf('D')"));
}

public bool TryConvert(Engine engine, object value, out JsValue result)
[Fact]
public void ArrayPrototypePopWithInteropJsonArray()
{
if (value is JsonValue jsonValue)
{
var valueKind = jsonValue.GetValueKind();
switch (valueKind)
{
case JsonValueKind.Object:
case JsonValueKind.Array:
result = JsValue.FromObject(engine, jsonValue);
break;
case JsonValueKind.String:
result = jsonValue.ToString();
break;
case JsonValueKind.Number:
if (jsonValue.TryGetValue<double>(out var doubleValue))
{
result = JsNumber.Create(doubleValue);
}
else
{
result = JsValue.Undefined;
}
break;
case JsonValueKind.True:
result = JsBoolean.True;
break;
case JsonValueKind.False:
result = JsBoolean.False;
break;
case JsonValueKind.Undefined:
result = JsValue.Undefined;
break;
case JsonValueKind.Null:
result = JsValue.Null;
break;
default:
result = JsValue.Undefined;
break;
}
return true;
}
result = JsValue.Undefined;
return false;
var engine = GetEngine();

var array = new JsonArray { "A", "B", "C" };
engine.SetValue("array", array);

Assert.Equal(2, engine.Evaluate("array.lastIndexOf('C')"));
Assert.Equal(3, array.Count);
Assert.Equal("C", engine.Evaluate("array.pop()"));
Assert.Equal(2, array.Count);
Assert.Equal(-1, engine.Evaluate("array.lastIndexOf('C')"));
}
}
public partial class InteropTests
{

[Fact]
public void AccessingJsonNodeShouldWork()
{
const string Json = """
{
"falseValue": false,
"employees": {
"trueValue": true,
"falseValue": false,
"number": 123.456,
"zeroNumber": 0,
"emptyString":"",
"nullValue":null,
"other": "abc",
"type": "array",
"value": [
{
"firstName": "John",
"lastName": "Doe"
},
{
"firstName": "Jane",
"lastName": "Doe"
}
]
}
}
""";
{
"falseValue": false,
"employees": {
"trueValue": true,
"falseValue": false,
"number": 123.456,
"zeroNumber": 0,
"emptyString":"",
"nullValue":null,
"other": "abc",
"type": "array",
"value": [
{
"firstName": "John",
"lastName": "Doe"
},
{
"firstName": "Jane",
"lastName": "Doe"
}
]
}
}
""";

var variables = JsonNode.Parse(Json);

var engine = new Engine(options =>
{
#if !NET8_0_OR_GREATER
// Jint doesn't know about the types statically as they are not part of the out-of-the-box experience
// make JsonArray behave like JS array
options.Interop.WrapObjectHandler = static (e, target, type) =>
{
if (target is JsonArray)
{
var wrapped = ObjectWrapper.Create(e, target);
wrapped.Prototype = e.Intrinsics.Array.PrototypeObject;
return wrapped;
}
return ObjectWrapper.Create(e, target);
};
options.AddObjectConverter(SystemTextJsonValueConverter.Instance);
// we cannot access this[string] with anything else than JsonObject, otherwise itw will throw
options.Interop.TypeResolver = new TypeResolver
{
MemberFilter = static info =>
{
if (info.ReflectedType != typeof(JsonObject) && info.Name == "Item" && info is System.Reflection.PropertyInfo p)
{
var parameters = p.GetIndexParameters();
return parameters.Length != 1 || parameters[0].ParameterType != typeof(string);
}
return true;
}
};
#endif
});
var engine = GetEngine();

engine
.SetValue("falseValue", false)
.SetValue("variables", variables)
.Execute("""
function populateFullName() {
return variables['employees'].value.map(item => {
var newItem =
{
"firstName": item.firstName,
"lastName": item.lastName,
"fullName": item.firstName + ' ' + item.lastName
};

return newItem;
});
}
""");
function populateFullName() {
return variables['employees'].value.map(item => {
var newItem =
{
"firstName": item.firstName,
"lastName": item.lastName,
"fullName": item.firstName + ' ' + item.lastName
};
return newItem;
});
}
""");

// reading data
var result = engine.Evaluate("populateFullName()").AsArray();
Expand Down Expand Up @@ -202,4 +153,67 @@ function populateFullName() {
Assert.True(engine.Evaluate("variables.employees.number == 456.789").AsBoolean());
Assert.True(engine.Evaluate("variables.employees.other == 'def'").AsBoolean());
}

private static Engine GetEngine()
{
var engine = new Engine(options =>
{
#if !NET8_0_OR_GREATER
// Jint doesn't know about the types statically as they are not part of the out-of-the-box experience
options.AddObjectConverter(SystemTextJsonValueConverter.Instance);
#endif
});

return engine;
}
}

file sealed class SystemTextJsonValueConverter : IObjectConverter
{
public static readonly SystemTextJsonValueConverter Instance = new();

private SystemTextJsonValueConverter()
{
}

public bool TryConvert(Engine engine, object value, out JsValue result)
{
if (value is JsonValue jsonValue)
{
var valueKind = jsonValue.GetValueKind();
switch (valueKind)
{
case JsonValueKind.Object:
case JsonValueKind.Array:
result = JsValue.FromObject(engine, jsonValue);
break;
case JsonValueKind.String:
result = jsonValue.ToString();
break;
case JsonValueKind.Number:
result = jsonValue.TryGetValue<double>(out var doubleValue) ? JsNumber.Create(doubleValue) : JsValue.Undefined;
break;
case JsonValueKind.True:
result = JsBoolean.True;
break;
case JsonValueKind.False:
result = JsBoolean.False;
break;
case JsonValueKind.Undefined:
result = JsValue.Undefined;
break;
case JsonValueKind.Null:
result = JsValue.Null;
break;
default:
result = JsValue.Undefined;
break;
}

return true;
}

result = JsValue.Undefined;
return false;
}
}
24 changes: 1 addition & 23 deletions Jint.Tests/Runtime/InteropTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -848,16 +848,6 @@ public void CanAddArrayPrototypeForArrayLikeClrObjects()
{
var e = new Engine(cfg => cfg
.AllowClr(typeof(Person).Assembly)
.SetWrapObjectHandler((engine, target, type) =>
{
var instance = ObjectWrapper.Create(engine, target);
if (instance.IsArrayLike)
{
instance.SetPrototypeOf(engine.Realm.Intrinsics.Array.PrototypeObject);
}
return instance;
})
);

var person = new Person
Expand All @@ -881,19 +871,7 @@ public void CanAddArrayPrototypeForArrayLikeClrObjects()
[Fact]
public void CanSetIsConcatSpreadableForArrays()
{
var engine = new Engine(opt =>
{
opt.SetWrapObjectHandler((eng, obj, type) =>
{
var wrapper = ObjectWrapper.Create(eng, obj);
if (wrapper.IsArrayLike)
{
wrapper.SetPrototypeOf(eng.Realm.Intrinsics.Array.PrototypeObject);
wrapper.Set(GlobalSymbolRegistry.IsConcatSpreadable, true);
}
return wrapper;
});
});
var engine = new Engine();

engine
.SetValue("list1", new List<string> { "A", "B", "C" })
Expand Down
Loading

0 comments on commit 45e0e7b

Please sign in to comment.