From b83e2dc85401fc567eb4b12151e2fadcb7f27360 Mon Sep 17 00:00:00 2001 From: Tony Han Date: Thu, 14 Mar 2024 10:51:09 +0800 Subject: [PATCH] Update the jint options to support System.text.json (#15449) --- .../JavaScriptEngine.cs | 39 +++++++- .../JsonValueConverter.cs | 56 ++++++++++++ .../Scripting/ScriptFunctionsTest.cs | 88 +++++++++++++++++++ 3 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 src/OrchardCore/OrchardCore.Scripting.JavaScript/JsonValueConverter.cs create mode 100644 test/OrchardCore.Tests/Scripting/ScriptFunctionsTest.cs diff --git a/src/OrchardCore/OrchardCore.Scripting.JavaScript/JavaScriptEngine.cs b/src/OrchardCore/OrchardCore.Scripting.JavaScript/JavaScriptEngine.cs index c59c2fc3529..8dd3421c99f 100644 --- a/src/OrchardCore/OrchardCore.Scripting.JavaScript/JavaScriptEngine.cs +++ b/src/OrchardCore/OrchardCore.Scripting.JavaScript/JavaScriptEngine.cs @@ -1,6 +1,10 @@ using System; using System.Collections.Generic; +using System.Reflection; +using System.Text.Json.Nodes; using Jint; +using Jint.Native; +using Jint.Runtime.Interop; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.FileProviders; @@ -19,7 +23,40 @@ public JavaScriptEngine(IMemoryCache memoryCache) public IScriptingScope CreateScope(IEnumerable methods, IServiceProvider serviceProvider, IFileProvider fileProvider, string basePath) { - var engine = new Engine(); + var engine = new Engine(options => + { + // Make JsonArray behave like JS array. + options.SetWrapObjectHandler(static (e, target, type) => + { + if (target is JsonArray) + { + var wrapped = new ObjectWrapper(e, target) + { + Prototype = e.Intrinsics.Array.PrototypeObject + }; + return wrapped; + } + + return new ObjectWrapper(e, target); + }); + + options.AddObjectConverter(); + + // We cannot access this[string] with anything else than JsonObject, otherwise itw will throw. + options.SetTypeResolver(new TypeResolver + { + MemberFilter = static info => + { + if (info.ReflectedType != typeof(JsonObject) && info.Name == "Item" && info is PropertyInfo p) + { + var parameters = p.GetIndexParameters(); + return parameters.Length != 1 || parameters[0].ParameterType != typeof(string); + } + + return true; + } + }); + }); foreach (var method in methods) { diff --git a/src/OrchardCore/OrchardCore.Scripting.JavaScript/JsonValueConverter.cs b/src/OrchardCore/OrchardCore.Scripting.JavaScript/JsonValueConverter.cs new file mode 100644 index 00000000000..5a59b572633 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Scripting.JavaScript/JsonValueConverter.cs @@ -0,0 +1,56 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using Jint; +using Jint.Native; +using Jint.Runtime.Interop; + +namespace OrchardCore.Scripting.JavaScript; +public class JsonValueConverter : IObjectConverter +{ + 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: + if (jsonValue.TryGetValue(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; + + } +} diff --git a/test/OrchardCore.Tests/Scripting/ScriptFunctionsTest.cs b/test/OrchardCore.Tests/Scripting/ScriptFunctionsTest.cs new file mode 100644 index 00000000000..4b51e3b60c7 --- /dev/null +++ b/test/OrchardCore.Tests/Scripting/ScriptFunctionsTest.cs @@ -0,0 +1,88 @@ +using System.Text.Json.Nodes; +using OrchardCore.Scripting; +using OrchardCore.Tests.Apis.Context; + +namespace OrchardCore.Tests.Scripting; +public class ScriptFunctionsTest +{ + [Fact] + public async Task TheScriptingEngineShouldBeAbleToHandleJsonObject() + { + using var context = new SiteContext(); + await context.InitializeAsync(); + await context.UsingTenantScopeAsync(scope => + { + var findUser = new GlobalMethod + { + Name = "tryAccessJsonObject", + Method = sp => () => + { + const string jsonData = """ + { + "age":33, + "falseValue":false, + "trueValue":true, + "stringValue":"stringTest", + "employees": { + "type": "array", + "value": [ + { + "firstName": "John", + "lastName": "Doe" + }, + { + "firstName": "Jane", + "lastName": "Doe" + } + ] + } + } + """; + + return JObject.Parse(jsonData); + } + }; + + var scriptingEngine = scope.ServiceProvider.GetRequiredService(); + var scriptingScope = scriptingEngine.CreateScope([findUser], scope.ServiceProvider, null, null); + var result = (bool)scriptingEngine.Evaluate(scriptingScope, + @"var jobj = tryAccessJsonObject(); + return jobj.age == 33; + "); + Assert.True(result); + + result = (bool)scriptingEngine.Evaluate(scriptingScope, + @"var jobj = tryAccessJsonObject(); + return jobj.stringValue == ""stringTest""; + "); + Assert.True(result); + result = (bool)scriptingEngine.Evaluate(scriptingScope, + @"var jobj = tryAccessJsonObject(); + return jobj.employees.type == ""array"" && + jobj.employees.value[0].firstName == ""John""; + "); + Assert.True(result); + + + var result1 = scriptingEngine.Evaluate(scriptingScope, + @"var jobj = tryAccessJsonObject(); + var steps = []; + if(!jobj.falseValue) steps.push(1); + if(jobj.trueValue) steps.push(2); + + // falseValue should be false + if(jobj.falseValue == false) steps.push(3); + if(jobj.trueValue == true) steps.push(4); + if(!!jobj.trueValue) steps.push(5); + steps.push(jobj.falseValue); + steps.push(jobj.falseValue.toString()); + + return steps.join(',') + "); + Assert.Equal("1,2,3,4,5,false,false", result1); + + + return Task.CompletedTask; + }); + } +}