101 lines
4.2 KiB
C#
101 lines
4.2 KiB
C#
using System.Text.Json.Nodes;
|
|
|
|
namespace w4c_workflows.Models.Nodes;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// A compiled node graph: the executable form of the node-mode half of a
|
|
// workflow definition. It is deliberately free of EF/DB types so it can be
|
|
// built, validated and run in isolation (unit tests, previews, dry runs).
|
|
//
|
|
// `next`/`onError` from the YAML are lowered to edges here, so the run kernel
|
|
// only ever follows edges — never a single "next" pointer.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// <summary>One step in a node graph: a resolved blueprint plus its parameters.</summary>
|
|
public sealed record NodeGraphNode
|
|
{
|
|
/// <summary>The YAML task id; stable and used in expressions such as <c>$("stepId")</c>.</summary>
|
|
public required string Id { get; init; }
|
|
|
|
/// <summary>Blueprint resolved from the catalog (validated at compile time).</summary>
|
|
public required NodeBlueprint Blueprint { get; init; }
|
|
|
|
/// <summary>Raw (unresolved) parameters; interpolation happens per item at run time.</summary>
|
|
public required JsonObject Parameters { get; init; }
|
|
|
|
/// <summary>
|
|
/// Credentials this step needs, keyed by the blueprint credential alias;
|
|
/// the value is a vault credential id or unique name, resolved at run time.
|
|
/// </summary>
|
|
public IReadOnlyDictionary<string, string> CredentialRefs { get; init; } =
|
|
new Dictionary<string, string>();
|
|
|
|
/// <summary>Run-mode override; null means "use the blueprint default".</summary>
|
|
public string? RunMode { get; init; }
|
|
|
|
/// <summary>Effective run mode (override, else blueprint).</summary>
|
|
public string EffectiveRunMode =>
|
|
string.IsNullOrWhiteSpace(RunMode) ? Blueprint.RunMode : RunMode!;
|
|
|
|
public bool ContinueOnFail { get; init; }
|
|
|
|
public TaskRetryDefinition? Retry { get; init; }
|
|
|
|
public NodePort Output(int index)
|
|
=> index >= 0 && index < Blueprint.Outputs.Count ? Blueprint.Outputs[index] : NodePort.Main;
|
|
|
|
public NodePort Input(int index)
|
|
=> index >= 0 && index < Blueprint.Inputs.Count ? Blueprint.Inputs[index] : NodePort.Main;
|
|
|
|
/// <summary>Index of the blueprint's error output, or -1 when it has none.</summary>
|
|
public int ErrorOutputIndex
|
|
=> Blueprint.Outputs.FindIndex(p => string.Equals(p.Kind, PortKind.Error, StringComparison.Ordinal));
|
|
}
|
|
|
|
/// <summary>A directed connection between two steps on specific ports.</summary>
|
|
public sealed record NodeGraphEdge
|
|
{
|
|
public required string FromNodeId { get; init; }
|
|
public int FromOutput { get; init; }
|
|
public required string ToNodeId { get; init; }
|
|
public int ToInput { get; init; }
|
|
|
|
/// <summary>
|
|
/// True when this edge closes a loop (a back edge into a loop node). It does
|
|
/// not count toward the target's readiness; instead it re-triggers the loop
|
|
/// node for the next iteration.
|
|
/// </summary>
|
|
public bool IsLoopBack { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// A validated, runnable node graph. <see cref="EntryNodeId"/> is the single
|
|
/// node with no incoming non-loop edge. Cycles are rejected at compile time
|
|
/// unless they close through a loop-capable node (a loop-back edge).
|
|
/// </summary>
|
|
public sealed record NodeGraph
|
|
{
|
|
public required IReadOnlyList<NodeGraphNode> Nodes { get; init; }
|
|
public required IReadOnlyList<NodeGraphEdge> Edges { get; init; }
|
|
public required string EntryNodeId { get; init; }
|
|
|
|
private Dictionary<string, NodeGraphNode>? _byId;
|
|
|
|
public NodeGraphNode? Find(string nodeId)
|
|
{
|
|
_byId ??= Nodes.ToDictionary(n => n.Id, StringComparer.Ordinal);
|
|
return _byId.TryGetValue(nodeId, out var node) ? node : null;
|
|
}
|
|
|
|
public NodeGraphNode Require(string nodeId)
|
|
=> Find(nodeId) ?? throw new InvalidOperationException($"node '{nodeId}' is not in the graph");
|
|
|
|
/// <summary>Edges leaving a specific output port.</summary>
|
|
public IEnumerable<NodeGraphEdge> EdgesFrom(string nodeId, int output)
|
|
=> Edges.Where(e => e.FromNodeId == nodeId && e.FromOutput == output);
|
|
|
|
/// <summary>Edges arriving at a node, across all inputs.</summary>
|
|
public IEnumerable<NodeGraphEdge> EdgesTo(string nodeId)
|
|
=> Edges.Where(e => e.ToNodeId == nodeId);
|
|
}
|