Skip to content

Commit 7a159b4

Browse files
authored
Add feature which allows formatting of conductor expression (#204)
* Add initial implemenation * Create alpha release * Improve exception messages, add test case * Improve messages further * Add ability to interpolate conductor expression formatters * Update ConductorSharp
1 parent 54365e3 commit 7a159b4

14 files changed

Lines changed: 184 additions & 7 deletions

src/ConductorSharp.Client/ConductorSharp.Client.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<Authors>Codaxy</Authors>
77
<Company>Codaxy</Company>
88
<PackageId>ConductorSharp.Client</PackageId>
9-
<Version>3.4.1</Version>
9+
<Version>3.5.0</Version>
1010
<Description>Client library for Netflix Conductor, with some additional quality of life features.</Description>
1111
<RepositoryUrl>https://github.com/codaxy/conductor-sharp</RepositoryUrl>
1212
<PackageTags>netflix;conductor</PackageTags>

src/ConductorSharp.Engine/ConductorSharp.Engine.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<Authors>Codaxy</Authors>
77
<Company>Codaxy</Company>
88
<PackageId>ConductorSharp.Engine</PackageId>
9-
<Version>3.4.1</Version>
9+
<Version>3.5.0</Version>
1010
<Description>Client library for Netflix Conductor, with some additional quality of life features.</Description>
1111
<RepositoryUrl>https://github.com/codaxy/conductor-sharp</RepositoryUrl>
1212
<PackageTags>netflix;conductor</PackageTags>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
using System;
2+
3+
namespace ConductorSharp.Engine.Util;
4+
5+
[AttributeUsage(AttributeTargets.Method)]
6+
public class ConductorExpressionFormatterAttribute : Attribute;

src/ConductorSharp.Engine/Util/ExpressionUtil.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,57 @@ private static object ParseExpression(Expression expression)
6363
if (IsNameOfExpression(expression))
6464
return CompileNameOfExpression((MethodCallExpression)expression);
6565

66+
if (IsConductorExpressionFormatString(expression))
67+
return CompileConductorExpressionFormatString((MethodCallExpression)expression);
68+
6669
return EvaluateExpression(expression);
6770
}
6871

72+
private static bool IsConductorExpressionFormatString(Expression expression) =>
73+
expression is MethodCallExpression methodExpr
74+
&& methodExpr.Method.GetCustomAttribute<ConductorExpressionFormatterAttribute>() is not null;
75+
76+
private static string CompileConductorExpressionFormatString(MethodCallExpression methodExpr)
77+
{
78+
if (methodExpr.Method.ReturnType != typeof(string))
79+
throw new InvalidOperationException($"Format method \"{methodExpr.Method.Name}\" must return string");
80+
81+
var args = new List<object>();
82+
83+
foreach (var (argExpr, paramInfo) in methodExpr.Arguments.Zip(methodExpr.Method.GetParameters(), (expr, paramInfo) => (expr, paramInfo)))
84+
{
85+
if (paramInfo.GetCustomAttribute<FormatterParameterAttribute>() is not null)
86+
{
87+
if (argExpr.Type != typeof(string))
88+
throw new InvalidOperationException(
89+
$"Parameter \"{paramInfo.Name}\" marked with {nameof(FormatterParameterAttribute)} of format method \"{methodExpr.Method.Name}\" is not a string"
90+
);
91+
92+
if (!ShouldCompileToJsonPathExpression(argExpr))
93+
throw new InvalidOperationException(
94+
$"Argument {argExpr} corresponding to parameter \"{paramInfo.Name}\" marked with {nameof(FormatterParameterAttribute)} of format method \"{methodExpr.Method.Name}\" can not be compiled to expression string"
95+
);
96+
97+
args.Add(CreateExpressionString(argExpr));
98+
}
99+
else if (!IsEvaluatable(argExpr))
100+
{
101+
throw new InvalidOperationException(
102+
$"Argument {argExpr} corresponding to parameter \"{paramInfo.Name}\" of format method \"{methodExpr.Method.Name}\" can not be evaluated"
103+
);
104+
}
105+
else
106+
args.Add(EvaluateExpression(argExpr));
107+
}
108+
109+
if (methodExpr.Object is not null && !IsEvaluatable(methodExpr.Object))
110+
throw new InvalidOperationException(
111+
$"Format method \"{methodExpr.Method.Name}\" is being called on non evaluatable expression {methodExpr.Object}"
112+
);
113+
114+
return (string)methodExpr.Method.Invoke(methodExpr.Object is not null ? EvaluateExpression(methodExpr.Object) : null, args.ToArray());
115+
}
116+
69117
private static bool IsStringInterpolation(Expression expression) =>
70118
expression is MethodCallExpression { Method.Name: nameof(string.Format) } methodExpression
71119
&& methodExpression.Method.DeclaringType == typeof(string);
@@ -271,6 +319,9 @@ private static object CompileInterpolatedStringArgument(Expression expr)
271319
return ParseConstantExpression(cex);
272320
if (expr is UnaryExpression { NodeType: ExpressionType.Convert } uex)
273321
return CompileInterpolatedStringArgument(uex.Operand);
322+
if (IsConductorExpressionFormatString(expr))
323+
return CompileConductorExpressionFormatString((MethodCallExpression)expr);
324+
274325
return EvaluateExpression(expr);
275326
}
276327

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
using System;
2+
3+
namespace ConductorSharp.Engine.Util;
4+
5+
[AttributeUsage(AttributeTargets.Parameter)]
6+
public class FormatterParameterAttribute : Attribute;

src/ConductorSharp.KafkaCancellationNotifier/ConductorSharp.KafkaCancellationNotifier.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<TargetFramework>net6.0</TargetFramework>
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
7-
<Version>3.4.1</Version>
7+
<Version>3.5.0</Version>
88
<Authors>Codaxy</Authors>
99
<Company>Codaxy</Company>
1010
</PropertyGroup>

src/ConductorSharp.Patterns/ConductorSharp.Patterns.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
88
<Authors>Codaxy</Authors>
99
<Company>Codaxy</Company>
10-
<Version>3.4.1</Version>
10+
<Version>3.5.0</Version>
1111
</PropertyGroup>
1212

1313
<ItemGroup>

test/ConductorSharp.Engine.Tests/ConductorSharp.Engine.Tests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<None Remove="Samples\Workflows\DynamicTask.json" />
2121
<None Remove="Samples\Workflows\EvaluateExpressionWorkflow.json" />
2222
<None Remove="Samples\Workflows\EventTaskWorkflow.json" />
23+
<None Remove="Samples\Workflows\FormatterWorkflow.json" />
2324
<None Remove="Samples\Workflows\HumanTaskWorkflow.json" />
2425
<None Remove="Samples\Workflows\IndexerWorkflow.json" />
2526
<None Remove="Samples\Workflows\ListInitializationWorkflow.json" />
@@ -41,6 +42,7 @@
4142

4243
<ItemGroup>
4344
<EmbeddedResource Include="Samples\Workflows\DictionaryInitializationWorkflow.json" />
45+
<EmbeddedResource Include="Samples\Workflows\FormatterWorkflow.json" />
4446
<EmbeddedResource Include="Samples\Workflows\HumanTaskWorkflow.json" />
4547
<EmbeddedResource Include="Samples\Workflows\ListInitializationWorkflow.json" />
4648
<EmbeddedResource Include="Samples\Workflows\EvaluateExpressionWorkflow.json" />

test/ConductorSharp.Engine.Tests/Integration/WorkflowBuilderTests.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,11 +239,17 @@ public void BuilderReturnsCorrectDefinitionEvaluateExpressionWorkflow()
239239
}
240240

241241
[Fact]
242-
public void BuilderReturnsCorrectDefinitionNonEvaluatableWorkflow()
242+
public void BuilderThrowsNonEvaluatableWorkflowExcepion()
243243
{
244244
Assert.Throws<NonEvaluatableExpressionException>(GetDefinitionFromWorkflow<NonEvaluatableWorkflow>);
245245
}
246246

247+
[Fact]
248+
public void BuilderThrowsInvalidOperationExceptionForInvalidFormatterArgument()
249+
{
250+
Assert.Throws<InvalidOperationException>(GetDefinitionFromWorkflow<InvalidFormatterArgumentWorkflow>);
251+
}
252+
247253
[Fact]
248254
public void BuilderReturnsCorrectDefinitionWorkflowMetadataWorkflow()
249255
{
@@ -271,6 +277,15 @@ public void BuilderReturnsCorrectDefinitionDictionaryInitializationWorkflow()
271277
Assert.Equal(expectedDefinition, definition);
272278
}
273279

280+
[Fact]
281+
public void BuilderReturnsCorrectDefinitionFormatterWorkflow()
282+
{
283+
var definition = GetDefinitionFromWorkflow<FormatterWorkflow>();
284+
var expectedDefinition = EmbeddedFileHelper.GetLinesFromEmbeddedFile("~/Samples/Workflows/FormatterWorkflow.json");
285+
286+
Assert.Equal(expectedDefinition, definition);
287+
}
288+
274289
private static string GetDefinitionFromWorkflow<TWorkflow>()
275290
where TWorkflow : IConfigurableWorkflow
276291
{
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
namespace ConductorSharp.Engine.Tests.Samples.Workflows;
2+
3+
public class FormatterWorkflowInput : WorkflowInput<FormatterWorkflowOutput>
4+
{
5+
public string FirstName { get; set; }
6+
public string LastName { get; set; }
7+
}
8+
9+
public class FormatterWorkflowOutput : WorkflowOutput { }
10+
11+
public class FormatterWorkflow : Workflow<FormatterWorkflow, FormatterWorkflowInput, FormatterWorkflowOutput>
12+
{
13+
public class Library
14+
{
15+
public static Library Singleton { get; } = new();
16+
17+
[ConductorExpressionFormatter]
18+
public string PrepareEmail([FormatterParameter] string firstName, [FormatterParameter] string lastName) =>
19+
$"{firstName}.{lastName}@example.com";
20+
21+
[ConductorExpressionFormatter]
22+
public static string FormatName([FormatterParameter] string firstName, [FormatterParameter] string lastName, string prefix) =>
23+
$"{prefix} {firstName} {lastName}";
24+
}
25+
26+
public EmailPrepareV1 EmailPrepare { get; set; }
27+
28+
public FormatterWorkflow(WorkflowDefinitionBuilder<FormatterWorkflow, FormatterWorkflowInput, FormatterWorkflowOutput> builder)
29+
: base(builder) { }
30+
31+
public override void BuildDefinition()
32+
{
33+
_builder.AddTask(
34+
wf => wf.EmailPrepare,
35+
wf =>
36+
new()
37+
{
38+
Address = Library.Singleton.PrepareEmail(wf.Input.FirstName, wf.Input.LastName),
39+
Name = Library.FormatName(wf.Input.FirstName, wf.Input.LastName, "Mr")
40+
}
41+
);
42+
}
43+
}

0 commit comments

Comments
 (0)