Skip to content

Commit

Permalink
Update DSL implementation and integration (#4073)
Browse files Browse the repository at this point in the history
  • Loading branch information
sfmskywalker authored May 28, 2023
1 parent 7d56d00 commit 6f455f7
Show file tree
Hide file tree
Showing 53 changed files with 957 additions and 385 deletions.
7 changes: 7 additions & 0 deletions Elsa.sln
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Environments", "src\mo
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.EntityFrameworkCore.MySql", "src\modules\Elsa.EntityFrameworkCore.MySql\Elsa.EntityFrameworkCore.MySql.csproj", "{8DC74562-1F06-4AFA-8308-8A779E8812A3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.AspNet.DslWorkflowProvider", "src\samples\aspnet\Elsa.Samples.AspNet.DslWorkflowProvider\Elsa.Samples.AspNet.DslWorkflowProvider.csproj", "{4BC5D3D2-9425-41D7-9804-D145B92520EE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -516,6 +518,10 @@ Global
{8DC74562-1F06-4AFA-8308-8A779E8812A3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8DC74562-1F06-4AFA-8308-8A779E8812A3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8DC74562-1F06-4AFA-8308-8A779E8812A3}.Release|Any CPU.Build.0 = Release|Any CPU
{4BC5D3D2-9425-41D7-9804-D145B92520EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4BC5D3D2-9425-41D7-9804-D145B92520EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4BC5D3D2-9425-41D7-9804-D145B92520EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4BC5D3D2-9425-41D7-9804-D145B92520EE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{155227F0-A33B-40AA-A4B4-06F813EB921B} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F}
Expand Down Expand Up @@ -606,5 +612,6 @@ Global
{165ACC9D-B67C-4B49-A818-ECFD247C899C} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5}
{B5CDF747-8066-40D5-9BAE-CBE397BC9B51} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
{8DC74562-1F06-4AFA-8308-8A779E8812A3} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
{4BC5D3D2-9425-41D7-9804-D145B92520EE} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5}
EndGlobalSection
EndGlobal
6 changes: 3 additions & 3 deletions src/common/Elsa.Features/Implementations/Module.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace Elsa.Features.Implementations;
/// <inheritdoc />
public class Module : IModule
{
private record HostedServiceDescriptor(int Order, Type HostedServiceType);
private record HostedServiceDescriptor(int Order, Type Type);

private IDictionary<Type, IFeature> _features = new Dictionary<Type, IFeature>();
private readonly ISet<IFeature> _configuredFeatures = new HashSet<IFeature>();
Expand Down Expand Up @@ -84,12 +84,12 @@ public void Apply()
}

foreach (var hostedServiceDescriptor in _hostedServiceDescriptors.OrderBy(x => x.Order))
Services.TryAddEnumerable(ServiceDescriptor.Singleton(typeof(IHostedService), hostedServiceDescriptor.HostedServiceType));
Services.TryAddEnumerable(ServiceDescriptor.Singleton(typeof(IHostedService), hostedServiceDescriptor.Type));

// Make sure to use the complete list of features when applying them.
foreach (var feature in _features.Values)
feature.Apply();

// Add a registry of enabled features to the service collection for client applications to reflect on what features are installed.
var registry = new InstalledFeatureRegistry();
foreach (var feature in _features.Values)
Expand Down
8 changes: 4 additions & 4 deletions src/common/Elsa.Features/Services/IModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,22 @@ public interface IModule
/// A dictionary into which features can stash away values for later use.
/// </summary>
IDictionary<object, object> Properties { get; }

/// <summary>
/// Creates and configures a feature of the specified type.
/// </summary>
T Configure<T>(Action<T>? configure = default) where T : class, IFeature;

/// <summary>
/// Creates and configures a feature of the specified type.
/// </summary>
T Configure<T>(Func<IModule, T> factory, Action<T>? configure = default) where T : class, IFeature;

/// <summary>
/// Configures a <see cref="IHostedService"/> using an optional priority to control in which order it will be registered with the service container.
/// </summary>
IModule ConfigureHostedService<T>(int priority = 0) where T : class, IHostedService;

/// <summary>
/// Will apply all configured features, causing the <see cref="Services"/> collection to be populated.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions src/common/Elsa.Testing.Shared/TestApplicationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public TestApplicationBuilder(ITestOutputHelper testOutputHelper)
.UseScheduling()
.UseJavaScript()
.UseLiquid()
.UseDsl()
.UseWorkflows(workflows => workflows
.WithStandardOutStreamProvider(_ => new StandardOutStreamProvider(new XunitConsoleTextWriter(_testOutputHelper)))
);
Expand Down
26 changes: 25 additions & 1 deletion src/modules/Elsa.Dsl/Contracts/IFunctionActivityRegistry.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,33 @@
using Elsa.Dsl.Models;
using Elsa.Workflows.Core.Contracts;

namespace Elsa.Dsl.Contracts;

/// <summary>
/// Provides a registry for mapping functions to activities that can be invoked from a DSL script.
/// </summary>
public interface IFunctionActivityRegistry
{
void RegisterFunction(string functionName, string activityTypeName, IEnumerable<string>? propertyNames = default);
/// <summary>
/// Registers a function that is mapped to an activity that can be invoked from a DSL script.
/// </summary>
/// <param name="functionName">The name of the function.</param>
/// <param name="activityTypeName">The name of the activity type.</param>
/// <param name="propertyNames">The names of the properties that are mapped to the function arguments.</param>
/// <param name="configure">An optional action that can be used to configure the activity.</param>
void RegisterFunction(string functionName, string activityTypeName, IEnumerable<string>? propertyNames = default, Action<IActivity>? configure = default);

/// <summary>
/// Registers a function that is mapped to an activity that can be invoked from a DSL script.
/// </summary>
/// <param name="descriptor">The descriptor that describes the function.</param>
void RegisterFunction(FunctionActivityDescriptor descriptor);

/// <summary>
/// Resolves a function to an activity that can be invoked from a DSL script.
/// </summary>
/// <param name="functionName">The name of the function.</param>
/// <param name="arguments">The arguments that are passed to the function.</param>
/// <returns>An activity that can be invoked from a DSL script.</returns>
IActivity ResolveFunction(string functionName, IEnumerable<object?>? arguments = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,21 @@ namespace Elsa.Dsl.Interpreters;

public partial class WorkflowDefinitionBuilderInterpreter
{
/// <inheritdoc />
public override IWorkflowBuilder VisitBracketsExpr(ElsaParser.BracketsExprContext context)
{
var propertyType = _expressionType.Get(context.Parent);
var targetElementType = propertyType.GetGenericArguments().First();
var targetElementType = propertyType?.GetGenericArguments().First() ?? typeof(object);
var contents = context.exprList().expr();

var items = contents.Select(x =>
{
Visit(x);
var objectContext = x.GetChild<ElsaParser.ObjectContext>(0);
return _expressionValue.Get(objectContext);
var stringContext = x.GetChild<ElsaParser.StringValueExprContext>(0) ?? x;
return _expressionValue.Get(stringContext);
}).ToList();

var stronglyTypedListType = typeof(ICollection<>).MakeGenericType(targetElementType);
var stronglyTypedListType = targetElementType;
var stronglyTypedList = items.ConvertTo(stronglyTypedListType);

_expressionValue.Put(context, stronglyTypedList);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
using Elsa.Workflows.Core.Contracts;
using System.Text.Json;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;

namespace Elsa.Dsl.Interpreters;

public partial class WorkflowDefinitionBuilderInterpreter
{
/// <inheritdoc />
public override IWorkflowBuilder VisitObjectExpr(ElsaParser.ObjectExprContext context)
{
VisitChildren(context);
Expand All @@ -13,6 +16,7 @@ public override IWorkflowBuilder VisitObjectExpr(ElsaParser.ObjectExprContext co
return DefaultResult;
}

/// <inheritdoc />
public override IWorkflowBuilder VisitObjectStat(ElsaParser.ObjectStatContext context)
{
VisitChildren(context);
Expand All @@ -22,9 +26,34 @@ public override IWorkflowBuilder VisitObjectStat(ElsaParser.ObjectStatContext co
return DefaultResult;
}

/// <inheritdoc />
public override IWorkflowBuilder VisitObject(ElsaParser.ObjectContext context)
{
var @object = GetObject(context);

_object.Put(context, @object);
_expressionValue.Put(context, @object);
VisitChildren(context);

return DefaultResult;
}

private object GetObject(ElsaParser.ObjectContext context)
{
var objectTypeName = context.ID().GetText();

// First, check if the symbol matches an activity type.
var activityType = _activityRegistry.Find(x => x.Name == objectTypeName);

if (activityType != null)
{
// TODO: Refactor this to remove the dependency on JsonElement and JsonSerializerOptions.
// This limits the ability to use this class in other contexts, such as constructing activities from the DSL.
var jsonElement = JsonSerializer.Deserialize<JsonElement>("{}");
var ctorArgs = new ActivityConstructorContext(jsonElement, new JsonSerializerOptions());
return activityType.Constructor(ctorArgs);
}

var objectTypeDescriptor = _typeSystem.ResolveTypeName(objectTypeName);

if (objectTypeDescriptor == null)
Expand All @@ -35,7 +64,7 @@ public override IWorkflowBuilder VisitObject(ElsaParser.ObjectContext context)
_expressionValue.Put(context, definedVariable.Value);
return DefaultResult;
}

// Or a workflow variable?
var workflowVariableQuery =
from container in _containerStack
Expand All @@ -50,17 +79,13 @@ from variable in container.Variables
_expressionValue.Put(context, workflowVariable);
return DefaultResult;
}

throw new Exception($"Unknown type: {objectTypeName}");
}

var objectType = objectTypeDescriptor.Type;
var @object = Activator.CreateInstance(objectType)!;

_object.Put(context, @object);
_expressionValue.Put(context, @object);
VisitChildren(context);

return DefaultResult;
return @object;
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
using System.Reflection;
using Elsa.Expressions.Helpers;
using Elsa.Expressions.Models;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;

namespace Elsa.Dsl.Interpreters;

public partial class WorkflowDefinitionBuilderInterpreter
{
/// <inheritdoc />
public override IWorkflowBuilder VisitProperty(ElsaParser.PropertyContext context)
{
var @object = _object.Get(context.Parent.Parent.Parent);
Expand All @@ -21,7 +23,9 @@ public override IWorkflowBuilder VisitProperty(ElsaParser.PropertyContext contex
VisitChildren(context);

var propertyValue = _expressionValue.Get(context.expr());
SetPropertyValue(@object, propertyInfo, propertyValue);
var propertyType = propertyInfo.PropertyType;
var parsedPropertyValue = typeof(Input).IsAssignableFrom(propertyType) ? propertyValue : propertyValue.ConvertTo(propertyType);
SetPropertyValue(@object, propertyInfo, parsedPropertyValue);

return DefaultResult;
}
Expand All @@ -37,19 +41,25 @@ private void SetPropertyValue(object target, PropertyInfo propertyInfo, object?
private Input CreateInputValue(PropertyInfo propertyInfo, object? propertyValue)
{
var underlyingType = propertyInfo.PropertyType.GetGenericArguments().First();
var propertyValueType = propertyValue?.GetType();
var parsedPropertyValue = propertyValue.ConvertTo(underlyingType);
var propertyValueType = parsedPropertyValue?.GetType();
var inputType = typeof(Input<>).MakeGenericType(underlyingType);

if (propertyValueType != null)
{
var hasCtorWithSpecifiedType = inputType.GetConstructors().Any(x => x.GetParameters().Any(y => y.ParameterType.IsAssignableFrom(propertyValueType)));
// Create a literal value.
var literalType = typeof(Literal<>).MakeGenericType(underlyingType);
var hasCtorWithSpecifiedType = inputType.GetConstructors().Any(x => x.GetParameters().Any(y => y.ParameterType.IsAssignableFrom(literalType)));

if (hasCtorWithSpecifiedType)
return (Input)Activator.CreateInstance(inputType, propertyValue)!;
{
var literalValue = Activator.CreateInstance(literalType, parsedPropertyValue)!;
return (Input)Activator.CreateInstance(inputType, literalValue)!;
}
}

return propertyValue is ExternalExpressionReference externalExpressionReference
? (Input)Activator.CreateInstance(inputType, externalExpressionReference.Expression, externalExpressionReference.BlockReference)!
: (Input)Activator.CreateInstance(inputType, propertyValue.ConvertTo(underlyingType))!;
return parsedPropertyValue is ExternalExpressionReference externalExpressionReference
? (Input)Activator.CreateInstance(inputType, externalExpressionReference.Expression, externalExpressionReference.BlockReference)!
: (Input)Activator.CreateInstance(inputType, parsedPropertyValue)!;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using Antlr4.Runtime.Tree;
using Elsa.Dsl.Contracts;
using Elsa.Dsl.Models;
using Elsa.Expressions.Contracts;
using Elsa.Workflows.Core.Activities;
using Elsa.Workflows.Core.Contracts;

Expand All @@ -10,8 +9,8 @@ namespace Elsa.Dsl.Interpreters;
public partial class WorkflowDefinitionBuilderInterpreter : ElsaParserBaseVisitor<IWorkflowBuilder>
{
private readonly ITypeSystem _typeSystem;
private readonly IActivityRegistry _activityRegistry;
private readonly IFunctionActivityRegistry _functionActivityRegistry;
private readonly IExpressionHandlerRegistry _expressionHandlerRegistry;
private readonly IWorkflowBuilder _workflowBuilder;
private readonly ParseTreeProperty<object> _object = new();
private readonly ParseTreeProperty<object?> _expressionValue = new();
Expand All @@ -22,22 +21,17 @@ public partial class WorkflowDefinitionBuilderInterpreter : ElsaParserBaseVisito

/// <inheritdoc />
public WorkflowDefinitionBuilderInterpreter(
ITypeSystem typeSystem,
IFunctionActivityRegistry functionActivityRegistry,
IExpressionHandlerRegistry expressionHandlerRegistry,
IWorkflowBuilderFactory workflowBuilderFactory,
WorkflowDefinitionInterpreterSettings settings)
ITypeSystem typeSystem,
IActivityRegistry activityRegistry,
IFunctionActivityRegistry functionActivityRegistry,
IWorkflowBuilderFactory workflowBuilderFactory)
{
_typeSystem = typeSystem;
_activityRegistry = activityRegistry;
_functionActivityRegistry = functionActivityRegistry;
_expressionHandlerRegistry = expressionHandlerRegistry;
_workflowBuilder = workflowBuilderFactory.CreateBuilder();
}

/// <inheritdoc />
protected override IWorkflowBuilder DefaultResult => _workflowBuilder;

private void VisitMany(IEnumerable<IParseTree> contexts)
{
foreach (var parseTree in contexts) Visit(parseTree);
}
}
12 changes: 12 additions & 0 deletions src/modules/Elsa.Dsl/Models/FunctionActivityDescriptor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Elsa.Workflows.Core.Contracts;

namespace Elsa.Dsl.Models;

/// <summary>
/// Describes a function that is mapped to an activity that can be invoked from a DSL script.
/// </summary>
/// <param name="FunctionName">The name of the function.</param>
/// <param name="ActivityTypeName">The name of the activity type.</param>
/// <param name="PropertyNames">The names of the properties that are mapped to the function arguments.</param>
/// <param name="Configure">An optional action that can be used to configure the activity.</param>
public record FunctionActivityDescriptor(string FunctionName, string ActivityTypeName, IEnumerable<string>? PropertyNames = default, Action<IActivity>? Configure = default);
Loading

0 comments on commit 6f455f7

Please sign in to comment.