Initial version of liquid handler
This commit is contained in:
parent
5ecd599996
commit
6e56d83e4d
7
Elsa.sln
7
Elsa.sln
|
|
@ -78,6 +78,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Persistence.EntityFram
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Serialization", "src\core\Elsa.Serialization\Elsa.Serialization.csproj", "{55F9B33D-5FAF-4355-97EF-445C93208E85}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Scripting.Liquid", "src\scripting\Elsa.Scripting.Liquid\Elsa.Scripting.Liquid.csproj", "{A7473430-F228-4534-AEA5-AA8D80E09364}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -164,6 +166,10 @@ Global
|
|||
{55F9B33D-5FAF-4355-97EF-445C93208E85}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{55F9B33D-5FAF-4355-97EF-445C93208E85}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{55F9B33D-5FAF-4355-97EF-445C93208E85}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A7473430-F228-4534-AEA5-AA8D80E09364}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A7473430-F228-4534-AEA5-AA8D80E09364}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A7473430-F228-4534-AEA5-AA8D80E09364}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A7473430-F228-4534-AEA5-AA8D80E09364}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{155227F0-A33B-40AA-A4B4-06F813EB921B} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F}
|
||||
|
|
@ -198,5 +204,6 @@ Global
|
|||
{D8130816-C1D9-40A3-9079-F2887FE35EA9} = {9F8AE7FB-E5F9-4DCB-9CF8-0362B3D18DAA}
|
||||
{820016B7-01CD-4032-8239-6A4E53F1A352} = {9F8AE7FB-E5F9-4DCB-9CF8-0362B3D18DAA}
|
||||
{55F9B33D-5FAF-4355-97EF-445C93208E85} = {C6658DE0-2B2F-47F0-BB61-2CA66D435C09}
|
||||
{A7473430-F228-4534-AEA5-AA8D80E09364} = {2633B8B9-4AEC-4A54-8832-1C940B54E041}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ using Elsa.Management.Options;
|
|||
using Elsa.Management.Providers;
|
||||
using Elsa.Management.Serialization;
|
||||
using Elsa.Management.Services;
|
||||
using Elsa.Persistence.Mappers;
|
||||
using Elsa.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Management.Extensions;
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export class InputControlSwitch {
|
|||
@Prop() syntax?: string;
|
||||
@Prop() expression?: string;
|
||||
@Prop() defaultSyntax: string = SyntaxNames.Literal;
|
||||
@Prop() supportedSyntaxes: Array<string> = ['JavaScript']; // TODO: Get available syntaxes from some more centralized settings.
|
||||
@Prop() supportedSyntaxes: Array<string> = ['JavaScript', 'Liquid']; // TODO: Get available syntaxes from some more centralized settings.
|
||||
@Prop() isReadOnly?: boolean;
|
||||
@Prop() codeEditorHeight: string = '10em';
|
||||
@Prop() codeEditorSingleLineMode: boolean = false;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using Elsa.Contracts;
|
||||
using Elsa.Models;
|
||||
using Elsa.Scripting.JavaScript;
|
||||
using Elsa.Scripting.JavaScript.Expressions;
|
||||
|
||||
namespace Elsa.Dsl.Interpreters;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
using Elsa.Activities.ControlFlow;
|
||||
using Elsa.Contracts;
|
||||
using Elsa.Models;
|
||||
using Elsa.Scripting.JavaScript;
|
||||
using Elsa.Scripting.JavaScript.Expressions;
|
||||
|
||||
namespace Elsa.Dsl.Interpreters;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Elsa.Mediator.Contracts;
|
||||
|
||||
namespace Elsa.Persistence.Commands;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Elsa.Mediator.Contracts;
|
||||
|
||||
namespace Elsa.Persistence.Commands;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using System.Collections.Generic;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Persistence.Entities;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Persistence.Entities;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Models;
|
||||
using Elsa.Persistence.Entities;
|
||||
|
||||
namespace Elsa.Persistence.Commands;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace Elsa.Persistence.Entities;
|
||||
|
||||
public class WorkflowBookmark : Entity
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Elsa.Contracts;
|
||||
|
||||
namespace Elsa.Persistence.Entities;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using System;
|
||||
using Elsa.State;
|
||||
|
||||
namespace Elsa.Persistence.Entities;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using Elsa.Persistence.Entities;
|
||||
using Elsa.Persistence.Models;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Elsa.Models;
|
||||
using Elsa.Persistence.Models;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Elsa.Models;
|
||||
using Elsa.Persistence.Entities;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Elsa.Contracts;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Models;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
using System;
|
||||
|
||||
namespace Elsa.Persistence.Models;
|
||||
|
||||
public record Cursor(string Id, DateTime Sort);
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace Elsa.Persistence.Models;
|
||||
|
||||
public record PagedList<T>(ICollection<T> Items, int PageSize, int TotalCount);
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
using System;
|
||||
using Elsa.Persistence.Entities;
|
||||
|
||||
namespace Elsa.Persistence.Models;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using System;
|
||||
using Elsa.Persistence.Entities;
|
||||
|
||||
namespace Elsa.Persistence.Models;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using System.Collections.Generic;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Models;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using System.Collections.Generic;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Persistence.Models;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using System.Collections.Generic;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Persistence.Entities;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using System.Collections.Generic;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Persistence.Entities;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using System.Collections.Generic;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Models;
|
||||
using Elsa.Persistence.Models;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using Elsa.Persistence.Models;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
using System.Text.Json;
|
||||
using Elsa.Contracts;
|
||||
using Elsa.Management.Serialization;
|
||||
using Elsa.Management.Serialization.Extensions;
|
||||
using Elsa.Persistence.Entities;
|
||||
using Elsa.Persistence.EntityFrameworkCore.Contracts;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
using System.Text.Json;
|
||||
using Elsa.Contracts;
|
||||
using Elsa.Management.Serialization;
|
||||
using Elsa.Management.Serialization.Extensions;
|
||||
using Elsa.Persistence.Entities;
|
||||
using Elsa.Persistence.EntityFrameworkCore.Contracts;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using System.Text.Json;
|
||||
using Elsa.Management.Serialization;
|
||||
using Elsa.Management.Serialization.Extensions;
|
||||
using Elsa.Persistence.Entities;
|
||||
using Elsa.Persistence.EntityFrameworkCore.Contracts;
|
||||
using Elsa.State;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
<ProjectReference Include="..\..\..\core\Elsa\Elsa.csproj" />
|
||||
<ProjectReference Include="..\..\..\persistence\Elsa.Persistence.EntityFrameworkCore.Sqlite\Elsa.Persistence.EntityFrameworkCore.Sqlite.csproj" />
|
||||
<ProjectReference Include="..\..\..\runtime\Elsa.Runtime.ProtoActor\Elsa.Runtime.ProtoActor.csproj" />
|
||||
<ProjectReference Include="..\..\..\scripting\Elsa.Scripting.Liquid\Elsa.Scripting.Liquid.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ using Elsa.Pipelines.WorkflowExecution.Components;
|
|||
using Elsa.Runtime.ProtoActor.Extensions;
|
||||
using Elsa.Samples.Web1.Activities;
|
||||
using Elsa.Samples.Web1.Workflows;
|
||||
using Elsa.Scripting.JavaScript;
|
||||
using Elsa.Scripting.JavaScript.Extensions;
|
||||
using Elsa.Scripting.Liquid.Extensions;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
|
@ -59,7 +59,9 @@ services
|
|||
.AddTrigger<HttpTrigger>();
|
||||
|
||||
// Register scripting languages.
|
||||
services.AddJavaScriptExpressions();
|
||||
services
|
||||
.AddJavaScriptExpressions()
|
||||
.AddLiquidExpressions();
|
||||
|
||||
// Configure middleware pipeline.
|
||||
var app = builder.Build();
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ using Elsa.Dsl.Extensions;
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Models;
|
||||
using Elsa.Persistence.InMemory.Extensions;
|
||||
using Elsa.Scripting.JavaScript;
|
||||
using Elsa.Scripting.JavaScript.Extensions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Elsa.Contracts;
|
||||
|
||||
namespace Elsa.Scripting.JavaScript.Expressions;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
using Fluid;
|
||||
using Fluid.Values;
|
||||
|
||||
namespace Elsa.Scripting.Liquid.Services
|
||||
{
|
||||
public interface ILiquidFilter
|
||||
{
|
||||
ValueTask<FluidValue> ProcessAsync(FluidValue input, FilterArguments arguments, TemplateContext ctx);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
using Elsa.Models;
|
||||
using Fluid;
|
||||
|
||||
namespace Elsa.Scripting.Liquid.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides services to render Liquid templates.
|
||||
/// </summary>
|
||||
public interface ILiquidTemplateManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Renders a Liquid template as a <see cref="string"/>.
|
||||
/// </summary>
|
||||
Task<string?> RenderAsync(string template, ExpressionExecutionContext expressionExecutionContext, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Validates a Liquid template.
|
||||
/// </summary>
|
||||
bool Validate(string template, out string error);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Fluid.Core" Version="2.2.13" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\core\Elsa.Management\Elsa.Management.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.Extensions.Caching.Abstractions, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
|
||||
<HintPath>..\..\..\..\..\..\Program Files\dotnet\shared\Microsoft.AspNetCore.App\6.0.0\Microsoft.Extensions.Caching.Abstractions.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Extensions.Configuration.Abstractions, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
|
||||
<HintPath>..\..\..\..\..\..\Program Files\dotnet\shared\Microsoft.AspNetCore.App\6.0.0\Microsoft.Extensions.Configuration.Abstractions.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
using Elsa.Contracts;
|
||||
|
||||
namespace Elsa.Scripting.Liquid.Expressions;
|
||||
|
||||
public class LiquidExpression : IExpression
|
||||
{
|
||||
public LiquidExpression(string value) => Value = value;
|
||||
public string Value { get; }
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
using Elsa.Contracts;
|
||||
using Elsa.Helpers;
|
||||
using Elsa.Models;
|
||||
using Elsa.Scripting.Liquid.Contracts;
|
||||
using Elsa.Scripting.Liquid.Services;
|
||||
|
||||
namespace Elsa.Scripting.Liquid.Expressions;
|
||||
|
||||
public class LiquidExpressionHandler : IExpressionHandler
|
||||
{
|
||||
private readonly ILiquidTemplateManager _liquidTemplateManager;
|
||||
|
||||
public LiquidExpressionHandler(ILiquidTemplateManager liquidTemplateManager)
|
||||
{
|
||||
_liquidTemplateManager = liquidTemplateManager;
|
||||
}
|
||||
|
||||
public async ValueTask<object?> EvaluateAsync(IExpression expression, Type returnType, ExpressionExecutionContext context)
|
||||
{
|
||||
var liquidExpression = (LiquidExpression)expression;
|
||||
var renderedString = await _liquidTemplateManager.RenderAsync(liquidExpression.Value, context);
|
||||
return renderedString.ConvertTo(returnType);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
using Elsa.Models;
|
||||
|
||||
namespace Elsa.Scripting.Liquid.Expressions;
|
||||
|
||||
public class LiquidExpressionReference : RegisterLocationReference
|
||||
{
|
||||
public LiquidExpressionReference(LiquidExpression expression) => Expression = expression;
|
||||
public LiquidExpression Expression { get; }
|
||||
public override RegisterLocation Declare() => new();
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Management.Contracts;
|
||||
using Elsa.Mediator.Extensions;
|
||||
using Elsa.Scripting.Liquid.Contracts;
|
||||
using Elsa.Scripting.Liquid.Expressions;
|
||||
using Elsa.Scripting.Liquid.Filters;
|
||||
using Elsa.Scripting.Liquid.Handlers;
|
||||
using Elsa.Scripting.Liquid.Options;
|
||||
using Elsa.Scripting.Liquid.Providers;
|
||||
using Elsa.Scripting.Liquid.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Scripting.Liquid.Extensions;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddLiquidExpressions(this IServiceCollection services)
|
||||
{
|
||||
return services
|
||||
.AddMemoryCache()
|
||||
.AddHandlersFrom<ConfigureLiquidEngine>()
|
||||
.AddSingleton<IExpressionSyntaxProvider, LiquidExpressionSyntaxProvider>()
|
||||
.AddSingleton<ILiquidTemplateManager, LiquidTemplateManager>()
|
||||
.AddSingleton<LiquidParser>()
|
||||
.AddExpressionHandler<LiquidExpressionHandler, LiquidExpression>()
|
||||
.AddLiquidFilter<JsonFilter>("json")
|
||||
.AddLiquidFilter<Base64Filter>("base64");
|
||||
}
|
||||
|
||||
public static IServiceCollection AddLiquidFilter<T>(this IServiceCollection services, string name) where T : class, ILiquidFilter
|
||||
{
|
||||
services.Configure<FluidOptions>(options => options.FilterRegistrations[name] = typeof(T));
|
||||
services.AddScoped<T>();
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection RegisterLiquidTag(this IServiceCollection services, Action<LiquidParser> configure)
|
||||
{
|
||||
services.Configure<FluidOptions>(options => options.ParserConfiguration.Add(configure));
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables access to .NET configuration via the Configuration keyword.
|
||||
/// Do not enable this option if you execute user supplied (or otherwise untrusted) workflows.
|
||||
/// </summary>
|
||||
public static IServiceCollection EnableLiquidConfigurationAccess(this IServiceCollection services)
|
||||
{
|
||||
services.Configure<FluidOptions>(options => options.AllowConfigurationAccess = true);
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
using Elsa.Scripting.Liquid.Options;
|
||||
using Elsa.Scripting.Liquid.Services;
|
||||
using Fluid;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Scripting.Liquid.Extensions
|
||||
{
|
||||
public static class TemplateContextExtensions
|
||||
{
|
||||
internal static void AddFilters(this TemplateContext templateContext, FluidOptions options, IServiceProvider services)
|
||||
{
|
||||
foreach (var (key, value) in options.FilterRegistrations)
|
||||
{
|
||||
templateContext.Options.Filters.AddFilter(key, (input, arguments, ctx) =>
|
||||
{
|
||||
var filter = (ILiquidFilter)services.GetRequiredService(value);
|
||||
return filter.ProcessAsync(input, arguments, ctx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/scripting/Elsa.Scripting.Liquid/Filters/Base64Filter.cs
Normal file
39
src/scripting/Elsa.Scripting.Liquid/Filters/Base64Filter.cs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Elsa.Scripting.Liquid.Services;
|
||||
using Fluid;
|
||||
using Fluid.Values;
|
||||
|
||||
namespace Elsa.Scripting.Liquid.Filters
|
||||
{
|
||||
public class Base64Filter : ILiquidFilter
|
||||
{
|
||||
public ValueTask<FluidValue> ProcessAsync(FluidValue input, FilterArguments arguments, TemplateContext context)
|
||||
{
|
||||
var text = InputToString(input, context);
|
||||
|
||||
if (text == null)
|
||||
return new ValueTask<FluidValue>(NilValue.Instance);
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(text);
|
||||
var base64 = Convert.ToBase64String(bytes);
|
||||
|
||||
return new ValueTask<FluidValue>(new StringValue(base64));
|
||||
}
|
||||
|
||||
private string? InputToString(FluidValue input, TemplateContext context)
|
||||
{
|
||||
return input.Type switch
|
||||
{
|
||||
FluidValues.Array => JsonSerializer.Serialize(input.Enumerate(context).Select(o => o.ToObjectValue())),
|
||||
FluidValues.Boolean => input.ToBooleanValue().ToString(),
|
||||
FluidValues.Nil => null,
|
||||
FluidValues.Number => input.ToNumberValue().ToString(CultureInfo.InvariantCulture),
|
||||
FluidValues.DateTime or FluidValues.Dictionary or FluidValues.Object => JsonSerializer.Serialize(input.ToObjectValue()),
|
||||
FluidValues.String => input.ToStringValue(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(input), "Unrecognized FluidValue")
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
42
src/scripting/Elsa.Scripting.Liquid/Filters/JsonFilter.cs
Normal file
42
src/scripting/Elsa.Scripting.Liquid/Filters/JsonFilter.cs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
using System.Text.Json;
|
||||
using Elsa.Scripting.Liquid.Services;
|
||||
using Fluid;
|
||||
using Fluid.Values;
|
||||
|
||||
namespace Elsa.Scripting.Liquid.Filters
|
||||
{
|
||||
public class JsonFilter : ILiquidFilter
|
||||
{
|
||||
public ValueTask<FluidValue> ProcessAsync(FluidValue input, FilterArguments arguments, TemplateContext context)
|
||||
{
|
||||
switch (input.Type)
|
||||
{
|
||||
case FluidValues.Array:
|
||||
return new ValueTask<FluidValue>(new StringValue(JsonSerializer.Serialize(input.Enumerate(context).Select(o => o.ToObjectValue()))));
|
||||
|
||||
case FluidValues.Boolean:
|
||||
return new ValueTask<FluidValue>(new StringValue(JsonSerializer.Serialize(input.ToBooleanValue())));
|
||||
|
||||
case FluidValues.Nil:
|
||||
return new ValueTask<FluidValue>(FluidValue.Create("null", context.Options));
|
||||
|
||||
case FluidValues.Number:
|
||||
return new ValueTask<FluidValue>(new StringValue(JsonSerializer.Serialize(input.ToNumberValue())));
|
||||
|
||||
case FluidValues.DateTime:
|
||||
case FluidValues.Dictionary:
|
||||
case FluidValues.Object:
|
||||
return new ValueTask<FluidValue>(new StringValue(JsonSerializer.Serialize(input.ToObjectValue())));
|
||||
|
||||
case FluidValues.String:
|
||||
var stringValue = input.ToStringValue();
|
||||
|
||||
return string.IsNullOrWhiteSpace(stringValue)
|
||||
? new ValueTask<FluidValue>(input)
|
||||
: new ValueTask<FluidValue>(new StringValue(JsonSerializer.Serialize(stringValue)));
|
||||
}
|
||||
|
||||
throw new NotSupportedException("Unrecognized FluidValue");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
using System.Dynamic;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Models;
|
||||
using Elsa.Scripting.Liquid.Helpers;
|
||||
using Elsa.Scripting.Liquid.Notifications;
|
||||
using Elsa.Scripting.Liquid.Options;
|
||||
using Fluid;
|
||||
using Fluid.Values;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Elsa.Scripting.Liquid.Handlers
|
||||
{
|
||||
public class ConfigureLiquidEngine : INotificationHandler<RenderingLiquidTemplate>
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly FluidOptions _fluidOptions;
|
||||
|
||||
public ConfigureLiquidEngine(IConfiguration configuration, IOptions<FluidOptions> fluidOptions)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_fluidOptions = fluidOptions.Value;
|
||||
}
|
||||
|
||||
public Task HandleAsync(RenderingLiquidTemplate notification, CancellationToken cancellationToken)
|
||||
{
|
||||
var context = notification.TemplateContext;
|
||||
var options = context.Options;
|
||||
var memberAccessStrategy = options.MemberAccessStrategy;
|
||||
|
||||
memberAccessStrategy.Register<ExpandoObject>();
|
||||
memberAccessStrategy.Register<LiquidPropertyAccessor, FluidValue>((x, name) => x.GetValueAsync(name));
|
||||
memberAccessStrategy.Register<ExpandoObject, object>((x, name) => ((IDictionary<string, object>)x!)[name]);
|
||||
|
||||
if (_fluidOptions.AllowConfigurationAccess)
|
||||
{
|
||||
memberAccessStrategy.Register<ActivityExecutionContext, LiquidPropertyAccessor>("Configuration", x => new LiquidPropertyAccessor(name => ToFluidValue(GetConfigurationValue(name), options)!));
|
||||
memberAccessStrategy.Register<ConfigurationSectionWrapper, ConfigurationSectionWrapper?>((source, name) => source.GetSection(name));
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private ConfigurationSectionWrapper GetConfigurationValue(string name) => new(_configuration.GetSection(name));
|
||||
private Task<FluidValue> ToFluidValue(object? input, TemplateOptions options) => Task.FromResult(FluidValue.Create(input, options));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Elsa.Scripting.Liquid.Helpers
|
||||
{
|
||||
public class ConfigurationSectionWrapper
|
||||
{
|
||||
private readonly IConfigurationSection _section;
|
||||
|
||||
public ConfigurationSectionWrapper(IConfigurationSection section)
|
||||
{
|
||||
_section = section;
|
||||
}
|
||||
|
||||
public override string ToString() => _section.Value;
|
||||
|
||||
public ConfigurationSectionWrapper GetSection(string name) => new(_section.GetSection(name));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
using Elsa.Models;
|
||||
|
||||
namespace Elsa.Scripting.Liquid.Helpers
|
||||
{
|
||||
public record LiquidActivityModel(ActivityExecutionContext ActivityExecutionContext, string? ActivityName, string? ActivityId)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
namespace Elsa.Scripting.Liquid.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Can be used to provide a factory to return an object based on a property name that is unknown at registration time.
|
||||
/// </summary>
|
||||
public class LiquidObjectAccessor<T>
|
||||
{
|
||||
private readonly Func<string, Task<T>> _getter;
|
||||
public LiquidObjectAccessor(Func<string, Task<T>> getter) => _getter = getter;
|
||||
public Task<T> GetValueAsync(string identifier) => _getter(identifier);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
using Fluid.Values;
|
||||
|
||||
namespace Elsa.Scripting.Liquid.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Can be used to provide a factory to return a value based on a property name
|
||||
/// that is unknown at registration time.
|
||||
///
|
||||
/// e.g. {{ LiquidPropertyAccessor.MyPropertyName }} (MyPropertyName will be passed as the identifier argument to the factory)
|
||||
/// </summary>
|
||||
public class LiquidPropertyAccessor : LiquidObjectAccessor<FluidValue>
|
||||
{
|
||||
public LiquidPropertyAccessor(Func<string, Task<FluidValue>> getter) : base(getter!)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Models;
|
||||
using Fluid;
|
||||
|
||||
namespace Elsa.Scripting.Liquid.Notifications;
|
||||
|
||||
public record RenderingLiquidTemplate(TemplateContext TemplateContext, ExpressionExecutionContext Context) : INotification;
|
||||
11
src/scripting/Elsa.Scripting.Liquid/Options/FluidOptions.cs
Normal file
11
src/scripting/Elsa.Scripting.Liquid/Options/FluidOptions.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using Elsa.Scripting.Liquid.Services;
|
||||
|
||||
namespace Elsa.Scripting.Liquid.Options
|
||||
{
|
||||
public class FluidOptions
|
||||
{
|
||||
public Dictionary<string, Type> FilterRegistrations { get; } = new();
|
||||
public IList<Action<LiquidParser>> ParserConfiguration { get; } = new List<Action<LiquidParser>>();
|
||||
public bool AllowConfigurationAccess { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
using Elsa.Contracts;
|
||||
using Elsa.Management.Contracts;
|
||||
using Elsa.Management.Models;
|
||||
using Elsa.Scripting.Liquid.Expressions;
|
||||
|
||||
namespace Elsa.Scripting.Liquid.Providers;
|
||||
|
||||
public class LiquidExpressionSyntaxProvider : IExpressionSyntaxProvider
|
||||
{
|
||||
public const string SyntaxName = "Liquid";
|
||||
private readonly IIdentityGenerator _identityGenerator;
|
||||
public LiquidExpressionSyntaxProvider(IIdentityGenerator identityGenerator) => _identityGenerator = identityGenerator;
|
||||
|
||||
public ValueTask<IEnumerable<ExpressionSyntaxDescriptor>> GetDescriptorsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var liquidDescriptor = CreateLiquidDescriptor();
|
||||
|
||||
return ValueTask.FromResult<IEnumerable<ExpressionSyntaxDescriptor>>(new[] { liquidDescriptor });
|
||||
}
|
||||
|
||||
private ExpressionSyntaxDescriptor CreateLiquidDescriptor() => new()
|
||||
{
|
||||
Syntax = SyntaxName,
|
||||
Type = typeof(LiquidExpression),
|
||||
CreateExpression = CreateJavaScriptExpression,
|
||||
CreateLocationReference = context =>
|
||||
{
|
||||
var reference = new LiquidExpressionReference(context.GetExpression<LiquidExpression>());
|
||||
|
||||
if (string.IsNullOrWhiteSpace(reference.Id))
|
||||
reference.Id = GenerateId();
|
||||
|
||||
return reference;
|
||||
},
|
||||
CreateSerializableObject = context => new
|
||||
{
|
||||
Type = SyntaxName,
|
||||
context.GetExpression<LiquidExpression>().Value
|
||||
}
|
||||
};
|
||||
|
||||
private IExpression CreateJavaScriptExpression(ExpressionConstructorContext context)
|
||||
{
|
||||
var script = context.Element.TryGetProperty("value", out var p) ? p.ToString() : "";
|
||||
return new LiquidExpression(script);
|
||||
}
|
||||
|
||||
private string GenerateId() => _identityGenerator.GenerateId();
|
||||
}
|
||||
15
src/scripting/Elsa.Scripting.Liquid/Services/LiquidParser.cs
Normal file
15
src/scripting/Elsa.Scripting.Liquid/Services/LiquidParser.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
using Elsa.Scripting.Liquid.Options;
|
||||
using Fluid;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Elsa.Scripting.Liquid.Services
|
||||
{
|
||||
public class LiquidParser : FluidParser
|
||||
{
|
||||
public LiquidParser(IOptions<FluidOptions> options)
|
||||
{
|
||||
foreach (var configuration in options.Value.ParserConfiguration)
|
||||
configuration(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
using System.Text.Encodings.Web;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Models;
|
||||
using Elsa.Scripting.Liquid.Contracts;
|
||||
using Elsa.Scripting.Liquid.Notifications;
|
||||
using Fluid;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
namespace Elsa.Scripting.Liquid.Services
|
||||
{
|
||||
public class LiquidTemplateManager : ILiquidTemplateManager
|
||||
{
|
||||
private readonly LiquidParser _parser;
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly IPublisher _publisher;
|
||||
|
||||
public LiquidTemplateManager(LiquidParser parser, IMemoryCache memoryCache, IPublisher publisher)
|
||||
{
|
||||
_parser = parser;
|
||||
_memoryCache = memoryCache;
|
||||
_publisher = publisher;
|
||||
}
|
||||
|
||||
public async Task<string?> RenderAsync(string template, ExpressionExecutionContext expressionExecutionContext, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(template))
|
||||
return default!;
|
||||
|
||||
var result = GetCachedTemplate(template);
|
||||
|
||||
var templateContext = await CreateTemplateContextAsync(expressionExecutionContext, cancellationToken);
|
||||
|
||||
return await result.RenderAsync(templateContext, HtmlEncoder.Default);
|
||||
}
|
||||
|
||||
private IFluidTemplate GetCachedTemplate(string source)
|
||||
{
|
||||
var result = _memoryCache.GetOrCreate(
|
||||
source,
|
||||
e =>
|
||||
{
|
||||
if (!TryParse(source, out var parsed, out var error))
|
||||
TryParse(error, out parsed, out error);
|
||||
|
||||
// TODO: add signal based cache invalidation.
|
||||
e.SetSlidingExpiration(TimeSpan.FromSeconds(30));
|
||||
return parsed;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool Validate(string template, out string error) => TryParse(template, out _, out error);
|
||||
|
||||
private bool TryParse(string template, out IFluidTemplate result, out string error) => _parser.TryParse(template, out result, out error);
|
||||
|
||||
private async Task<TemplateContext> CreateTemplateContextAsync(ExpressionExecutionContext expressionExecutionContext, CancellationToken cancellationToken)
|
||||
{
|
||||
var context = new TemplateContext(expressionExecutionContext, new TemplateOptions());
|
||||
await _publisher.PublishAsync(new RenderingLiquidTemplate(context, expressionExecutionContext), cancellationToken);
|
||||
context.SetValue("ExpressionExecutionContext", expressionExecutionContext);
|
||||
return context;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue