Add JS functions for encoding/decoding byte arrays from and to strings (#5902)

* Add handlers and improve JavaScript engine configuration

Added several new handlers for configuring the JavaScript engine with common types, functions, variable accessors, and input/output accessors. Refactored the JintJavaScriptEvaluator for better clarity and modularity by breaking down configuration steps into separate methods. Also replaced `IsInsideCompositeActivity` with `IsContainedWithinCompositeActivity` for better semantic consistency.

* Refactor to use primary constructor syntax for TypeDefinitionProviders

Updated CommonTypeDefinitionProvider, VariableTypeDefinitionProvider, and ActivityOutputFunctionsDefinitionProvider to use primary constructors for dependency injection. This change reduces redundancy and simplifies the code structure for better readability and maintainability.

* Add byte conversion functions to JavaScript engine

Introduced functions for converting bytes to/from strings and Base64. Updated CommonFunctionsDefinitionProvider and ConfigureEngineWithCommonFunctions to incorporate these new functions. This enhances the JavaScript engine's ability to handle byte array manipulations.

* Add tests for JavaScript byte and string conversions.

Introduce integration tests to verify byte array to string, string to byte array, byte array to Base64, and Base64 to byte array conversions using JavaScript functions. Ensure accurate transformation of data within different encoding scenarios.

* Add meaningful summaries to Jint engine configuration handlers

Updated comment summaries in four handler classes to provide clear and concise descriptions of their purpose. This helps improve code readability and understanding for future developers.

* Rename and merge variable and input/output handlers

Merged the variable accessor logic into the input/output handler and renamed the class to reflect its broader functionality. This consolidation ensures the accessors are registered in the right order.

* Add string base64 conversion functions

Introduced `stringToBase64` and `stringFromBase64` functions to handle base64 encoding and decoding of strings. Updated corresponding provider, handler, and added tests to ensure functionality.
This commit is contained in:
Sipke Schoorstra 2024-08-14 17:43:35 +02:00 committed by GitHub
parent fbf3418544
commit 5f11026ee7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 367 additions and 178 deletions

View file

@ -0,0 +1,74 @@
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Text.Unicode;
using Elsa.Extensions;
using Elsa.JavaScript.Notifications;
using Elsa.Mediator.Contracts;
using JetBrains.Annotations;
namespace Elsa.JavaScript.Handlers;
/// A handler that configures the Jint engine with common functions.
[UsedImplicitly]
public class ConfigureEngineWithCommonFunctions : INotificationHandler<EvaluatingJavaScript>
{
private readonly JsonSerializerOptions _jsonSerializerOptions = CreateJsonSerializerOptions();
/// <inheritdoc />
public Task HandleAsync(EvaluatingJavaScript notification, CancellationToken cancellationToken)
{
var engine = notification.Engine;
var context = notification.Context;
// Add common functions.
engine.SetValue("getWorkflowDefinitionId", (Func<string>)(() => context.GetWorkflowExecutionContext().Workflow.Identity.DefinitionId));
engine.SetValue("getWorkflowDefinitionVersionId", (Func<string>)(() => context.GetWorkflowExecutionContext().Workflow.Identity.Id));
engine.SetValue("getWorkflowDefinitionVersion", (Func<int>)(() => context.GetWorkflowExecutionContext().Workflow.Identity.Version));
engine.SetValue("getWorkflowInstanceId", (Func<string>)(() => context.GetActivityExecutionContext().WorkflowExecutionContext.Id));
engine.SetValue("setCorrelationId", (Action<string?>)(value => context.GetActivityExecutionContext().WorkflowExecutionContext.CorrelationId = value));
engine.SetValue("getCorrelationId", (Func<string?>)(() => context.GetActivityExecutionContext().WorkflowExecutionContext.CorrelationId));
engine.SetValue("setVariable", (Action<string, object>)((name, value) => context.SetVariableInScope(name, value)));
engine.SetValue("getVariable", (Func<string, object?>)(name => context.GetVariableInScope(name)));
engine.SetValue("getInput", (Func<string, object?>)(name => context.GetInput(name)));
engine.SetValue("getOutputFrom", (Func<string, string?, object?>)((activityIdName, outputName) => context.GetOutput(activityIdName, outputName)));
engine.SetValue("getLastResult", (Func<object?>)(() => context.GetLastResult()));
engine.SetValue("isNullOrWhiteSpace", (Func<string, bool>)(value => string.IsNullOrWhiteSpace(value)));
engine.SetValue("isNullOrEmpty", (Func<string, bool>)(value => string.IsNullOrEmpty(value)));
engine.SetValue("toJson", (Func<object, string>)(value => Serialize(value)));
engine.SetValue("parseGuid", (Func<string, Guid>)(value => Guid.Parse(value)));
engine.SetValue("newGuid", (Func<Guid>)(() => Guid.NewGuid()));
engine.SetValue("newGuidString", (Func<string>)(() => Guid.NewGuid().ToString()));
engine.SetValue("newShortGuid", (Func<string>)(() => Regex.Replace(Convert.ToBase64String(Guid.NewGuid().ToByteArray()), "[/+=]", "")));
engine.SetValue("bytesToString", (Func<byte[], string>)(value => Encoding.UTF8.GetString(value)));
engine.SetValue("bytesFromString", (Func<string, byte[]>)(value => Encoding.UTF8.GetBytes(value)));
engine.SetValue("bytesToBase64", (Func<byte[], string>)(value => Convert.ToBase64String(value)));
engine.SetValue("bytesFromBase64", (Func<string, byte[]>)(value => Convert.FromBase64String(value)));
engine.SetValue("stringToBase64", (Func<string, string>)(value => Convert.ToBase64String(Encoding.UTF8.GetBytes(value))));
engine.SetValue("stringFromBase64", (Func<string, string>)(value => Encoding.UTF8.GetString(Convert.FromBase64String(value))));
// Deprecated, use newGuidString instead.
engine.SetValue("getGuidString", (Func<string>)(() => Guid.NewGuid().ToString()));
// Deprecated, use newShortGuid instead.
engine.SetValue("getShortGuid", (Func<string>)(() => Regex.Replace(Convert.ToBase64String(Guid.NewGuid().ToByteArray()), "[/+=]", "")));
return Task.CompletedTask;
}
private string Serialize(object value)
{
return JsonSerializer.Serialize(value, _jsonSerializerOptions);
}
private static JsonSerializerOptions CreateJsonSerializerOptions()
{
var options = new JsonSerializerOptions
{
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
};
options.Converters.Add(new JsonStringEnumConverter());
return options;
}
}

View file

@ -0,0 +1,25 @@
using Elsa.Extensions;
using Elsa.JavaScript.Notifications;
using Elsa.Mediator.Contracts;
using JetBrains.Annotations;
namespace Elsa.JavaScript.Handlers;
/// A handler that configures the Jint engine with common types.
[UsedImplicitly]
public class ConfigureEngineWithCommonTypes : INotificationHandler<EvaluatingJavaScript>
{
/// <inheritdoc />
public Task HandleAsync(EvaluatingJavaScript notification, CancellationToken cancellationToken)
{
var engine = notification.Engine;
// Add common .NET types.
engine.RegisterType<DateTime>();
engine.RegisterType<DateTimeOffset>();
engine.RegisterType<TimeSpan>();
engine.RegisterType<Guid>();
return Task.CompletedTask;
}
}

View file

@ -0,0 +1,68 @@
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.JavaScript.Notifications;
using Elsa.Mediator.Contracts;
using Humanizer;
using JetBrains.Annotations;
using Jint;
namespace Elsa.JavaScript.Handlers;
/// A handler that configures the Jint engine with workflow input and output accessors.
[UsedImplicitly]
public class ConfigureEngineWithVariablesAndInputOutputAccessors : INotificationHandler<EvaluatingJavaScript>
{
/// <inheritdoc />
public async Task HandleAsync(EvaluatingJavaScript notification, CancellationToken cancellationToken)
{
var engine = notification.Engine;
var context = notification.Context;
// The order of the next 3 lines is important.
CreateVariableAccessors(engine, context);
CreateWorkflowInputAccessors(engine, context);
await CreateActivityOutputAccessorsAsync(engine, context);
}
private void CreateVariableAccessors(Engine engine, ExpressionExecutionContext context)
{
var variableNames = context.GetVariableNamesInScope().ToList();
foreach (var variableName in variableNames)
{
var pascalName = variableName.Pascalize();
engine.SetValue($"get{pascalName}", (Func<object?>)(() => context.GetVariableInScope(variableName)));
engine.SetValue($"set{pascalName}", (Action<object?>)(value => context.SetVariableInScope(variableName, value)));
}
}
private void CreateWorkflowInputAccessors(Engine engine, ExpressionExecutionContext context)
{
// Create workflow input accessors - only if the current activity is not part of a composite activity definition.
// Otherwise, the workflow input accessors will hide the composite activity input accessors which rely on variable accessors.
if (context.IsContainedWithinCompositeActivity())
return;
var inputs = context.GetWorkflowInputs().ToDictionary(x => x.Name);
if (!context.TryGetWorkflowExecutionContext(out var workflowExecutionContext))
return;
var inputDefinitions = workflowExecutionContext.Workflow.Inputs;
foreach (var inputDefinition in inputDefinitions)
{
var input = inputs.GetValueOrDefault(inputDefinition.Name);
engine.SetValue($"get{inputDefinition.Name}", (Func<object?>)(() => input?.Value));
}
}
private static async Task CreateActivityOutputAccessorsAsync(Engine engine, ExpressionExecutionContext context)
{
var activityOutputs = context.GetActivityOutputs();
await foreach (var activityOutput in activityOutputs)
foreach (var outputName in activityOutput.OutputNames)
engine.SetValue($"get{outputName}From{activityOutput.ActivityName}", (Func<object?>)(() => context.GetOutput(activityOutput.ActivityId, outputName)));
}
}

View file

@ -6,12 +6,12 @@ using Elsa.Workflows.Management;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Entities;
using Humanizer;
using JetBrains.Annotations;
namespace Elsa.JavaScript.Providers;
/// <summary>
/// Produces <see cref="FunctionDefinition"/>s for common functions.
/// </summary>
[UsedImplicitly]
internal class InputFunctionsDefinitionProvider(ITypeAliasRegistry typeAliasRegistry, IWorkflowDefinitionService workflowDefinitionService)
: FunctionDefinitionProvider
{

View file

@ -2,10 +2,12 @@ using Elsa.Expressions.Contracts;
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.JavaScript.Expressions;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.JavaScript.Providers;
[UsedImplicitly]
internal class JavaScriptExpressionDescriptorProvider : IExpressionDescriptorProvider
{
private const string TypeName = "JavaScript";

View file

@ -1,24 +1,15 @@
using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Text.Unicode;
using Elsa.Expressions.Helpers;
using Elsa.Expressions.Models;
using Elsa.Extensions;
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 Humanizer;
using Esprima.Ast;
using Jint;
using Jint.Native;
using Jint.Native.TypedArray;
using Jint.Runtime.Interop;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
@ -34,7 +25,6 @@ public class JintJavaScriptEvaluator(IConfiguration configuration, INotification
: IJavaScriptEvaluator
{
private readonly JintOptions _jintOptions = scriptOptions.Value;
private readonly JsonSerializerOptions _jsonSerializerOptions = CreateJsonSerializerOptions();
/// <inheritdoc />
public async Task<object?> EvaluateAsync(string expression,
@ -56,164 +46,86 @@ public class JintJavaScriptEvaluator(IConfiguration configuration, INotification
var engine = new Engine(opts =>
{
if (_jintOptions.AllowClrAccess)
opts.AllowClr();
// Wrap objects in ObjectWrapper instances and set their prototype to Array.prototype if they are array-like.
opts.SetWrapObjectHandler((engine, target, type) =>
{
var instance = ObjectWrapper.Create(engine, target);
if (ObjectArrayHelper.DetermineIfObjectIsArrayLikeClrCollection(target.GetType()))
instance.Prototype = engine.Intrinsics.Array.PrototypeObject;
return instance;
});
opts.Interop.ObjectConverters.AddRange([new ByteArrayConverter(), new ExpandoObjectConverter()]);
ConfigureClrAccess(opts);
ConfigureObjectWrapper(opts);
ConfigureObjectConverters(opts);
});
configureEngine?.Invoke(engine);
// Add common functions.
engine.SetValue("getWorkflowDefinitionId", (Func<string>)(() => context.GetWorkflowExecutionContext().Workflow.Identity.DefinitionId));
engine.SetValue("getWorkflowDefinitionVersionId", (Func<string>)(() => context.GetWorkflowExecutionContext().Workflow.Identity.Id));
engine.SetValue("getWorkflowDefinitionVersion", (Func<int>)(() => context.GetWorkflowExecutionContext().Workflow.Identity.Version));
engine.SetValue("getWorkflowInstanceId", (Func<string>)(() => context.GetActivityExecutionContext().WorkflowExecutionContext.Id));
engine.SetValue("setCorrelationId", (Action<string?>)(value => context.GetActivityExecutionContext().WorkflowExecutionContext.CorrelationId = value));
engine.SetValue("getCorrelationId", (Func<string?>)(() => context.GetActivityExecutionContext().WorkflowExecutionContext.CorrelationId));
engine.SetValue("setVariable", (Action<string, object>)((name, value) => context.SetVariableInScope(name, value)));
engine.SetValue("getVariable", (Func<string, object?>)(name => context.GetVariableInScope(name)));
engine.SetValue("getInput", (Func<string, object?>)(name => context.GetInput(name)));
engine.SetValue("getOutputFrom", (Func<string, string?, object?>)((activityIdName, outputName) => context.GetOutput(activityIdName, outputName)));
engine.SetValue("getLastResult", (Func<object?>)(() => context.GetLastResult()));
// Create variable getters and setters for each variable.
CreateVariableAccessors(engine, context);
// Create workflow input accessors - only if the current activity is not part of a composite activity definition.
// Otherwise, the workflow input accessors will hide the composite activity input accessors which rely on variable accessors created above.
CreateWorkflowInputAccessors(engine, context);
// Create output getters for each activity.
await CreateActivityOutputAccessorsAsync(engine, context);
// Create argument getters for each argument.
foreach (var argument in options.Arguments)
engine.SetValue($"get{argument.Key}", (Func<object?>)(() => argument.Value));
// Add common functions.
engine.SetValue("isNullOrWhiteSpace", (Func<string, bool>)(value => string.IsNullOrWhiteSpace(value)));
engine.SetValue("isNullOrEmpty", (Func<string, bool>)(value => string.IsNullOrEmpty(value)));
engine.SetValue("toJson", (Func<object, string>)(value => Serialize(value)));
engine.SetValue("parseGuid", (Func<string, Guid>)(value => Guid.Parse(value)));
engine.SetValue("newGuid", (Func<Guid>)(() => Guid.NewGuid()));
engine.SetValue("newGuidString", (Func<string>)(() => Guid.NewGuid().ToString()));
engine.SetValue("newShortGuid", (Func<string>)(() => Regex.Replace(Convert.ToBase64String(Guid.NewGuid().ToByteArray()), "[/+=]", "")));
// Deprecated, use newGuidString instead.
engine.SetValue("getGuidString", (Func<string>)(() => Guid.NewGuid().ToString()));
// Deprecated, use newShortGuid instead.
engine.SetValue("getShortGuid", (Func<string>)(() => Regex.Replace(Convert.ToBase64String(Guid.NewGuid().ToByteArray()), "[/+=]", "")));
// Create configuration value accessor
if (_jintOptions.AllowConfigurationAccess)
engine.SetValue("getConfig", (Func<string, object?>)(name => configuration.GetSection(name).Value));
// Add common .NET types.
engine.RegisterType<DateTime>();
engine.RegisterType<DateTimeOffset>();
engine.RegisterType<TimeSpan>();
engine.RegisterType<Guid>();
// Invoke registered configuration callback.
ConfigureArgumentGetters(engine, options);
ConfigureConfigurationAccess(engine);
_jintOptions.ConfigureEngineCallback(engine, context);
// Allow listeners invoked by the mediator to configure the engine.
await mediator.SendAsync(new EvaluatingJavaScript(engine, context), cancellationToken);
return engine;
}
private void CreateWorkflowInputAccessors(Engine engine, ExpressionExecutionContext context)
private void ConfigureClrAccess(Jint.Options options)
{
if (context.IsInsideCompositeActivity())
return;
var inputs = context.GetWorkflowInputs().ToDictionary(x => x.Name);
if (!context.TryGetWorkflowExecutionContext(out var workflowExecutionContext))
return;
var inputDefinitions = workflowExecutionContext.Workflow.Inputs;
foreach (var inputDefinition in inputDefinitions)
if (_jintOptions.AllowClrAccess)
options.AllowClr();
}
private void ConfigureObjectWrapper(Jint.Options options)
{
options.SetWrapObjectHandler((engine, target, type) =>
{
var input = inputs.GetValueOrDefault(inputDefinition.Name);
engine.SetValue($"get{inputDefinition.Name}", (Func<object?>)(() => input?.Value));
}
var instance = ObjectWrapper.Create(engine, target);
if (ObjectArrayHelper.DetermineIfObjectIsArrayLikeClrCollection(target.GetType()))
instance.Prototype = engine.Intrinsics.Array.PrototypeObject;
return instance;
});
}
[RequiresUnreferencedCode("Calls Jint.Engine.SetValue<T>(String, T)")]
private static void CreateVariableAccessors(Engine engine, ExpressionExecutionContext context)
private void ConfigureObjectConverters(Jint.Options options)
{
var variableNames = context.GetVariableNamesInScope().ToList();
foreach (var variableName in variableNames)
{
var pascalName = variableName.Pascalize();
engine.SetValue($"get{pascalName}", (Func<object?>)(() => context.GetVariableInScope(variableName)));
engine.SetValue($"set{pascalName}", (Action<object?>)(value => context.SetVariableInScope(variableName, value)));
}
options.Interop.ObjectConverters.AddRange([new ByteArrayConverter(), new ExpandoObjectConverter()]);
}
private static async Task CreateActivityOutputAccessorsAsync(Engine engine, ExpressionExecutionContext context)
private void ConfigureArgumentGetters(Engine engine, ExpressionEvaluatorOptions options)
{
var activityOutputs = context.GetActivityOutputs();
await foreach (var activityOutput in activityOutputs)
foreach (var outputName in activityOutput.OutputNames)
engine.SetValue($"get{outputName}From{activityOutput.ActivityName}", (Func<object?>)(() => context.GetOutput(activityOutput.ActivityId, outputName)));
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.ToObject();
}
private Prepared<Script> GetOrCreatePrepareScript(string expression)
{
var cacheKey = "jint:script:" + Hash(expression);
var parsedScript = memoryCache.GetOrCreate(cacheKey, entry =>
return memoryCache.GetOrCreate(cacheKey, entry =>
{
if (_jintOptions.ScriptCacheTimeout.HasValue)
entry.SetAbsoluteExpiration(_jintOptions.ScriptCacheTimeout.Value);
var prepareOptions = new ScriptPreparationOptions
{
ParsingOptions = new ScriptParsingOptions
{
AllowReturnOutsideFunction = true
}
};
return Engine.PrepareScript(expression, options: prepareOptions);
return PrepareScript(expression);
})!;
var result = engine.Evaluate(parsedScript);
return result.ToObject();
}
[RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Serialize<TValue>(TValue, JsonSerializerOptions)")]
private string Serialize(object value)
private Prepared<Script> PrepareScript(string expression)
{
return JsonSerializer.Serialize(value, _jsonSerializerOptions);
}
private static JsonSerializerOptions CreateJsonSerializerOptions()
{
var options = new JsonSerializerOptions
var prepareOptions = new ScriptPreparationOptions
{
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
ParsingOptions = new ScriptParsingOptions
{
AllowReturnOutsideFunction = true
}
};
options.Converters.Add(new JsonStringEnumConverter());
return options;
return Engine.PrepareScript(expression, options: prepareOptions);
}
private string Hash(string input)

View file

@ -9,29 +9,21 @@ namespace Elsa.JavaScript.TypeDefinitions.Providers;
/// <summary>
/// Produces <see cref="FunctionDefinition"/>s for common functions.
/// </summary>
internal class ActivityOutputFunctionsDefinitionProvider : FunctionDefinitionProvider
internal class ActivityOutputFunctionsDefinitionProvider(
IActivityVisitor activityVisitor,
IActivityRegistryLookupService activityRegistryLookup,
IIdentityGraphService identityGraphService) : FunctionDefinitionProvider
{
private readonly IActivityVisitor _activityVisitor;
private readonly IActivityRegistryLookupService _activityRegistryLookup;
private readonly IIdentityGraphService _identityGraphService;
public ActivityOutputFunctionsDefinitionProvider(IActivityVisitor activityVisitor, IActivityRegistryLookupService activityRegistryLookup, IIdentityGraphService identityGraphService)
{
_activityVisitor = activityVisitor;
_activityRegistryLookup = activityRegistryLookup;
_identityGraphService = identityGraphService;
}
protected override async ValueTask<IEnumerable<FunctionDefinition>> GetFunctionDefinitionsAsync(TypeDefinitionContext context)
{
// Output getters.
var workflow = context.Workflow;
var nodes = (await _activityVisitor.VisitAsync(workflow.Root, context.CancellationToken)).Flatten().Distinct().ToList();
var nodes = (await activityVisitor.VisitAsync(workflow.Root, context.CancellationToken)).Flatten().Distinct().ToList();
// Ensure identities.
await _identityGraphService.AssignIdentitiesAsync(nodes);
await identityGraphService.AssignIdentitiesAsync(nodes);
var activitiesWithOutputs = nodes.GetActivitiesWithOutputs(_activityRegistryLookup);
var activitiesWithOutputs = nodes.GetActivitiesWithOutputs(activityRegistryLookup);
var definitions = new List<FunctionDefinition>();
await foreach (var (activity, activityDescriptor) in activitiesWithOutputs)

View file

@ -14,15 +14,15 @@ internal class CommonFunctionsDefinitionProvider(ITypeAliasRegistry typeAliasReg
yield return CreateFunctionDefinition(builder => builder
.Name("getWorkflowDefinitionId")
.ReturnType("string"));
yield return CreateFunctionDefinition(builder => builder
.Name("getWorkflowDefinitionVersionId")
.ReturnType("string"));
yield return CreateFunctionDefinition(builder => builder
.Name("getWorkflowDefinitionVersion")
.ReturnType("number"));
yield return CreateFunctionDefinition(builder => builder
.Name("getWorkflowInstanceId")
.ReturnType("string"));
@ -55,7 +55,7 @@ internal class CommonFunctionsDefinitionProvider(ITypeAliasRegistry typeAliasReg
.Parameter("activityId", "string")
.Parameter("outputName", "string", true)
.ReturnType("any"));
yield return CreateFunctionDefinition(builder => builder
.Name("getLastResult")
.ReturnType("any"));
@ -92,6 +92,36 @@ internal class CommonFunctionsDefinitionProvider(ITypeAliasRegistry typeAliasReg
.Parameter("value", "any")
.ReturnType("string"));
yield return CreateFunctionDefinition(builder => builder
.Name("bytesToString")
.Parameter("value", "Byte[]")
.ReturnType("string"));
yield return CreateFunctionDefinition(builder => builder
.Name("bytesFromString")
.Parameter("value", "string")
.ReturnType("Byte[]"));
yield return CreateFunctionDefinition(builder => builder
.Name("bytesToBase64")
.Parameter("value", "Byte[]")
.ReturnType("string"));
yield return CreateFunctionDefinition(builder => builder
.Name("bytesFromBase64")
.Parameter("value", "string")
.ReturnType("Byte[]"));
yield return CreateFunctionDefinition(builder => builder
.Name("stringFromBase64")
.Parameter("value", "string")
.ReturnType("string"));
yield return CreateFunctionDefinition(builder => builder
.Name("stringToBase64")
.Parameter("value", "string")
.ReturnType("string"));
// Variable getter and setters.
foreach (var variable in context.Workflow.Variables)
{

View file

@ -8,18 +8,11 @@ namespace Elsa.JavaScript.TypeDefinitions.Providers;
/// <summary>
/// Produces <see cref="FunctionDefinition"/>s for common functions.
/// </summary>
internal class CommonTypeDefinitionProvider : TypeDefinitionProvider
internal class CommonTypeDefinitionProvider(ITypeDescriber typeDescriber) : TypeDefinitionProvider
{
private readonly ITypeDescriber _typeDescriber;
public CommonTypeDefinitionProvider(ITypeDescriber typeDescriber)
{
_typeDescriber = typeDescriber;
}
protected override IEnumerable<TypeDefinition> GetTypeDefinitions(TypeDefinitionContext context)
{
yield return _typeDescriber.DescribeType(typeof(Guid));
yield return _typeDescriber.DescribeType(typeof(JsonObject));
yield return typeDescriber.DescribeType(typeof(Guid));
yield return typeDescriber.DescribeType(typeof(JsonObject));
}
}

View file

@ -9,15 +9,8 @@ namespace Elsa.JavaScript.TypeDefinitions.Providers;
/// <summary>
/// Produces <see cref="TypeDefinition"/>s for variable types.
/// </summary>
internal class VariableTypeDefinitionProvider : TypeDefinitionProvider
internal class VariableTypeDefinitionProvider(ITypeDescriber typeDescriber) : TypeDefinitionProvider
{
private readonly ITypeDescriber _typeDescriber;
public VariableTypeDefinitionProvider(ITypeDescriber typeDescriber)
{
_typeDescriber = typeDescriber;
}
protected override IEnumerable<TypeDefinition> GetTypeDefinitions(TypeDefinitionContext context)
{
var excludedTypes = new Func<Type, bool>[]
@ -39,7 +32,7 @@ internal class VariableTypeDefinitionProvider : TypeDefinitionProvider
foreach (var variableType in variableTypes)
{
yield return _typeDescriber.DescribeType(variableType);
yield return typeDescriber.DescribeType(variableType);
}
}
}

View file

@ -394,7 +394,7 @@ public static class ExpressionExecutionContextExtensions
/// <returns>The value of the specified input.</returns>
public static object? GetInput(this ExpressionExecutionContext context, string name)
{
if (context.IsInsideCompositeActivity())
if (context.IsContainedWithinCompositeActivity())
{
// If there's a variable in the current scope with the specified name, return that.
var variable = context.GetVariable(name);
@ -469,7 +469,7 @@ public static class ExpressionExecutionContextExtensions
/// <summary>
/// Returns a value indicating whether the current activity is inside a composite activity.
/// </summary>
public static bool IsInsideCompositeActivity(this ExpressionExecutionContext context)
public static bool IsContainedWithinCompositeActivity(this ExpressionExecutionContext context)
{
if (!context.TryGetActivityExecutionContext(out var activityExecutionContext))
return false;

View file

@ -0,0 +1,100 @@
using System.Text;
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.JavaScript.Contracts;
using Elsa.Testing.Shared;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
using Xunit.Abstractions;
namespace Elsa.Workflows.IntegrationTests.Scenarios.JavaScriptFunctions;
public class BytesAndStringEncodingTests
{
private readonly IJavaScriptEvaluator _evaluator;
private readonly ExpressionExecutionContext _expressionContext;
public BytesAndStringEncodingTests(ITestOutputHelper testOutputHelper)
{
var testOutputHelper1 = testOutputHelper ?? throw new ArgumentNullException(nameof(testOutputHelper));
var services = new TestApplicationBuilder(testOutputHelper1).Build();
_evaluator = services.GetRequiredService<IJavaScriptEvaluator>();
_expressionContext = new ExpressionExecutionContext(services, new MemoryRegister());
}
[Fact]
public async Task ByteArrayToString_ConvertsTo_String()
{
const string data = "Hello World!";
var bytes = Encoding.UTF8.GetBytes(data);
var script = "bytesToString(getData())";
_expressionContext.SetVariable("Data", bytes);
var result = (string)(await _evaluator.EvaluateAsync(script, typeof(string), _expressionContext))!;
Assert.Equal(data, result);
}
[Fact]
public async Task ByteArrayFromString_ConvertsTo_ByteArray()
{
const string data = "Hello World!";
var script = "bytesFromString(getData())";
_expressionContext.SetVariable("Data", data);
var result = (byte[])(await _evaluator.EvaluateAsync(script, typeof(byte[]), _expressionContext))!;
var bytes = Encoding.UTF8.GetBytes(data);
Assert.Equal(bytes, result);
}
[Fact]
public async Task ByteArrayToBase64_ConvertsTo_Base64()
{
const string data = "Hello World!";
var bytes = Encoding.UTF8.GetBytes(data);
var base64 = Convert.ToBase64String(bytes);
var script = "bytesToBase64(getData())";
_expressionContext.SetVariable("Data", bytes);
var result = (string)(await _evaluator.EvaluateAsync(script, typeof(string), _expressionContext))!;
Assert.Equal(base64, result);
}
[Fact]
public async Task ByteArrayFromBase64_ConvertsTo_ByteArray()
{
const string data = "Hello World!";
var bytes = Encoding.UTF8.GetBytes(data);
var base64 = Convert.ToBase64String(bytes);
var script = "bytesFromBase64(getData())";
_expressionContext.SetVariable("Data", base64);
var result = (byte[])(await _evaluator.EvaluateAsync(script, typeof(byte[]), _expressionContext))!;
Assert.Equal(bytes, result);
}
[Fact]
public async Task StringToBase64_ConvertsTo_Base64()
{
const string data = "Hello World!";
var bytes = Encoding.UTF8.GetBytes(data);
var base64 = Convert.ToBase64String(bytes);
var script = "stringToBase64(getData())";
_expressionContext.SetVariable("Data", data);
var result = (string)(await _evaluator.EvaluateAsync(script, typeof(string), _expressionContext))!;
Assert.Equal(base64, result);
}
[Fact]
public async Task StringFromBase64_ConvertsTo_String()
{
const string data = "Hello World!";
var bytes = Encoding.UTF8.GetBytes(data);
var base64 = Convert.ToBase64String(bytes);
var script = "stringFromBase64(getData())";
_expressionContext.SetVariable("Data", base64);
var result = (string)(await _evaluator.EvaluateAsync(script, typeof(string), _expressionContext))!;
Assert.Equal(data, result);
}
}