From 4b5d86135dc0df2bb0f6a7bdb6380c0c2cdeac24 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Tue, 30 Mar 2021 19:11:03 +0100 Subject: [PATCH] Follow-up #761 - Fix for recursive case Unfortunately Jint still chokes in this case if the expando contains further nested expandos. It turns out that I need to do this recursively for it to always work. We also have trouble with IEnumerable types. They also can't be round-tripped from/to JSON when they aren't deserialized as a very specific type. --- .../Services/JavaScriptService.cs | 40 ++++++++++++- .../Services/JavaScriptServiceTests.cs | 60 +++++++++++++++++++ 2 files changed, 98 insertions(+), 2 deletions(-) diff --git a/src/scripting/Elsa.Scripting.JavaScript/Services/JavaScriptService.cs b/src/scripting/Elsa.Scripting.JavaScript/Services/JavaScriptService.cs index 1c5fd0d48..dad1c48a6 100644 --- a/src/scripting/Elsa.Scripting.JavaScript/Services/JavaScriptService.cs +++ b/src/scripting/Elsa.Scripting.JavaScript/Services/JavaScriptService.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Dynamic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Elsa.Scripting.JavaScript.Converters; @@ -50,9 +51,12 @@ namespace Elsa.Scripting.JavaScript.Services if (converter.CanConvertTo(returnType)) converter.ConvertTo(returnValue, returnType); - returnType = (returnValue is ExpandoObject && returnType == typeof(object))? typeof(Dictionary) : returnType; - if (returnValue is IEnumerable) + if(returnValue is ExpandoObject expando && returnType == typeof(object)) + return RecursivelyPrepareExpandoObjectForReturn(expando); + + if (returnValue is IEnumerable && !(returnValue is string)) { + returnType = (returnType == typeof(object))? typeof(object[]) : returnType; var json = JsonConvert.SerializeObject(returnValue); return JsonConvert.DeserializeObject(json, returnType); } @@ -62,6 +66,38 @@ namespace Elsa.Scripting.JavaScript.Services return Convert.ChangeType(returnValue, returnType); } + + static object? RecursivelyPrepareExpandoObjectForReturn(ExpandoObject obj) + { + IDictionary ExpandoToDictionary(ExpandoObject expando) + { + var json = JsonConvert.SerializeObject(expando); + return (IDictionary) JsonConvert.DeserializeObject(json, typeof(Dictionary))!; + } + + object? EnumerableToObject(IEnumerable enumerable) + { + var json = JsonConvert.SerializeObject(enumerable); + return JsonConvert.DeserializeObject(json, typeof(object[])); + } + + var expandoDictionary = obj as IDictionary; + + var allValuesToReplace = (from kvp in expandoDictionary + where kvp.Value is IEnumerable && !(kvp.Value is string) + let val = (IEnumerable) kvp.Value + let replacementValue = (val is ExpandoObject expando) + ? RecursivelyPrepareExpandoObjectForReturn(expando) + : EnumerableToObject(val) + select new { Key = kvp.Key, Value = replacementValue }) + .ToDictionary(k => k.Key, v => v.Value); + + var output = ExpandoToDictionary(obj); + foreach(var kvp in allValuesToReplace) + output[kvp.Key] = kvp.Value; + + return output; + } private void ConfigureJintEngine(Jint.Options options) { diff --git a/test/unit/Elsa.UnitTests/Scripting/JavaScript/Services/JavaScriptServiceTests.cs b/test/unit/Elsa.UnitTests/Scripting/JavaScript/Services/JavaScriptServiceTests.cs index 6224f48fb..6301366dd 100644 --- a/test/unit/Elsa.UnitTests/Scripting/JavaScript/Services/JavaScriptServiceTests.cs +++ b/test/unit/Elsa.UnitTests/Scripting/JavaScript/Services/JavaScriptServiceTests.cs @@ -34,6 +34,66 @@ namespace Elsa.Scripting.JavaScript.Services Assert.Equal(@"{""foo"":""bar""}", result); } + [Theory(DisplayName = "The EvaluateAsync method should be able to roundtrip a JSON object (which contains an embedded object) via JSON.parse and then JSON.stringify"), AutoMoqData] + public async Task EvaluateAsyncShouldBeAbleToRoundtripAJsonStringWithAnEmbeddedObjectViaJsonStringify([Frozen] IMediator mediator, + [Frozen] IOptions options, + JavaScriptService sut, + [StubActivityExecutionContext] ActivityExecutionContext context1, + [StubActivityExecutionContext] ActivityExecutionContext context2) + { + object returnedValue = null; + Mock.Get(mediator) + .Setup(x => x.Publish(It.Is(e => e.ActivityExecutionContext == context2), It.IsAny())) + .Callback((EvaluatingJavaScriptExpression expression, CancellationToken t) => { + expression.Engine.SetValue("MyVariable", returnedValue); + }); + + returnedValue = await sut.EvaluateAsync(@"JSON.parse(""{\""foo\"":\""bar\"", \""child\"":{\""one\"":\""two\""}}"")", typeof(object), context1); + var result = await sut.EvaluateAsync("JSON.stringify(MyVariable)", typeof(object), context2); + + Assert.Equal(@"{""foo"":""bar"",""child"":{""one"":""two""}}", result); + } + + [Theory(DisplayName = "The EvaluateAsync method should be able to roundtrip a JSON object (which contains an embedded array) via JSON.parse and then JSON.stringify"), AutoMoqData] + public async Task EvaluateAsyncShouldBeAbleToRoundtripAJsonStringWithAnEmbeddedArrayViaJsonStringify([Frozen] IMediator mediator, + [Frozen] IOptions options, + JavaScriptService sut, + [StubActivityExecutionContext] ActivityExecutionContext context1, + [StubActivityExecutionContext] ActivityExecutionContext context2) + { + object returnedValue = null; + Mock.Get(mediator) + .Setup(x => x.Publish(It.Is(e => e.ActivityExecutionContext == context2), It.IsAny())) + .Callback((EvaluatingJavaScriptExpression expression, CancellationToken t) => { + expression.Engine.SetValue("MyVariable", returnedValue); + }); + + returnedValue = await sut.EvaluateAsync(@"JSON.parse(""{\""foo\"":\""bar\"", \""child\"":[\""one\"",\""two\""]}"")", typeof(object), context1); + var result = await sut.EvaluateAsync("JSON.stringify(MyVariable)", typeof(object), context2); + + Assert.Equal(@"{""foo"":""bar"",""child"":[""one"",""two""]}", result); + } + + [Theory(DisplayName = "The EvaluateAsync method should be able to roundtrip a JSON array via JSON.parse and then JSON.stringify"), AutoMoqData] + public async Task EvaluateAsyncShouldBeAbleToRoundtripAJsonStringOfAnArrayViaJsonStringify([Frozen] IMediator mediator, + [Frozen] IOptions options, + JavaScriptService sut, + [StubActivityExecutionContext] ActivityExecutionContext context1, + [StubActivityExecutionContext] ActivityExecutionContext context2) + { + object returnedValue = null; + Mock.Get(mediator) + .Setup(x => x.Publish(It.Is(e => e.ActivityExecutionContext == context2), It.IsAny())) + .Callback((EvaluatingJavaScriptExpression expression, CancellationToken t) => { + expression.Engine.SetValue("MyVariable", returnedValue); + }); + + returnedValue = await sut.EvaluateAsync(@"JSON.parse(""[\""foo\"",\""bar\""]"")", typeof(object), context1); + var result = await sut.EvaluateAsync("JSON.stringify(MyVariable)", typeof(object), context2); + + Assert.Equal(@"[""foo"",""bar""]", result); + } + [Theory(DisplayName = "The EvaluateAsync method should be able to access a property of an object which was created via JSON.parse"), AutoMoqData] public async Task EvaluateAsyncShouldBeAbleToAccessAPropertyWhichWasParsed([Frozen] IMediator mediator, [Frozen] IOptions options,