From 6e56d83e4de444c8cc6a5f415dacadbb8c5d3bf6 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 12 Jan 2022 12:37:40 +0100 Subject: [PATCH] Initial version of liquid handler --- Elsa.sln | 7 ++ .../Extensions/ServiceCollectionExtensions.cs | 2 - .../input-control-switch.tsx | 2 +- .../VisitExternalExpression.cs | 1 - .../VisitIfStatement.cs | 1 - .../Commands/DeleteWorkflowBookmarks.cs | 2 - .../Commands/DeleteWorkflowInstances.cs | 2 - .../Commands/ReplaceWorkflowTriggers.cs | 1 - .../Commands/SaveWorkflowBookmarks.cs | 2 - .../Commands/SaveWorkflowExecutionLog.cs | 1 - .../Entities/WorkflowBookmark.cs | 2 - .../Entities/WorkflowDefinition.cs | 2 - .../Entities/WorkflowInstance.cs | 1 - .../WorkflowDefinitionExtensions.cs | 3 - .../Extensions/WorkflowExtensions.cs | 2 - .../Mappers/WorkflowDefinitionMapper.cs | 2 - .../PersistWorkflowInstanceMiddleware.cs | 3 - .../Models/Cursor.cs | 2 - .../Models/PagedList.cs | 2 - .../Models/WorkflowInstanceSummary.cs | 1 - .../Models/WorkflowSummary.cs | 1 - .../FindLatestAndPublishedWorkflows.cs | 1 - .../Requests/FindManyWorkflowDefinitions.cs | 1 - .../Requests/FindWorkflowBookmarks.cs | 1 - .../Requests/FindWorkflowTriggers.cs | 1 - .../Requests/ListWorkflows.cs | 1 - .../VersionOptionsTypeConverter.cs | 3 +- .../WorkflowDefinitionSerializer.cs | 1 - .../WorkflowExecutionLogRecordSerializer.cs | 2 - .../WorkflowInstanceSerializer.cs | 1 - .../Elsa.Samples.Web1.csproj | 1 + .../aspnet/Elsa.Samples.Web1/Program.cs | 6 +- .../console/Elsa.Samples.Console2/Program.cs | 1 - .../Expressions/JavaScriptExpression.cs | 2 - .../Contracts/ILiquidFilter.cs | 10 +++ .../Contracts/ILiquidTemplateManager.cs | 21 ++++++ .../Elsa.Scripting.Liquid.csproj | 27 ++++++++ .../Expressions/LiquidExpression.cs | 9 +++ .../Expressions/LiquidExpressionHandler.cs | 24 +++++++ .../Expressions/LiquidExpressionReference.cs | 10 +++ .../Extensions/ServiceCollectionExtensions.cs | 52 +++++++++++++++ .../Extensions/TemplateContextExtensions.cs | 22 +++++++ .../Filters/Base64Filter.cs | 39 +++++++++++ .../Filters/JsonFilter.cs | 42 ++++++++++++ .../Handlers/ConfigureLiquidEngine.cs | 47 ++++++++++++++ .../Helpers/ConfigurationSectionWrapper.cs | 18 ++++++ .../Helpers/LiquidActivityModel.cs | 8 +++ .../Helpers/LiquidObjectAccessor.cs | 12 ++++ .../Helpers/LiquidPropertyAccessor.cs | 17 +++++ .../Notifications/RenderingLiquidTemplate.cs | 7 ++ .../Options/FluidOptions.cs | 11 ++++ .../LiquidExpressionSyntaxProvider.cs | 49 ++++++++++++++ .../Services/LiquidParser.cs | 15 +++++ .../Services/LiquidTemplateManager.cs | 64 +++++++++++++++++++ 54 files changed, 518 insertions(+), 50 deletions(-) create mode 100644 src/scripting/Elsa.Scripting.Liquid/Contracts/ILiquidFilter.cs create mode 100644 src/scripting/Elsa.Scripting.Liquid/Contracts/ILiquidTemplateManager.cs create mode 100644 src/scripting/Elsa.Scripting.Liquid/Elsa.Scripting.Liquid.csproj create mode 100644 src/scripting/Elsa.Scripting.Liquid/Expressions/LiquidExpression.cs create mode 100644 src/scripting/Elsa.Scripting.Liquid/Expressions/LiquidExpressionHandler.cs create mode 100644 src/scripting/Elsa.Scripting.Liquid/Expressions/LiquidExpressionReference.cs create mode 100644 src/scripting/Elsa.Scripting.Liquid/Extensions/ServiceCollectionExtensions.cs create mode 100644 src/scripting/Elsa.Scripting.Liquid/Extensions/TemplateContextExtensions.cs create mode 100644 src/scripting/Elsa.Scripting.Liquid/Filters/Base64Filter.cs create mode 100644 src/scripting/Elsa.Scripting.Liquid/Filters/JsonFilter.cs create mode 100644 src/scripting/Elsa.Scripting.Liquid/Handlers/ConfigureLiquidEngine.cs create mode 100644 src/scripting/Elsa.Scripting.Liquid/Helpers/ConfigurationSectionWrapper.cs create mode 100644 src/scripting/Elsa.Scripting.Liquid/Helpers/LiquidActivityModel.cs create mode 100644 src/scripting/Elsa.Scripting.Liquid/Helpers/LiquidObjectAccessor.cs create mode 100644 src/scripting/Elsa.Scripting.Liquid/Helpers/LiquidPropertyAccessor.cs create mode 100644 src/scripting/Elsa.Scripting.Liquid/Notifications/RenderingLiquidTemplate.cs create mode 100644 src/scripting/Elsa.Scripting.Liquid/Options/FluidOptions.cs create mode 100644 src/scripting/Elsa.Scripting.Liquid/Providers/LiquidExpressionSyntaxProvider.cs create mode 100644 src/scripting/Elsa.Scripting.Liquid/Services/LiquidParser.cs create mode 100644 src/scripting/Elsa.Scripting.Liquid/Services/LiquidTemplateManager.cs diff --git a/Elsa.sln b/Elsa.sln index bb551fd90..f851333bf 100644 --- a/Elsa.sln +++ b/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 diff --git a/src/core/Elsa.Management/Extensions/ServiceCollectionExtensions.cs b/src/core/Elsa.Management/Extensions/ServiceCollectionExtensions.cs index 82267c3f8..98928b654 100644 --- a/src/core/Elsa.Management/Extensions/ServiceCollectionExtensions.cs +++ b/src/core/Elsa.Management/Extensions/ServiceCollectionExtensions.cs @@ -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; diff --git a/src/designer/elsa-workflows-designer/src/components/designer/input-control-switch/input-control-switch.tsx b/src/designer/elsa-workflows-designer/src/components/designer/input-control-switch/input-control-switch.tsx index c4e74b372..f32dccf95 100644 --- a/src/designer/elsa-workflows-designer/src/components/designer/input-control-switch/input-control-switch.tsx +++ b/src/designer/elsa-workflows-designer/src/components/designer/input-control-switch/input-control-switch.tsx @@ -26,7 +26,7 @@ export class InputControlSwitch { @Prop() syntax?: string; @Prop() expression?: string; @Prop() defaultSyntax: string = SyntaxNames.Literal; - @Prop() supportedSyntaxes: Array = ['JavaScript']; // TODO: Get available syntaxes from some more centralized settings. + @Prop() supportedSyntaxes: Array = ['JavaScript', 'Liquid']; // TODO: Get available syntaxes from some more centralized settings. @Prop() isReadOnly?: boolean; @Prop() codeEditorHeight: string = '10em'; @Prop() codeEditorSingleLineMode: boolean = false; diff --git a/src/dsl/Elsa.Dsl/Interpreters/WorkflowDefinitionBuilderInterpreter/VisitExternalExpression.cs b/src/dsl/Elsa.Dsl/Interpreters/WorkflowDefinitionBuilderInterpreter/VisitExternalExpression.cs index a41dc0dfa..97d29b0b6 100644 --- a/src/dsl/Elsa.Dsl/Interpreters/WorkflowDefinitionBuilderInterpreter/VisitExternalExpression.cs +++ b/src/dsl/Elsa.Dsl/Interpreters/WorkflowDefinitionBuilderInterpreter/VisitExternalExpression.cs @@ -1,6 +1,5 @@ using Elsa.Contracts; using Elsa.Models; -using Elsa.Scripting.JavaScript; using Elsa.Scripting.JavaScript.Expressions; namespace Elsa.Dsl.Interpreters; diff --git a/src/dsl/Elsa.Dsl/Interpreters/WorkflowDefinitionBuilderInterpreter/VisitIfStatement.cs b/src/dsl/Elsa.Dsl/Interpreters/WorkflowDefinitionBuilderInterpreter/VisitIfStatement.cs index 84c5cd392..27d69967a 100644 --- a/src/dsl/Elsa.Dsl/Interpreters/WorkflowDefinitionBuilderInterpreter/VisitIfStatement.cs +++ b/src/dsl/Elsa.Dsl/Interpreters/WorkflowDefinitionBuilderInterpreter/VisitIfStatement.cs @@ -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; diff --git a/src/persistence/Elsa.Persistence.Abstractions/Commands/DeleteWorkflowBookmarks.cs b/src/persistence/Elsa.Persistence.Abstractions/Commands/DeleteWorkflowBookmarks.cs index fb5f73fa0..5bdd631c4 100644 --- a/src/persistence/Elsa.Persistence.Abstractions/Commands/DeleteWorkflowBookmarks.cs +++ b/src/persistence/Elsa.Persistence.Abstractions/Commands/DeleteWorkflowBookmarks.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.Linq; using Elsa.Mediator.Contracts; namespace Elsa.Persistence.Commands; diff --git a/src/persistence/Elsa.Persistence.Abstractions/Commands/DeleteWorkflowInstances.cs b/src/persistence/Elsa.Persistence.Abstractions/Commands/DeleteWorkflowInstances.cs index dc1488085..7e83df811 100644 --- a/src/persistence/Elsa.Persistence.Abstractions/Commands/DeleteWorkflowInstances.cs +++ b/src/persistence/Elsa.Persistence.Abstractions/Commands/DeleteWorkflowInstances.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.Linq; using Elsa.Mediator.Contracts; namespace Elsa.Persistence.Commands; diff --git a/src/persistence/Elsa.Persistence.Abstractions/Commands/ReplaceWorkflowTriggers.cs b/src/persistence/Elsa.Persistence.Abstractions/Commands/ReplaceWorkflowTriggers.cs index 370d0f97c..cf6595c70 100644 --- a/src/persistence/Elsa.Persistence.Abstractions/Commands/ReplaceWorkflowTriggers.cs +++ b/src/persistence/Elsa.Persistence.Abstractions/Commands/ReplaceWorkflowTriggers.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using Elsa.Mediator.Contracts; using Elsa.Persistence.Entities; diff --git a/src/persistence/Elsa.Persistence.Abstractions/Commands/SaveWorkflowBookmarks.cs b/src/persistence/Elsa.Persistence.Abstractions/Commands/SaveWorkflowBookmarks.cs index 38d30a288..cbf81b1e2 100644 --- a/src/persistence/Elsa.Persistence.Abstractions/Commands/SaveWorkflowBookmarks.cs +++ b/src/persistence/Elsa.Persistence.Abstractions/Commands/SaveWorkflowBookmarks.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.Linq; using Elsa.Mediator.Contracts; using Elsa.Persistence.Entities; diff --git a/src/persistence/Elsa.Persistence.Abstractions/Commands/SaveWorkflowExecutionLog.cs b/src/persistence/Elsa.Persistence.Abstractions/Commands/SaveWorkflowExecutionLog.cs index 54bfc85e6..ab1da783b 100644 --- a/src/persistence/Elsa.Persistence.Abstractions/Commands/SaveWorkflowExecutionLog.cs +++ b/src/persistence/Elsa.Persistence.Abstractions/Commands/SaveWorkflowExecutionLog.cs @@ -1,5 +1,4 @@ using Elsa.Mediator.Contracts; -using Elsa.Models; using Elsa.Persistence.Entities; namespace Elsa.Persistence.Commands; diff --git a/src/persistence/Elsa.Persistence.Abstractions/Entities/WorkflowBookmark.cs b/src/persistence/Elsa.Persistence.Abstractions/Entities/WorkflowBookmark.cs index 477e05367..18e74446c 100644 --- a/src/persistence/Elsa.Persistence.Abstractions/Entities/WorkflowBookmark.cs +++ b/src/persistence/Elsa.Persistence.Abstractions/Entities/WorkflowBookmark.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; - namespace Elsa.Persistence.Entities; public class WorkflowBookmark : Entity diff --git a/src/persistence/Elsa.Persistence.Abstractions/Entities/WorkflowDefinition.cs b/src/persistence/Elsa.Persistence.Abstractions/Entities/WorkflowDefinition.cs index 526e7c976..fddf9228c 100644 --- a/src/persistence/Elsa.Persistence.Abstractions/Entities/WorkflowDefinition.cs +++ b/src/persistence/Elsa.Persistence.Abstractions/Entities/WorkflowDefinition.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using Elsa.Contracts; namespace Elsa.Persistence.Entities; diff --git a/src/persistence/Elsa.Persistence.Abstractions/Entities/WorkflowInstance.cs b/src/persistence/Elsa.Persistence.Abstractions/Entities/WorkflowInstance.cs index be2a510b6..b0e721604 100644 --- a/src/persistence/Elsa.Persistence.Abstractions/Entities/WorkflowInstance.cs +++ b/src/persistence/Elsa.Persistence.Abstractions/Entities/WorkflowInstance.cs @@ -1,4 +1,3 @@ -using System; using Elsa.State; namespace Elsa.Persistence.Entities; diff --git a/src/persistence/Elsa.Persistence.Abstractions/Extensions/WorkflowDefinitionExtensions.cs b/src/persistence/Elsa.Persistence.Abstractions/Extensions/WorkflowDefinitionExtensions.cs index ffc2a49e6..c2896d927 100644 --- a/src/persistence/Elsa.Persistence.Abstractions/Extensions/WorkflowDefinitionExtensions.cs +++ b/src/persistence/Elsa.Persistence.Abstractions/Extensions/WorkflowDefinitionExtensions.cs @@ -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; diff --git a/src/persistence/Elsa.Persistence.Abstractions/Extensions/WorkflowExtensions.cs b/src/persistence/Elsa.Persistence.Abstractions/Extensions/WorkflowExtensions.cs index fc55a2d59..25b5fe661 100644 --- a/src/persistence/Elsa.Persistence.Abstractions/Extensions/WorkflowExtensions.cs +++ b/src/persistence/Elsa.Persistence.Abstractions/Extensions/WorkflowExtensions.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.Linq; using Elsa.Models; using Elsa.Persistence.Models; diff --git a/src/persistence/Elsa.Persistence.Abstractions/Mappers/WorkflowDefinitionMapper.cs b/src/persistence/Elsa.Persistence.Abstractions/Mappers/WorkflowDefinitionMapper.cs index bb61cbe25..1951fb381 100644 --- a/src/persistence/Elsa.Persistence.Abstractions/Mappers/WorkflowDefinitionMapper.cs +++ b/src/persistence/Elsa.Persistence.Abstractions/Mappers/WorkflowDefinitionMapper.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.Linq; using Elsa.Models; using Elsa.Persistence.Entities; diff --git a/src/persistence/Elsa.Persistence.Abstractions/Middleware/WorkflowExecution/PersistWorkflowInstanceMiddleware.cs b/src/persistence/Elsa.Persistence.Abstractions/Middleware/WorkflowExecution/PersistWorkflowInstanceMiddleware.cs index 4579050f4..a055a25c5 100644 --- a/src/persistence/Elsa.Persistence.Abstractions/Middleware/WorkflowExecution/PersistWorkflowInstanceMiddleware.cs +++ b/src/persistence/Elsa.Persistence.Abstractions/Middleware/WorkflowExecution/PersistWorkflowInstanceMiddleware.cs @@ -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; diff --git a/src/persistence/Elsa.Persistence.Abstractions/Models/Cursor.cs b/src/persistence/Elsa.Persistence.Abstractions/Models/Cursor.cs index d8e08fbd6..5ef6f6bba 100644 --- a/src/persistence/Elsa.Persistence.Abstractions/Models/Cursor.cs +++ b/src/persistence/Elsa.Persistence.Abstractions/Models/Cursor.cs @@ -1,5 +1,3 @@ -using System; - namespace Elsa.Persistence.Models; public record Cursor(string Id, DateTime Sort); \ No newline at end of file diff --git a/src/persistence/Elsa.Persistence.Abstractions/Models/PagedList.cs b/src/persistence/Elsa.Persistence.Abstractions/Models/PagedList.cs index ab7c7f72f..8c09f0c67 100644 --- a/src/persistence/Elsa.Persistence.Abstractions/Models/PagedList.cs +++ b/src/persistence/Elsa.Persistence.Abstractions/Models/PagedList.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; - namespace Elsa.Persistence.Models; public record PagedList(ICollection Items, int PageSize, int TotalCount); \ No newline at end of file diff --git a/src/persistence/Elsa.Persistence.Abstractions/Models/WorkflowInstanceSummary.cs b/src/persistence/Elsa.Persistence.Abstractions/Models/WorkflowInstanceSummary.cs index ecec50f3b..287705aaf 100644 --- a/src/persistence/Elsa.Persistence.Abstractions/Models/WorkflowInstanceSummary.cs +++ b/src/persistence/Elsa.Persistence.Abstractions/Models/WorkflowInstanceSummary.cs @@ -1,4 +1,3 @@ -using System; using Elsa.Persistence.Entities; namespace Elsa.Persistence.Models; diff --git a/src/persistence/Elsa.Persistence.Abstractions/Models/WorkflowSummary.cs b/src/persistence/Elsa.Persistence.Abstractions/Models/WorkflowSummary.cs index a0456d492..40f74ce8d 100644 --- a/src/persistence/Elsa.Persistence.Abstractions/Models/WorkflowSummary.cs +++ b/src/persistence/Elsa.Persistence.Abstractions/Models/WorkflowSummary.cs @@ -1,4 +1,3 @@ -using System; using Elsa.Persistence.Entities; namespace Elsa.Persistence.Models; diff --git a/src/persistence/Elsa.Persistence.Abstractions/Requests/FindLatestAndPublishedWorkflows.cs b/src/persistence/Elsa.Persistence.Abstractions/Requests/FindLatestAndPublishedWorkflows.cs index 73e2f3350..c375254ce 100644 --- a/src/persistence/Elsa.Persistence.Abstractions/Requests/FindLatestAndPublishedWorkflows.cs +++ b/src/persistence/Elsa.Persistence.Abstractions/Requests/FindLatestAndPublishedWorkflows.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using Elsa.Mediator.Contracts; using Elsa.Models; diff --git a/src/persistence/Elsa.Persistence.Abstractions/Requests/FindManyWorkflowDefinitions.cs b/src/persistence/Elsa.Persistence.Abstractions/Requests/FindManyWorkflowDefinitions.cs index 686e9b346..562b7fc80 100644 --- a/src/persistence/Elsa.Persistence.Abstractions/Requests/FindManyWorkflowDefinitions.cs +++ b/src/persistence/Elsa.Persistence.Abstractions/Requests/FindManyWorkflowDefinitions.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using Elsa.Mediator.Contracts; using Elsa.Persistence.Models; diff --git a/src/persistence/Elsa.Persistence.Abstractions/Requests/FindWorkflowBookmarks.cs b/src/persistence/Elsa.Persistence.Abstractions/Requests/FindWorkflowBookmarks.cs index 70278cefd..6e3ecdfbd 100644 --- a/src/persistence/Elsa.Persistence.Abstractions/Requests/FindWorkflowBookmarks.cs +++ b/src/persistence/Elsa.Persistence.Abstractions/Requests/FindWorkflowBookmarks.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using Elsa.Mediator.Contracts; using Elsa.Persistence.Entities; diff --git a/src/persistence/Elsa.Persistence.Abstractions/Requests/FindWorkflowTriggers.cs b/src/persistence/Elsa.Persistence.Abstractions/Requests/FindWorkflowTriggers.cs index 65f74f846..7a5c02171 100644 --- a/src/persistence/Elsa.Persistence.Abstractions/Requests/FindWorkflowTriggers.cs +++ b/src/persistence/Elsa.Persistence.Abstractions/Requests/FindWorkflowTriggers.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using Elsa.Mediator.Contracts; using Elsa.Persistence.Entities; diff --git a/src/persistence/Elsa.Persistence.Abstractions/Requests/ListWorkflows.cs b/src/persistence/Elsa.Persistence.Abstractions/Requests/ListWorkflows.cs index c3af6dab9..5f5b272d8 100644 --- a/src/persistence/Elsa.Persistence.Abstractions/Requests/ListWorkflows.cs +++ b/src/persistence/Elsa.Persistence.Abstractions/Requests/ListWorkflows.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using Elsa.Mediator.Contracts; using Elsa.Models; using Elsa.Persistence.Models; diff --git a/src/persistence/Elsa.Persistence.Abstractions/TypeConverters/VersionOptionsTypeConverter.cs b/src/persistence/Elsa.Persistence.Abstractions/TypeConverters/VersionOptionsTypeConverter.cs index 16993f2cb..0de27b02f 100644 --- a/src/persistence/Elsa.Persistence.Abstractions/TypeConverters/VersionOptionsTypeConverter.cs +++ b/src/persistence/Elsa.Persistence.Abstractions/TypeConverters/VersionOptionsTypeConverter.cs @@ -1,5 +1,4 @@ -using System; -using System.ComponentModel; +using System.ComponentModel; using System.Globalization; using Elsa.Persistence.Models; diff --git a/src/persistence/Elsa.Persistence.EntityFrameworkCore/Handlers/Serialization/WorkflowDefinitionSerializer.cs b/src/persistence/Elsa.Persistence.EntityFrameworkCore/Handlers/Serialization/WorkflowDefinitionSerializer.cs index dc7f8df18..ac41dc39c 100644 --- a/src/persistence/Elsa.Persistence.EntityFrameworkCore/Handlers/Serialization/WorkflowDefinitionSerializer.cs +++ b/src/persistence/Elsa.Persistence.EntityFrameworkCore/Handlers/Serialization/WorkflowDefinitionSerializer.cs @@ -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; diff --git a/src/persistence/Elsa.Persistence.EntityFrameworkCore/Handlers/Serialization/WorkflowExecutionLogRecordSerializer.cs b/src/persistence/Elsa.Persistence.EntityFrameworkCore/Handlers/Serialization/WorkflowExecutionLogRecordSerializer.cs index c82f28eb9..2f015438e 100644 --- a/src/persistence/Elsa.Persistence.EntityFrameworkCore/Handlers/Serialization/WorkflowExecutionLogRecordSerializer.cs +++ b/src/persistence/Elsa.Persistence.EntityFrameworkCore/Handlers/Serialization/WorkflowExecutionLogRecordSerializer.cs @@ -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; diff --git a/src/persistence/Elsa.Persistence.EntityFrameworkCore/Handlers/Serialization/WorkflowInstanceSerializer.cs b/src/persistence/Elsa.Persistence.EntityFrameworkCore/Handlers/Serialization/WorkflowInstanceSerializer.cs index 696a81876..2564facfb 100644 --- a/src/persistence/Elsa.Persistence.EntityFrameworkCore/Handlers/Serialization/WorkflowInstanceSerializer.cs +++ b/src/persistence/Elsa.Persistence.EntityFrameworkCore/Handlers/Serialization/WorkflowInstanceSerializer.cs @@ -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; diff --git a/src/samples/aspnet/Elsa.Samples.Web1/Elsa.Samples.Web1.csproj b/src/samples/aspnet/Elsa.Samples.Web1/Elsa.Samples.Web1.csproj index 7cfa5390a..4684e150c 100644 --- a/src/samples/aspnet/Elsa.Samples.Web1/Elsa.Samples.Web1.csproj +++ b/src/samples/aspnet/Elsa.Samples.Web1/Elsa.Samples.Web1.csproj @@ -11,6 +11,7 @@ + diff --git a/src/samples/aspnet/Elsa.Samples.Web1/Program.cs b/src/samples/aspnet/Elsa.Samples.Web1/Program.cs index 72e7c3b7f..a8523af01 100644 --- a/src/samples/aspnet/Elsa.Samples.Web1/Program.cs +++ b/src/samples/aspnet/Elsa.Samples.Web1/Program.cs @@ -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(); // Register scripting languages. -services.AddJavaScriptExpressions(); +services + .AddJavaScriptExpressions() + .AddLiquidExpressions(); // Configure middleware pipeline. var app = builder.Build(); diff --git a/src/samples/console/Elsa.Samples.Console2/Program.cs b/src/samples/console/Elsa.Samples.Console2/Program.cs index b52e388c3..f8c17e992 100644 --- a/src/samples/console/Elsa.Samples.Console2/Program.cs +++ b/src/samples/console/Elsa.Samples.Console2/Program.cs @@ -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; diff --git a/src/scripting/Elsa.Scripting.JavaScript/Expressions/JavaScriptExpression.cs b/src/scripting/Elsa.Scripting.JavaScript/Expressions/JavaScriptExpression.cs index 49776deec..feac10e49 100644 --- a/src/scripting/Elsa.Scripting.JavaScript/Expressions/JavaScriptExpression.cs +++ b/src/scripting/Elsa.Scripting.JavaScript/Expressions/JavaScriptExpression.cs @@ -1,5 +1,3 @@ -using System; -using System.Threading.Tasks; using Elsa.Contracts; namespace Elsa.Scripting.JavaScript.Expressions; diff --git a/src/scripting/Elsa.Scripting.Liquid/Contracts/ILiquidFilter.cs b/src/scripting/Elsa.Scripting.Liquid/Contracts/ILiquidFilter.cs new file mode 100644 index 000000000..60b644b5a --- /dev/null +++ b/src/scripting/Elsa.Scripting.Liquid/Contracts/ILiquidFilter.cs @@ -0,0 +1,10 @@ +using Fluid; +using Fluid.Values; + +namespace Elsa.Scripting.Liquid.Services +{ + public interface ILiquidFilter + { + ValueTask ProcessAsync(FluidValue input, FilterArguments arguments, TemplateContext ctx); + } +} diff --git a/src/scripting/Elsa.Scripting.Liquid/Contracts/ILiquidTemplateManager.cs b/src/scripting/Elsa.Scripting.Liquid/Contracts/ILiquidTemplateManager.cs new file mode 100644 index 000000000..014a662df --- /dev/null +++ b/src/scripting/Elsa.Scripting.Liquid/Contracts/ILiquidTemplateManager.cs @@ -0,0 +1,21 @@ +using Elsa.Models; +using Fluid; + +namespace Elsa.Scripting.Liquid.Contracts +{ + /// + /// Provides services to render Liquid templates. + /// + public interface ILiquidTemplateManager + { + /// + /// Renders a Liquid template as a . + /// + Task RenderAsync(string template, ExpressionExecutionContext expressionExecutionContext, CancellationToken cancellationToken = default); + + /// + /// Validates a Liquid template. + /// + bool Validate(string template, out string error); + } +} \ No newline at end of file diff --git a/src/scripting/Elsa.Scripting.Liquid/Elsa.Scripting.Liquid.csproj b/src/scripting/Elsa.Scripting.Liquid/Elsa.Scripting.Liquid.csproj new file mode 100644 index 000000000..47d7dcf31 --- /dev/null +++ b/src/scripting/Elsa.Scripting.Liquid/Elsa.Scripting.Liquid.csproj @@ -0,0 +1,27 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + ..\..\..\..\..\..\Program Files\dotnet\shared\Microsoft.AspNetCore.App\6.0.0\Microsoft.Extensions.Caching.Abstractions.dll + + + ..\..\..\..\..\..\Program Files\dotnet\shared\Microsoft.AspNetCore.App\6.0.0\Microsoft.Extensions.Configuration.Abstractions.dll + + + + diff --git a/src/scripting/Elsa.Scripting.Liquid/Expressions/LiquidExpression.cs b/src/scripting/Elsa.Scripting.Liquid/Expressions/LiquidExpression.cs new file mode 100644 index 000000000..b98253efe --- /dev/null +++ b/src/scripting/Elsa.Scripting.Liquid/Expressions/LiquidExpression.cs @@ -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; } +} \ No newline at end of file diff --git a/src/scripting/Elsa.Scripting.Liquid/Expressions/LiquidExpressionHandler.cs b/src/scripting/Elsa.Scripting.Liquid/Expressions/LiquidExpressionHandler.cs new file mode 100644 index 000000000..6140d453f --- /dev/null +++ b/src/scripting/Elsa.Scripting.Liquid/Expressions/LiquidExpressionHandler.cs @@ -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 EvaluateAsync(IExpression expression, Type returnType, ExpressionExecutionContext context) + { + var liquidExpression = (LiquidExpression)expression; + var renderedString = await _liquidTemplateManager.RenderAsync(liquidExpression.Value, context); + return renderedString.ConvertTo(returnType); + } +} \ No newline at end of file diff --git a/src/scripting/Elsa.Scripting.Liquid/Expressions/LiquidExpressionReference.cs b/src/scripting/Elsa.Scripting.Liquid/Expressions/LiquidExpressionReference.cs new file mode 100644 index 000000000..f90096890 --- /dev/null +++ b/src/scripting/Elsa.Scripting.Liquid/Expressions/LiquidExpressionReference.cs @@ -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(); +} \ No newline at end of file diff --git a/src/scripting/Elsa.Scripting.Liquid/Extensions/ServiceCollectionExtensions.cs b/src/scripting/Elsa.Scripting.Liquid/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..c287bd606 --- /dev/null +++ b/src/scripting/Elsa.Scripting.Liquid/Extensions/ServiceCollectionExtensions.cs @@ -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() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddExpressionHandler() + .AddLiquidFilter("json") + .AddLiquidFilter("base64"); + } + + public static IServiceCollection AddLiquidFilter(this IServiceCollection services, string name) where T : class, ILiquidFilter + { + services.Configure(options => options.FilterRegistrations[name] = typeof(T)); + services.AddScoped(); + return services; + } + + public static IServiceCollection RegisterLiquidTag(this IServiceCollection services, Action configure) + { + services.Configure(options => options.ParserConfiguration.Add(configure)); + return services; + } + + /// + /// Enables access to .NET configuration via the Configuration keyword. + /// Do not enable this option if you execute user supplied (or otherwise untrusted) workflows. + /// + public static IServiceCollection EnableLiquidConfigurationAccess(this IServiceCollection services) + { + services.Configure(options => options.AllowConfigurationAccess = true); + return services; + } +} \ No newline at end of file diff --git a/src/scripting/Elsa.Scripting.Liquid/Extensions/TemplateContextExtensions.cs b/src/scripting/Elsa.Scripting.Liquid/Extensions/TemplateContextExtensions.cs new file mode 100644 index 000000000..21c174db4 --- /dev/null +++ b/src/scripting/Elsa.Scripting.Liquid/Extensions/TemplateContextExtensions.cs @@ -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); + }); + } + } + } +} \ No newline at end of file diff --git a/src/scripting/Elsa.Scripting.Liquid/Filters/Base64Filter.cs b/src/scripting/Elsa.Scripting.Liquid/Filters/Base64Filter.cs new file mode 100644 index 000000000..426ffbfdd --- /dev/null +++ b/src/scripting/Elsa.Scripting.Liquid/Filters/Base64Filter.cs @@ -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 ProcessAsync(FluidValue input, FilterArguments arguments, TemplateContext context) + { + var text = InputToString(input, context); + + if (text == null) + return new ValueTask(NilValue.Instance); + + var bytes = Encoding.UTF8.GetBytes(text); + var base64 = Convert.ToBase64String(bytes); + + return new ValueTask(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") + }; + } + } +} \ No newline at end of file diff --git a/src/scripting/Elsa.Scripting.Liquid/Filters/JsonFilter.cs b/src/scripting/Elsa.Scripting.Liquid/Filters/JsonFilter.cs new file mode 100644 index 000000000..0223b3f42 --- /dev/null +++ b/src/scripting/Elsa.Scripting.Liquid/Filters/JsonFilter.cs @@ -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 ProcessAsync(FluidValue input, FilterArguments arguments, TemplateContext context) + { + switch (input.Type) + { + case FluidValues.Array: + return new ValueTask(new StringValue(JsonSerializer.Serialize(input.Enumerate(context).Select(o => o.ToObjectValue())))); + + case FluidValues.Boolean: + return new ValueTask(new StringValue(JsonSerializer.Serialize(input.ToBooleanValue()))); + + case FluidValues.Nil: + return new ValueTask(FluidValue.Create("null", context.Options)); + + case FluidValues.Number: + return new ValueTask(new StringValue(JsonSerializer.Serialize(input.ToNumberValue()))); + + case FluidValues.DateTime: + case FluidValues.Dictionary: + case FluidValues.Object: + return new ValueTask(new StringValue(JsonSerializer.Serialize(input.ToObjectValue()))); + + case FluidValues.String: + var stringValue = input.ToStringValue(); + + return string.IsNullOrWhiteSpace(stringValue) + ? new ValueTask(input) + : new ValueTask(new StringValue(JsonSerializer.Serialize(stringValue))); + } + + throw new NotSupportedException("Unrecognized FluidValue"); + } + } +} \ No newline at end of file diff --git a/src/scripting/Elsa.Scripting.Liquid/Handlers/ConfigureLiquidEngine.cs b/src/scripting/Elsa.Scripting.Liquid/Handlers/ConfigureLiquidEngine.cs new file mode 100644 index 000000000..dacf7f6da --- /dev/null +++ b/src/scripting/Elsa.Scripting.Liquid/Handlers/ConfigureLiquidEngine.cs @@ -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 + { + private readonly IConfiguration _configuration; + private readonly FluidOptions _fluidOptions; + + public ConfigureLiquidEngine(IConfiguration configuration, IOptions 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(); + memberAccessStrategy.Register((x, name) => x.GetValueAsync(name)); + memberAccessStrategy.Register((x, name) => ((IDictionary)x!)[name]); + + if (_fluidOptions.AllowConfigurationAccess) + { + memberAccessStrategy.Register("Configuration", x => new LiquidPropertyAccessor(name => ToFluidValue(GetConfigurationValue(name), options)!)); + memberAccessStrategy.Register((source, name) => source.GetSection(name)); + } + + return Task.CompletedTask; + } + + private ConfigurationSectionWrapper GetConfigurationValue(string name) => new(_configuration.GetSection(name)); + private Task ToFluidValue(object? input, TemplateOptions options) => Task.FromResult(FluidValue.Create(input, options)); + } +} \ No newline at end of file diff --git a/src/scripting/Elsa.Scripting.Liquid/Helpers/ConfigurationSectionWrapper.cs b/src/scripting/Elsa.Scripting.Liquid/Helpers/ConfigurationSectionWrapper.cs new file mode 100644 index 000000000..de2c2786a --- /dev/null +++ b/src/scripting/Elsa.Scripting.Liquid/Helpers/ConfigurationSectionWrapper.cs @@ -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)); + } +} \ No newline at end of file diff --git a/src/scripting/Elsa.Scripting.Liquid/Helpers/LiquidActivityModel.cs b/src/scripting/Elsa.Scripting.Liquid/Helpers/LiquidActivityModel.cs new file mode 100644 index 000000000..3a0254c41 --- /dev/null +++ b/src/scripting/Elsa.Scripting.Liquid/Helpers/LiquidActivityModel.cs @@ -0,0 +1,8 @@ +using Elsa.Models; + +namespace Elsa.Scripting.Liquid.Helpers +{ + public record LiquidActivityModel(ActivityExecutionContext ActivityExecutionContext, string? ActivityName, string? ActivityId) + { + } +} \ No newline at end of file diff --git a/src/scripting/Elsa.Scripting.Liquid/Helpers/LiquidObjectAccessor.cs b/src/scripting/Elsa.Scripting.Liquid/Helpers/LiquidObjectAccessor.cs new file mode 100644 index 000000000..5029b8ea5 --- /dev/null +++ b/src/scripting/Elsa.Scripting.Liquid/Helpers/LiquidObjectAccessor.cs @@ -0,0 +1,12 @@ +namespace Elsa.Scripting.Liquid.Helpers +{ + /// + /// Can be used to provide a factory to return an object based on a property name that is unknown at registration time. + /// + public class LiquidObjectAccessor + { + private readonly Func> _getter; + public LiquidObjectAccessor(Func> getter) => _getter = getter; + public Task GetValueAsync(string identifier) => _getter(identifier); + } +} \ No newline at end of file diff --git a/src/scripting/Elsa.Scripting.Liquid/Helpers/LiquidPropertyAccessor.cs b/src/scripting/Elsa.Scripting.Liquid/Helpers/LiquidPropertyAccessor.cs new file mode 100644 index 000000000..0aaf55f81 --- /dev/null +++ b/src/scripting/Elsa.Scripting.Liquid/Helpers/LiquidPropertyAccessor.cs @@ -0,0 +1,17 @@ +using Fluid.Values; + +namespace Elsa.Scripting.Liquid.Helpers +{ + /// + /// 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) + /// + public class LiquidPropertyAccessor : LiquidObjectAccessor + { + public LiquidPropertyAccessor(Func> getter) : base(getter!) + { + } + } +} \ No newline at end of file diff --git a/src/scripting/Elsa.Scripting.Liquid/Notifications/RenderingLiquidTemplate.cs b/src/scripting/Elsa.Scripting.Liquid/Notifications/RenderingLiquidTemplate.cs new file mode 100644 index 000000000..81b5d9aba --- /dev/null +++ b/src/scripting/Elsa.Scripting.Liquid/Notifications/RenderingLiquidTemplate.cs @@ -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; \ No newline at end of file diff --git a/src/scripting/Elsa.Scripting.Liquid/Options/FluidOptions.cs b/src/scripting/Elsa.Scripting.Liquid/Options/FluidOptions.cs new file mode 100644 index 000000000..174a7ac5f --- /dev/null +++ b/src/scripting/Elsa.Scripting.Liquid/Options/FluidOptions.cs @@ -0,0 +1,11 @@ +using Elsa.Scripting.Liquid.Services; + +namespace Elsa.Scripting.Liquid.Options +{ + public class FluidOptions + { + public Dictionary FilterRegistrations { get; } = new(); + public IList> ParserConfiguration { get; } = new List>(); + public bool AllowConfigurationAccess { get; set; } + } +} diff --git a/src/scripting/Elsa.Scripting.Liquid/Providers/LiquidExpressionSyntaxProvider.cs b/src/scripting/Elsa.Scripting.Liquid/Providers/LiquidExpressionSyntaxProvider.cs new file mode 100644 index 000000000..97604002c --- /dev/null +++ b/src/scripting/Elsa.Scripting.Liquid/Providers/LiquidExpressionSyntaxProvider.cs @@ -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> GetDescriptorsAsync(CancellationToken cancellationToken = default) + { + var liquidDescriptor = CreateLiquidDescriptor(); + + return ValueTask.FromResult>(new[] { liquidDescriptor }); + } + + private ExpressionSyntaxDescriptor CreateLiquidDescriptor() => new() + { + Syntax = SyntaxName, + Type = typeof(LiquidExpression), + CreateExpression = CreateJavaScriptExpression, + CreateLocationReference = context => + { + var reference = new LiquidExpressionReference(context.GetExpression()); + + if (string.IsNullOrWhiteSpace(reference.Id)) + reference.Id = GenerateId(); + + return reference; + }, + CreateSerializableObject = context => new + { + Type = SyntaxName, + context.GetExpression().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(); +} \ No newline at end of file diff --git a/src/scripting/Elsa.Scripting.Liquid/Services/LiquidParser.cs b/src/scripting/Elsa.Scripting.Liquid/Services/LiquidParser.cs new file mode 100644 index 000000000..85a804efe --- /dev/null +++ b/src/scripting/Elsa.Scripting.Liquid/Services/LiquidParser.cs @@ -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 options) + { + foreach (var configuration in options.Value.ParserConfiguration) + configuration(this); + } + } +} \ No newline at end of file diff --git a/src/scripting/Elsa.Scripting.Liquid/Services/LiquidTemplateManager.cs b/src/scripting/Elsa.Scripting.Liquid/Services/LiquidTemplateManager.cs new file mode 100644 index 000000000..acc7b02b3 --- /dev/null +++ b/src/scripting/Elsa.Scripting.Liquid/Services/LiquidTemplateManager.cs @@ -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 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 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; + } + } +} \ No newline at end of file