diff --git a/Elsa.sln b/Elsa.sln index 0f9d79d47..97074488c 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -283,6 +283,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "storage", "storage", "{B818 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.CSharp", "src\modules\Elsa.CSharp\Elsa.CSharp.csproj", "{24331E82-D7AF-45B1-ACF0-CA6C3B0B77DC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Python", "src\modules\Elsa.Python\Elsa.Python.csproj", "{790E94F2-5393-47DF-AC52-D9247F5B243A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -709,6 +711,10 @@ Global {24331E82-D7AF-45B1-ACF0-CA6C3B0B77DC}.Debug|Any CPU.Build.0 = Debug|Any CPU {24331E82-D7AF-45B1-ACF0-CA6C3B0B77DC}.Release|Any CPU.ActiveCfg = Release|Any CPU {24331E82-D7AF-45B1-ACF0-CA6C3B0B77DC}.Release|Any CPU.Build.0 = Release|Any CPU + {790E94F2-5393-47DF-AC52-D9247F5B243A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {790E94F2-5393-47DF-AC52-D9247F5B243A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {790E94F2-5393-47DF-AC52-D9247F5B243A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {790E94F2-5393-47DF-AC52-D9247F5B243A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -839,6 +845,7 @@ Global {B818988E-639C-4E6E-85C1-B231BCAD9DAB} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} {732BF088-6AD7-4C4D-9A48-8074253596D4} = {B818988E-639C-4E6E-85C1-B231BCAD9DAB} {24331E82-D7AF-45B1-ACF0-CA6C3B0B77DC} = {6EF07978-A6D2-40EB-891D-7D70C5F37E76} + {790E94F2-5393-47DF-AC52-D9247F5B243A} = {6EF07978-A6D2-40EB-891D-7D70C5F37E76} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E} diff --git a/src/modules/Elsa.CSharp/Extensions/ModuleExtensions.cs b/src/modules/Elsa.CSharp/Extensions/ModuleExtensions.cs index 72480ce8d..b264bbb8f 100644 --- a/src/modules/Elsa.CSharp/Extensions/ModuleExtensions.cs +++ b/src/modules/Elsa.CSharp/Extensions/ModuleExtensions.cs @@ -24,6 +24,6 @@ public static class ModuleExtensions /// public static IModule UseCSharp(this IModule module, Action configureOptions) { - return module.UseCSharp(csharp => csharp.RoslynOptions += configureOptions); + return module.UseCSharp(csharp => csharp.CSharpOptions += configureOptions); } } \ No newline at end of file diff --git a/src/modules/Elsa.CSharp/Features/CSharpFeature.cs b/src/modules/Elsa.CSharp/Features/CSharpFeature.cs index 6bf753627..c2239cc11 100644 --- a/src/modules/Elsa.CSharp/Features/CSharpFeature.cs +++ b/src/modules/Elsa.CSharp/Features/CSharpFeature.cs @@ -27,14 +27,14 @@ public class CSharpFeature : FeatureBase } /// - /// Configures the . + /// Configures the . /// - public Action RoslynOptions { get; set; } = _ => { }; + public Action CSharpOptions { get; set; } = _ => { }; /// public override void Apply() { - Services.Configure(RoslynOptions); + Services.Configure(CSharpOptions); // C# services. Services diff --git a/src/modules/Elsa.Python/Contracts/IPythonEvaluator.cs b/src/modules/Elsa.Python/Contracts/IPythonEvaluator.cs new file mode 100644 index 000000000..758cc7e12 --- /dev/null +++ b/src/modules/Elsa.Python/Contracts/IPythonEvaluator.cs @@ -0,0 +1,25 @@ +using Elsa.Expressions.Models; +using JetBrains.Annotations; + +namespace Elsa.Python.Contracts; + +/// +/// Evaluates C# expressions. +/// +[PublicAPI] +public interface IPythonEvaluator +{ + /// + /// Evaluates a Python expression. + /// + /// The expression to evaluate. + /// The type of the return value. + /// The context in which the expression is evaluated. + /// An optional cancellation token. + /// The result of the evaluation. + Task EvaluateAsync( + string expression, + Type returnType, + ExpressionExecutionContext context, + CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/modules/Elsa.Python/Elsa.Python.csproj b/src/modules/Elsa.Python/Elsa.Python.csproj new file mode 100644 index 000000000..d0d1c7175 --- /dev/null +++ b/src/modules/Elsa.Python/Elsa.Python.csproj @@ -0,0 +1,23 @@ + + + + + + + net6.0;net7.0 + + Provides a Python expression provider. + + elsa module expressions scripting python + + + + + + + + + + + + diff --git a/src/modules/Elsa.Python/Expressions/PythonExpression.cs b/src/modules/Elsa.Python/Expressions/PythonExpression.cs new file mode 100644 index 000000000..c07a5c154 --- /dev/null +++ b/src/modules/Elsa.Python/Expressions/PythonExpression.cs @@ -0,0 +1,23 @@ +using Elsa.Expressions.Contracts; + +namespace Elsa.Python.Expressions; + +/// +/// A Python expression. +/// +public class PythonExpression : IExpression +{ + /// + /// Initializes a new instance of the class. + /// + /// The Python expression. + public PythonExpression(string value) + { + Value = value; + } + + /// + /// Gets the Python expression. + /// + public string Value { get; } +} \ No newline at end of file diff --git a/src/modules/Elsa.Python/Expressions/PythonExpressionBlockReference.cs b/src/modules/Elsa.Python/Expressions/PythonExpressionBlockReference.cs new file mode 100644 index 000000000..a6fc64a3e --- /dev/null +++ b/src/modules/Elsa.Python/Expressions/PythonExpressionBlockReference.cs @@ -0,0 +1,29 @@ +using Elsa.Expressions.Models; + +namespace Elsa.Python.Expressions; + +/// +/// A reference to a Python expression. +/// +public class PythonExpressionBlockReference : MemoryBlockReference +{ + /// + /// Initializes a new instance of the class. + /// + /// The expression to reference. + public PythonExpressionBlockReference(PythonExpression expression) + { + Expression = expression; + } + + /// + /// Gets the referenced expression. + /// + public PythonExpression Expression { get; } + + /// + /// Declares the referenced expression. + /// + /// A memory block holding the expression. + public override MemoryBlock Declare() => new(); +} \ No newline at end of file diff --git a/src/modules/Elsa.Python/Expressions/PythonExpressionHandler.cs b/src/modules/Elsa.Python/Expressions/PythonExpressionHandler.cs new file mode 100644 index 000000000..85b6cd30d --- /dev/null +++ b/src/modules/Elsa.Python/Expressions/PythonExpressionHandler.cs @@ -0,0 +1,28 @@ +using Elsa.Expressions.Contracts; +using Elsa.Expressions.Models; +using Elsa.Python.Contracts; + +namespace Elsa.Python.Expressions; + +/// +/// Evaluates C# expressions. +/// +public class PythonExpressionHandler : IExpressionHandler +{ + private readonly IPythonEvaluator _evaluator; + + /// + /// Initializes a new instance of the class. + /// + public PythonExpressionHandler(IPythonEvaluator evaluator) + { + _evaluator = evaluator; + } + + /// + public async ValueTask EvaluateAsync(IExpression expression, Type returnType, ExpressionExecutionContext context) + { + var pythonExpression = (PythonExpression)expression; + return await _evaluator.EvaluateAsync(pythonExpression.Value, returnType, context); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Python/Features/PythonFeature.cs b/src/modules/Elsa.Python/Features/PythonFeature.cs new file mode 100644 index 000000000..9c565789d --- /dev/null +++ b/src/modules/Elsa.Python/Features/PythonFeature.cs @@ -0,0 +1,52 @@ +using Elsa.Common.Features; +using Elsa.Expressions.Contracts; +using Elsa.Expressions.Features; +using Elsa.Extensions; +using Elsa.Features.Abstractions; +using Elsa.Features.Attributes; +using Elsa.Features.Services; +using Elsa.Python.Contracts; +using Elsa.Python.Expressions; +using Elsa.Python.Options; +using Elsa.Python.Providers; +using Elsa.Python.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Python.Features; + +/// +/// Installs C# integration. +/// +[DependsOn(typeof(MediatorFeature))] +[DependsOn(typeof(ExpressionsFeature))] +public class PythonFeature : FeatureBase +{ + /// + public PythonFeature(IModule module) : base(module) + { + } + + /// + /// Configures the . + /// + public Action RoslynOptions { get; set; } = _ => { }; + + /// + public override void Apply() + { + Services.Configure(RoslynOptions); + + // C# services. + Services + .AddSingleton() + .AddSingleton() + .AddExpressionHandler() + ; + + // Handlers. + Services.AddNotificationHandlersFrom(); + + // Activities. + Module.AddActivitiesFrom(); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Python/Options/PythonOptions.cs b/src/modules/Elsa.Python/Options/PythonOptions.cs new file mode 100644 index 000000000..595bc2f3f --- /dev/null +++ b/src/modules/Elsa.Python/Options/PythonOptions.cs @@ -0,0 +1,9 @@ +namespace Elsa.Python.Options; + +/// +/// Options for the Python expression evaluator. +/// +public class PythonOptions +{ + +} \ No newline at end of file diff --git a/src/modules/Elsa.Python/Providers/PythonExpressionSyntaxProvider.cs b/src/modules/Elsa.Python/Providers/PythonExpressionSyntaxProvider.cs new file mode 100644 index 000000000..9f2ed68f4 --- /dev/null +++ b/src/modules/Elsa.Python/Providers/PythonExpressionSyntaxProvider.cs @@ -0,0 +1,36 @@ +using Elsa.Expressions.Contracts; +using Elsa.Expressions.Models; +using Elsa.Python.Expressions; + +namespace Elsa.Python.Providers; + +internal class PythonExpressionSyntaxProvider : IExpressionSyntaxProvider +{ + private const string SyntaxName = "Python"; + + public ValueTask> GetDescriptorsAsync(CancellationToken cancellationToken = default) + { + var javaScript = CreatePythonDescriptor(); + + return ValueTask.FromResult>(new[] { javaScript }); + } + + private ExpressionSyntaxDescriptor CreatePythonDescriptor() => new() + { + Syntax = SyntaxName, + Type = typeof(PythonExpression), + CreateExpression = CreateCSharpExpression, + CreateBlockReference = context => new PythonExpressionBlockReference(context.GetExpression()), + CreateSerializableObject = context => new + { + Type = SyntaxName, + context.GetExpression().Value + } + }; + + private IExpression CreateCSharpExpression(ExpressionConstructorContext context) + { + var script = context.Element.TryGetProperty("value", out var p) ? p.ToString() : ""; + return new PythonExpression(script); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Python/Services/IronPythonEvaluator.cs b/src/modules/Elsa.Python/Services/IronPythonEvaluator.cs new file mode 100644 index 000000000..21299a97a --- /dev/null +++ b/src/modules/Elsa.Python/Services/IronPythonEvaluator.cs @@ -0,0 +1,19 @@ +using Elsa.Expressions.Models; +using Elsa.Python.Contracts; + +namespace Elsa.Python.Services; + +/// +/// Evaluates Python expressions using IronPython. +/// +public class IronPythonEvaluator : IPythonEvaluator +{ + /// + public async Task EvaluateAsync(string expression, Type returnType, ExpressionExecutionContext context, CancellationToken cancellationToken = default) + { + var eng = IronPython.Hosting.Python.CreateEngine(); + var scope = eng.CreateScope(); + var result = eng.Execute(expression, scope); + return result; + } +} \ No newline at end of file