Add event-driven quick action dispatch
This commit is contained in:
62
F4SD-ActionConnector/Execution/ActionDispatcher.cs
Normal file
62
F4SD-ActionConnector/Execution/ActionDispatcher.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using F4SD.ActionConnector.Enumerations;
|
||||
using F4SD.ActionConnector.Models;
|
||||
using static C4IT.Logging.cLogManager;
|
||||
|
||||
namespace F4SD.ActionConnector.Execution
|
||||
{
|
||||
/// <summary>
|
||||
/// Routes a single action definition to the correct executor based on its <see cref="ActionType"/>.
|
||||
/// </summary>
|
||||
internal sealed class ActionDispatcher
|
||||
{
|
||||
private readonly Func<QuickActionRequest, Task<ActionResult>> _quickActionExecutor;
|
||||
|
||||
internal ActionDispatcher(Func<QuickActionRequest, Task<ActionResult>> quickActionExecutor)
|
||||
{
|
||||
_quickActionExecutor = quickActionExecutor;
|
||||
}
|
||||
|
||||
internal async Task<ActionResult> DispatchAsync(ActionDefinitionBase action, ActionContext context)
|
||||
{
|
||||
if (!action.IsEnabled)
|
||||
return ActionResult.Skip(action.Id);
|
||||
|
||||
switch (action.Type)
|
||||
{
|
||||
case ActionType.QuickAction:
|
||||
return await ExecuteQuickActionAsync((QuickActionDefinition)action, context);
|
||||
|
||||
case ActionType.HttpCall:
|
||||
case ActionType.Webhook:
|
||||
throw new NotImplementedException(
|
||||
$"ActionType '{action.Type}' is not yet implemented.");
|
||||
|
||||
default:
|
||||
LogEntry($"ActionDispatcher: unknown ActionType '{action.Type}' for action '{action.Id}'.",
|
||||
C4IT.Logging.LogLevels.Warning);
|
||||
return ActionResult.Skip(action.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ActionResult> ExecuteQuickActionAsync(QuickActionDefinition definition, ActionContext context)
|
||||
{
|
||||
if (_quickActionExecutor == null)
|
||||
throw new InvalidOperationException(
|
||||
$"A QuickAction ('{definition.Id}') was triggered but no {nameof(Bus.ActionBusOptions.QuickActionExecutor)} was registered.");
|
||||
|
||||
var resolvedParameters = PayloadVariableResolver.Resolve(definition.Parameters, context.Payload);
|
||||
|
||||
var request = new QuickActionRequest
|
||||
{
|
||||
ActionId = definition.Id,
|
||||
QuickActionRef = definition.QuickActionRef,
|
||||
Parameters = resolvedParameters,
|
||||
Context = context,
|
||||
};
|
||||
|
||||
return await _quickActionExecutor(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
94
F4SD-ActionConnector/Execution/PayloadVariableResolver.cs
Normal file
94
F4SD-ActionConnector/Execution/PayloadVariableResolver.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using F4SD.ActionConnector.Payloads;
|
||||
|
||||
namespace F4SD.ActionConnector.Execution
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves template variables such as {{Case.Id}} or {{Case.ClosedAt:yyyy-MM-ddTHH:mm:ssZ}}
|
||||
/// against a payload object.
|
||||
///
|
||||
/// Variable format: {{ObjectName.PropertyName[:FormatSpecifier]}}
|
||||
/// </summary>
|
||||
internal static class PayloadVariableResolver
|
||||
{
|
||||
private static readonly Regex VariablePattern =
|
||||
new Regex(@"\{\{(?<obj>[^.}]+)\.(?<prop>[^:}]+)(?::(?<fmt>[^}]+))?\}\}",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new dictionary with every value in <paramref name="rawParameters"/> having
|
||||
/// its template variables replaced by the corresponding payload property values.
|
||||
/// Pure single-variable templates (e.g. <c>{{Case.ClosedAt}}</c>) yield the raw typed
|
||||
/// object; mixed or formatted templates yield a resolved string.
|
||||
/// </summary>
|
||||
internal static IDictionary<string, object> Resolve(
|
||||
IDictionary<string, string> rawParameters,
|
||||
PayloadBase payload)
|
||||
{
|
||||
var resolved = new Dictionary<string, object>(rawParameters.Count);
|
||||
|
||||
foreach (var pair in rawParameters)
|
||||
resolved[pair.Key] = ResolveValue(pair.Value, payload);
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private static object ResolveValue(string template, PayloadBase payload)
|
||||
{
|
||||
var match = VariablePattern.Match(template);
|
||||
|
||||
// Pure single variable with no format specifier → return raw object
|
||||
if (match.Success && match.Value == template && !match.Groups["fmt"].Success)
|
||||
return ResolveRawProperty(match, payload);
|
||||
|
||||
// Mixed template, literal, or explicit format specifier → return resolved string
|
||||
return ResolveString(template, payload);
|
||||
}
|
||||
|
||||
private static object ResolveRawProperty(Match match, PayloadBase payload)
|
||||
{
|
||||
string propertyName = match.Groups["prop"].Value;
|
||||
|
||||
PropertyInfo property = payload.GetType().GetProperty(
|
||||
propertyName,
|
||||
BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
||||
|
||||
if (property == null)
|
||||
return match.Value;
|
||||
|
||||
return property.GetValue(payload);
|
||||
}
|
||||
|
||||
private static string ResolveString(string template, PayloadBase payload)
|
||||
{
|
||||
return VariablePattern.Replace(template, match => ResolveMatch(match, payload));
|
||||
}
|
||||
|
||||
private static string ResolveMatch(Match match, PayloadBase payload)
|
||||
{
|
||||
string propertyName = match.Groups["prop"].Value;
|
||||
string formatSpecifier = match.Groups["fmt"].Success ? match.Groups["fmt"].Value : null;
|
||||
|
||||
PropertyInfo property = payload.GetType().GetProperty(
|
||||
propertyName,
|
||||
BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
||||
|
||||
if (property == null)
|
||||
return match.Value;
|
||||
|
||||
object value = property.GetValue(payload);
|
||||
|
||||
if (value == null)
|
||||
return string.Empty;
|
||||
|
||||
if (formatSpecifier != null && value is IFormattable formattable)
|
||||
return formattable.ToString(formatSpecifier, CultureInfo.InvariantCulture);
|
||||
|
||||
return value.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
29
F4SD-ActionConnector/Execution/QuickActionRequest.cs
Normal file
29
F4SD-ActionConnector/Execution/QuickActionRequest.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System.Collections.Generic;
|
||||
using F4SD.ActionConnector.Models;
|
||||
|
||||
namespace F4SD.ActionConnector.Execution
|
||||
{
|
||||
public sealed class QuickActionRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Matches <see cref="QuickActionDefinition.Id"/> from the configuration.
|
||||
/// </summary>
|
||||
public string ActionId { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the Quick Action to invoke, e.g. "Document case in JIRA".
|
||||
/// </summary>
|
||||
public string QuickActionRef { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Template variables resolved against the event payload.
|
||||
/// Values are the raw payload values; callers are responsible for casting.
|
||||
/// </summary>
|
||||
public IDictionary<string, object> Parameters { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Full context of the triggering event.
|
||||
/// </summary>
|
||||
public ActionContext Context { get; internal set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user