diff --git a/.gitignore b/.gitignore index fd2d83a8..673f996f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ dist *.user .env *.xmldocs.xml -**/CONDUCTORSHARP_HEALTH.json \ No newline at end of file +**/CONDUCTORSHARP_HEALTH.json +.idea diff --git a/.husky/pre-commit b/.husky/pre-commit old mode 100644 new mode 100755 diff --git a/src/ConductorSharp.Engine/Util/ExpressionUtil.cs b/src/ConductorSharp.Engine/Util/ExpressionUtil.cs index b93b1272..3f8554ac 100644 --- a/src/ConductorSharp.Engine/Util/ExpressionUtil.cs +++ b/src/ConductorSharp.Engine/Util/ExpressionUtil.cs @@ -71,30 +71,63 @@ private static bool IsStringInterpolation(Expression expression) => && methodExpression.Method.DeclaringType == typeof(string); private static object EvaluateExpression(Expression expr) => - IsEvaluatable(expr) ? Expression.Lambda(expr).Compile().DynamicInvoke() : throw new NonEvaluatableExpressionException(expr); + GetEvaluatableExpression(expr) is { } evaluatableExpression + ? Expression.Lambda(evaluatableExpression).Compile().DynamicInvoke() + : throw new NonEvaluatableExpressionException(expr); - private static bool IsEvaluatable(Expression expr) + private static Expression GetEvaluatableExpression(Expression expr, Expression parentExpr = null) { switch (expr) { case MemberExpression memExpr: + bool shouldCompileToJsonPath = ShouldCompileToJsonPathExpression(expr); + if (shouldCompileToJsonPath && parentExpr is MethodCallExpression methodCallExpression && methodCallExpression.Object == expr) + return null; + + if (shouldCompileToJsonPath) + return Expression.Constant(CreateExpressionString(expr), typeof(string)); + if ( typeof(ITaskModel).IsAssignableFrom(memExpr.Type) || typeof(WorkflowId).IsAssignableFrom(memExpr.Type) || typeof(IWorkflowInput).IsAssignableFrom(memExpr.Type) ) - return false; - return IsEvaluatable(memExpr.Expression); + return null; + + if (memExpr.Member is PropertyInfo { GetMethod.IsStatic: true }) + return memExpr; + + if (memExpr.Member is PropertyInfo propertyInfo && propertyInfo.DeclaringType == typeof(Type)) + return memExpr; + + var subExpr = GetEvaluatableExpression(memExpr.Expression, parentExpr); + return memExpr.Expression is null || subExpr is not null ? memExpr.Update(subExpr) : null; case MethodCallExpression methodExpr: - return IsEvaluatable(methodExpr.Object) && methodExpr.Arguments.All(IsEvaluatable); + var argumentExpressions = methodExpr + .Arguments.Select(arg => GetEvaluatableExpression(arg, methodExpr)) + .Where(argExpr => argExpr is not null) + .ToArray(); + var objectExpression = GetEvaluatableExpression(methodExpr.Object, methodExpr); + return (methodExpr.Object is null || objectExpression is not null) && argumentExpressions.Length == methodExpr.Arguments.Count + ? methodExpr.Update(objectExpression, argumentExpressions) + : null; case BinaryExpression binaryExpr: - return IsEvaluatable(binaryExpr.Left) && IsEvaluatable(binaryExpr.Right); + return + GetEvaluatableExpression(binaryExpr.Left, binaryExpr) is { } left + && GetEvaluatableExpression(binaryExpr.Right, binaryExpr) is { } right + ? binaryExpr.Update(left, binaryExpr.Conversion, right) + : null; case UnaryExpression unaryExpr: - return IsEvaluatable(unaryExpr.Operand); + return unaryExpr.Update(GetEvaluatableExpression(unaryExpr.Operand, unaryExpr)); case ConditionalExpression condExpr: - return IsEvaluatable(condExpr.Test) && IsEvaluatable(condExpr.IfTrue) && IsEvaluatable(condExpr.IfFalse); + return + GetEvaluatableExpression(condExpr.Test, condExpr) is { } test + && GetEvaluatableExpression(condExpr.IfTrue, condExpr) is { } ifTrue + && GetEvaluatableExpression(condExpr.IfFalse, condExpr) is { } ifFalse + ? condExpr.Update(test, ifTrue, ifFalse) + : null; default: - return true; + return expr; } } diff --git a/test/ConductorSharp.Engine.Tests/ConductorSharp.Engine.Tests.csproj b/test/ConductorSharp.Engine.Tests/ConductorSharp.Engine.Tests.csproj index 87ec8a06..f58b0ef4 100644 --- a/test/ConductorSharp.Engine.Tests/ConductorSharp.Engine.Tests.csproj +++ b/test/ConductorSharp.Engine.Tests/ConductorSharp.Engine.Tests.csproj @@ -44,6 +44,7 @@ + diff --git a/test/ConductorSharp.Engine.Tests/Integration/WorkflowBuilderTests.cs b/test/ConductorSharp.Engine.Tests/Integration/WorkflowBuilderTests.cs index 727affcc..a1e60a11 100644 --- a/test/ConductorSharp.Engine.Tests/Integration/WorkflowBuilderTests.cs +++ b/test/ConductorSharp.Engine.Tests/Integration/WorkflowBuilderTests.cs @@ -271,6 +271,15 @@ public void BuilderReturnsCorrectDefinitionDictionaryInitializationWorkflow() Assert.Equal(expectedDefinition, definition); } + [Fact] + public void BuilderReturnsCorrectDefinitionStaticFormatter() + { + var definition = GetDefinitionFromWorkflow(); + var expectedDefinition = EmbeddedFileHelper.GetLinesFromEmbeddedFile("~/Samples/Workflows/StaticFormatter.json"); + + Assert.Equal(expectedDefinition, definition); + } + private static string GetDefinitionFromWorkflow() where TWorkflow : IConfigurableWorkflow { diff --git a/test/ConductorSharp.Engine.Tests/Samples/Workflows/NonEvaluatableWorkflow.cs b/test/ConductorSharp.Engine.Tests/Samples/Workflows/NonEvaluatableWorkflow.cs index f8ba3ee5..99ee0e63 100644 --- a/test/ConductorSharp.Engine.Tests/Samples/Workflows/NonEvaluatableWorkflow.cs +++ b/test/ConductorSharp.Engine.Tests/Samples/Workflows/NonEvaluatableWorkflow.cs @@ -20,13 +20,14 @@ public class NonEvaluatableWorkflow : Workflow builder - ) : base(builder) { } + ) + : base(builder) { } public override void BuildDefinition() { _builder.AddTask(wf => wf.GetCustomer, wf => new() { CustomerId = wf.WorkflowInput.Input }); - _builder.AddTask(wf => wf.PrepareEmail, wf => new() { Address = $"{wf.GetCustomer.Output.Address}".ToUpperInvariant(), }); + _builder.AddTask(wf => wf.PrepareEmail, wf => new() { Address = wf.GetCustomer.Output.Address.ToString().ToUpperInvariant(), }); } } } diff --git a/test/ConductorSharp.Engine.Tests/Samples/Workflows/StaticFormatter.cs b/test/ConductorSharp.Engine.Tests/Samples/Workflows/StaticFormatter.cs new file mode 100644 index 00000000..b171e228 --- /dev/null +++ b/test/ConductorSharp.Engine.Tests/Samples/Workflows/StaticFormatter.cs @@ -0,0 +1,43 @@ +namespace ConductorSharp.Engine.Tests.Samples.Workflows +{ + public class StaticFormatterInput : WorkflowInput + { + public string FirstName { get; set; } + public string LastName { get; set; } + } + + public class StaticFormatterOutput : WorkflowOutput + { + public object EmailBody { get; set; } + } + + public class StaticFormatter : Workflow + { + public StaticFormatter(WorkflowDefinitionBuilder builder) + : base(builder) { } + + public EmailPrepareV1 EmailPrepare { get; set; } + + public override void BuildDefinition() + { + _builder.AddTask( + wf => wf.EmailPrepare, + wf => + new() + { + Address = SmartEmailConverter.Format(wf.Input.FirstName, wf.Input.LastName), + Name = $"Workflow name: {NamingUtil.NameOf()}" + } + ); + + _builder.SetOutput(a => new() { EmailBody = a.EmailPrepare.Output.EmailBody }); + } + + public static EmailConverter SmartEmailConverter => new EmailConverter(); + + public class EmailConverter + { + public string Format(string firstName, string lastName) => $"{firstName}.{lastName}@example.com"; + } + } +} diff --git a/test/ConductorSharp.Engine.Tests/Samples/Workflows/StaticFormatter.json b/test/ConductorSharp.Engine.Tests/Samples/Workflows/StaticFormatter.json new file mode 100644 index 00000000..da204307 --- /dev/null +++ b/test/ConductorSharp.Engine.Tests/Samples/Workflows/StaticFormatter.json @@ -0,0 +1,26 @@ +{ + "name": "static_formatter", + "version": 1, + "tasks": [ + { + "name": "EMAIL_prepare", + "taskReferenceName": "email_prepare", + "inputParameters": { + "address": "${workflow.input.first_name}.${workflow.input.last_name}@example.com", + "name": "Workflow name: TEST_StringInterpolation" + }, + "type": "SIMPLE", + "optional": false, + "workflowTaskType": "SIMPLE" + } + ], + "inputParameters": [ + "first_name", + "last_name" + ], + "outputParameters": { + "email_body": "${email_prepare.output.email_body}" + }, + "schemaVersion": 2, + "timeoutSeconds": 0 +} \ No newline at end of file