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.
This commit is contained in:
Craig Fowler 2021-03-30 19:11:03 +01:00 committed by Sipke Schoorstra
parent 7ed875e53c
commit 4b5d86135d
2 changed files with 98 additions and 2 deletions

View file

@ -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<string,object>) : 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<string,object?> ExpandoToDictionary(ExpandoObject expando)
{
var json = JsonConvert.SerializeObject(expando);
return (IDictionary<string,object?>) JsonConvert.DeserializeObject(json, typeof(Dictionary<string,object?>))!;
}
object? EnumerableToObject(IEnumerable enumerable)
{
var json = JsonConvert.SerializeObject(enumerable);
return JsonConvert.DeserializeObject(json, typeof(object[]));
}
var expandoDictionary = obj as IDictionary<string,object>;
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)
{

View file

@ -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<ScriptOptions> options,
JavaScriptService sut,
[StubActivityExecutionContext] ActivityExecutionContext context1,
[StubActivityExecutionContext] ActivityExecutionContext context2)
{
object returnedValue = null;
Mock.Get(mediator)
.Setup(x => x.Publish(It.Is<EvaluatingJavaScriptExpression>(e => e.ActivityExecutionContext == context2), It.IsAny<CancellationToken>()))
.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<ScriptOptions> options,
JavaScriptService sut,
[StubActivityExecutionContext] ActivityExecutionContext context1,
[StubActivityExecutionContext] ActivityExecutionContext context2)
{
object returnedValue = null;
Mock.Get(mediator)
.Setup(x => x.Publish(It.Is<EvaluatingJavaScriptExpression>(e => e.ActivityExecutionContext == context2), It.IsAny<CancellationToken>()))
.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<ScriptOptions> options,
JavaScriptService sut,
[StubActivityExecutionContext] ActivityExecutionContext context1,
[StubActivityExecutionContext] ActivityExecutionContext context2)
{
object returnedValue = null;
Mock.Get(mediator)
.Setup(x => x.Publish(It.Is<EvaluatingJavaScriptExpression>(e => e.ActivityExecutionContext == context2), It.IsAny<CancellationToken>()))
.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<ScriptOptions> options,