elsa-core/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs
Sipke Schoorstra 341eeb471f Implement Enum to String Conversion for JavaScript Engine
Add a new `EnumToStringConverter` to convert enums to strings within the JavaScript engine, enhancing object conversion capabilities. Update `DefaultActivityExecutionMapper` with logging functionality for error handling. Modify constructors and improve code styling across multiple files.
2024-11-28 19:23:13 +01:00

147 lines
5.2 KiB
C#

using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography;
using System.Text;
using Acornima.Ast;
using Elsa.Expressions.Helpers;
using Elsa.Expressions.Models;
using Elsa.JavaScript.Contracts;
using Elsa.JavaScript.Helpers;
using Elsa.JavaScript.Notifications;
using Elsa.JavaScript.ObjectConverters;
using Elsa.JavaScript.Options;
using Elsa.Mediator.Contracts;
using Jint;
using Jint.Runtime.Interop;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
// ReSharper disable ConvertClosureToMethodGroup
namespace Elsa.JavaScript.Services;
/// <summary>
/// Provides a JavaScript evaluator using Jint.
/// </summary>
public class JintJavaScriptEvaluator(IConfiguration configuration, INotificationSender mediator, IOptions<JintOptions> scriptOptions, IMemoryCache memoryCache)
: IJavaScriptEvaluator
{
private readonly JintOptions _jintOptions = scriptOptions.Value;
/// <inheritdoc />
[RequiresUnreferencedCode("The Jint library uses reflection and can't be statically analyzed.")]
public async Task<object?> EvaluateAsync(string expression,
Type returnType,
ExpressionExecutionContext context,
ExpressionEvaluatorOptions? options = default,
Action<Engine>? configureEngine = default,
CancellationToken cancellationToken = default)
{
var engine = await GetConfiguredEngine(configureEngine, context, options, cancellationToken);
await mediator.SendAsync(new EvaluatingJavaScript(engine, context, expression), cancellationToken);
var result = ExecuteExpressionAndGetResult(engine, expression);
await mediator.SendAsync(new EvaluatedJavaScript(engine, context, expression, result), cancellationToken);
return result.ConvertTo(returnType);
}
private async Task<Engine> GetConfiguredEngine(Action<Engine>? configureEngine, ExpressionExecutionContext context, ExpressionEvaluatorOptions? options, CancellationToken cancellationToken)
{
options ??= new ExpressionEvaluatorOptions();
var engineOptions = new Jint.Options
{
ExperimentalFeatures = ExperimentalFeature.TaskInterop,
};
ConfigureClrAccess(engineOptions);
ConfigureObjectWrapper(engineOptions);
ConfigureObjectConverters(engineOptions);
await mediator.SendAsync(new CreatingJavaScriptEngine(engineOptions, context), cancellationToken);
_jintOptions.ConfigureEngineOptionsCallback(engineOptions, context);
var engine = new Engine(engineOptions);
configureEngine?.Invoke(engine);
ConfigureArgumentGetters(engine, options);
ConfigureConfigurationAccess(engine);
_jintOptions.ConfigureEngineCallback(engine, context);
return engine;
}
private void ConfigureClrAccess(Jint.Options options)
{
if (_jintOptions.AllowClrAccess)
options.AllowClr();
}
private void ConfigureObjectWrapper(Jint.Options options)
{
options.SetWrapObjectHandler((engine, target, type) =>
{
var instance = ObjectWrapper.Create(engine, target);
if (ObjectArrayHelper.DetermineIfObjectIsArrayLikeClrCollection(target.GetType()))
instance.Prototype = engine.Intrinsics.Array.PrototypeObject;
return instance;
});
}
private void ConfigureObjectConverters(Jint.Options options)
{
options.Interop.ObjectConverters.AddRange([new ByteArrayConverter(), new EnumToStringConverter()]);
}
private void ConfigureArgumentGetters(Engine engine, ExpressionEvaluatorOptions options)
{
foreach (var argument in options.Arguments)
engine.SetValue($"get{argument.Key}", (Func<object?>)(() => argument.Value));
}
private void ConfigureConfigurationAccess(Engine engine)
{
if (_jintOptions.AllowConfigurationAccess)
engine.SetValue("getConfig", (Func<string, object?>)(name => configuration.GetSection(name).Value));
}
private object? ExecuteExpressionAndGetResult(Engine engine, string expression)
{
var preparedScript = GetOrCreatePrepareScript(expression);
var result = engine.Evaluate(preparedScript);
return result.UnwrapIfPromise().ToObject();
}
private Prepared<Script> GetOrCreatePrepareScript(string expression)
{
var cacheKey = "jint:script:" + Hash(expression);
return memoryCache.GetOrCreate(cacheKey, entry =>
{
if (_jintOptions.ScriptCacheTimeout.HasValue)
entry.SetSlidingExpiration(_jintOptions.ScriptCacheTimeout.Value);
return PrepareScript(expression);
})!;
}
private Prepared<Script> PrepareScript(string expression)
{
var prepareOptions = new ScriptPreparationOptions
{
ParsingOptions = new ScriptParsingOptions
{
AllowReturnOutsideFunction = true
}
};
return Engine.PrepareScript(expression, options: prepareOptions);
}
private string Hash(string input)
{
var bytes = Encoding.UTF8.GetBytes(input);
var hash = SHA256.HashData(bytes);
return Convert.ToBase64String(hash);
}
}