using System.Text.Json;
using System.Text.Json.Serialization;
namespace w4c_workflows.Models.Nodes;
// ---------------------------------------------------------------------------
// The declarative definition of a node TYPE ("blueprint"). This is the catalog
// entry that both the compiler/validator and the authoring UI consume: it says
// what a node is called, which ports it has, which parameters it exposes and
// which credentials it needs. Runtime behaviour lives behind INodeExecutor.
//
// Field names are camelCase on the wire (System.Text.Json Web defaults) so the
// catalog JSON files in Services/Nodes/Catalog read naturally.
// ---------------------------------------------------------------------------
/// Broad role of a blueprint. Stored as text so new roles need no migration.
public static class NodeKind
{
public const string Action = "action";
public const string Trigger = "trigger";
public const string Transform = "transform";
public const string Control = "control";
public const string Ai = "ai";
}
/// Connection channel a port belongs to. "main" carries flow items.
public static class PortKind
{
public const string Main = "main";
public const string Error = "error";
public const string Ai = "ai";
}
/// How the runtime hands items to a node.
public static class NodeRunMode
{
/// Invoke once per input item (the default).
public const string EachItem = "eachItem";
/// Invoke once with the whole input array (aggregates, code nodes).
public const string AllItems = "allItems";
}
/// Parameter field kinds the authoring UI knows how to render.
public static class NodeParameterType
{
public const string String = "string";
public const string Number = "number";
public const string Boolean = "boolean";
public const string Options = "options";
public const string MultiOptions = "multiOptions";
public const string Collection = "collection";
public const string FixedCollection = "fixedCollection";
public const string ResourceLocator = "resourceLocator";
public const string Json = "json";
public const string Notice = "notice";
public const string DateTime = "dateTime";
}
/// Where a blueprint came from — drives provenance badges and filtering.
public static class NodeOrigin
{
public const string BuiltIn = "builtIn";
public const string Connector = "connector";
public const string OpenApi = "openApi";
public const string Custom = "custom";
}
///
/// A node type definition. Version is a decimal so it can track the same
/// minor-version history the reference integrations use (e.g. 2.7).
///
public sealed record NodeBlueprint
{
/// Stable machine id, e.g. "httpRequest" or "core.set".
public required string Type { get; init; }
public double Version { get; init; } = 1;
public required string DisplayName { get; init; }
public string? Description { get; init; }
/// Optional expression string for the step-card subtitle.
public string? Subtitle { get; init; }
/// One of .
public string Kind { get; init; } = NodeKind.Action;
public List Categories { get; init; } = new();
public string? Icon { get; init; }
public string? IconColor { get; init; }
public List Inputs { get; init; } = new() { NodePort.Main };
public List Outputs { get; init; } = new() { NodePort.Main };
/// One of .
public string RunMode { get; init; } = NodeRunMode.EachItem;
public List Parameters { get; init; } = new();
public List Credentials { get; init; } = new();
/// Names of dynamic methods this node exposes (loadOptions/listSearch/schema).
public List Methods { get; init; } = new();
///
/// Declarative REST connector definition. When present (and
/// is ) the shared
/// RestConnectorExecutor runs it, so an integration is catalog data,
/// not a bespoke executor.
///
public NodeConnector? Connector { get; init; }
/// One of .
public string Origin { get; init; } = NodeOrigin.BuiltIn;
public string? DocumentationUrl { get; init; }
/// True when the node is hidden from the default palette (internal/test).
public bool Hidden { get; init; }
///
/// True when the node can host a loop: an edge back into it is treated as a
/// loop-back edge (excluded from the readiness count) so the loop body can
/// re-run. Only nodes that keep their own iteration state should set this.
///
public bool LoopBack { get; init; }
[JsonIgnore]
public bool ProducesErrorBranch =>
Outputs.Any(p => string.Equals(p.Kind, PortKind.Error, StringComparison.Ordinal));
/// True when the blueprint exposes a parameter with the given name at any depth.
public bool HasParameter(string name) => ParameterIndex.ContainsKey(name);
///
/// Resolves a top-level parameter by name, or null. Precomputed so compile
/// validation does not rescan the parameter list per supplied parameter.
///
public NodeParameter? FindParameter(string name)
=> TopLevelParameterIndex.TryGetValue(name, out var parameter) ? parameter : null;
private Dictionary? _parameterIndex;
private Dictionary? _topLevelParameterIndex;
private Dictionary ParameterIndex => _parameterIndex ??=
BuildParameterIndex(NodeParameterDescender.DescendAll(Parameters));
private Dictionary TopLevelParameterIndex => _topLevelParameterIndex ??=
BuildParameterIndex(Parameters);
/// First-wins name index; duplicate names at one level are tolerated.
private static Dictionary BuildParameterIndex(IEnumerable parameters)
{
var index = new Dictionary(StringComparer.Ordinal);
foreach (var parameter in parameters)
index.TryAdd(parameter.Name, parameter);
return index;
}
}
/// A named, typed connection point on a node.
public sealed record NodePort
{
/// One of .
public string Kind { get; init; } = PortKind.Main;
/// Optional display label ("true", "false", "done", "loop").
public string? Label { get; init; }
public static readonly NodePort Main = new() { Kind = PortKind.Main };
public static readonly NodePort Error = new() { Kind = PortKind.Error, Label = "error" };
}
/// A credential a node may need, referenced by alias in task parameters.
public sealed record NodeCredentialLink
{
public required string Alias { get; init; }
public string? DisplayName { get; init; }
public bool Required { get; init; }
}
///
/// One form field in a node's parameter schema. Intentionally close to the
/// reference corpora's property model so migrated integrations map cleanly, but
/// with our own (clearer) names.
///
public sealed record NodeParameter
{
/// Machine name used in task parameters and expressions.
public required string Name { get; init; }
public required string DisplayName { get; init; }
/// One of .
public required string Type { get; init; }
///
/// Default value, or null when the parameter has none. Nullable is
/// deliberate: a missing default deserializes to null, whereas a
/// cannot be
/// written by System.Text.Json (it throws on serialize).
///
public JsonElement? Default { get; init; }
public string? Description { get; init; }
public string? Placeholder { get; init; }
public bool Required { get; init; }
/// When true the field is a configuration value and never interpolated.
public bool NoDataExpression { get; init; }
/// Value list for options/multiOptions.
public List? Options { get; init; }
/// Nested fields for collection/fixedCollection.
public List? Fields { get; init; }
/// Conditional visibility relative to sibling parameter values.
public NodeVisibility? VisibleWhen { get; init; }
/// Method on the blueprint used to fetch options dynamically.
public string? LoadOptionsMethod { get; init; }
/// Parameter names that must change before options are re-fetched.
public List? DependsOn { get; init; }
/// JSON Schema-ish extras (min/max, precision, multi-value, …); null when none.
[JsonPropertyName("typeOptions")]
public JsonElement? TypeOptions { get; init; }
}
/// A selectable value for an options/multiOptions parameter.
public sealed record NodeParameterOption
{
public required string Name { get; init; }
public required JsonElement Value { get; init; }
public string? Description { get; init; }
}
///
/// Conditional visibility for a parameter. A field is shown when every entry in
/// matches and no entry in matches; a
/// missing parameter is treated as not matching.
///
public sealed record NodeVisibility
{
public Dictionary>? Show { get; init; }
public Dictionary>? Hide { get; init; }
}
/// Walks a parameter tree (nested collection fields) in depth-first order.
internal static class NodeParameterDescender
{
internal static IEnumerable DescendAll(IEnumerable parameters)
{
foreach (var parameter in parameters)
{
yield return parameter;
if (parameter.Fields is { Count: > 0 })
{
foreach (var nested in DescendAll(parameter.Fields))
yield return nested;
}
}
}
}
/// Serialization options shared by catalog loading and API responses.
public static class NodeJson
{
public static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = false,
};
}