w4c-workflows-api/Models/Nodes/FlowItem.cs

191 lines
6.3 KiB
C#
Raw Permalink Normal View History

2026-09-11 16:04:50 +00:00
using System.Text.Json.Nodes;
namespace w4c_workflows.Models.Nodes;
// ---------------------------------------------------------------------------
// The unit of data that flows between nodes. A node receives one or more
// FlowItems and produces zero or more per output port. Items carry their own
// provenance so expressions like $("Prepare") and the UI's "trace this item"
// can resolve which upstream item produced a given downstream item.
// ---------------------------------------------------------------------------
/// <summary>A binary payload stored out-of-band; items only hold a reference.</summary>
public sealed record BinaryAttachment(
string AssetId,
string? FileName = null,
string? MimeType = null,
long? SizeBytes = null);
/// <summary>A pointer back to an item emitted by another node run.</summary>
public sealed record ItemOrigin(
string NodeId,
int OutputIndex = 0,
int RunIndex = 0,
int ItemIndex = 0);
/// <summary>Where a <see cref="FlowItem"/> came from (for tracing and expressions).</summary>
public sealed record FlowItemOrigin(
string NodeId,
int OutputIndex = 0,
int RunIndex = 0,
int ItemIndex = 0,
List<ItemOrigin>? PairedItems = null);
/// <summary>
/// One data item. <see cref="Json"/> is the payload; <see cref="Binary"/> holds
/// named binary references; <see cref="Origin"/> links it to its source.
/// </summary>
public sealed record FlowItem
{
public required JsonObject Json { get; init; }
public Dictionary<string, BinaryAttachment>? Binary { get; init; }
public FlowItemOrigin? Origin { get; init; }
/// <summary>Convenience factory for a plain JSON item.</summary>
public static FlowItem FromJson(JsonObject json) => new() { Json = json };
/// <summary>Wraps a bare JSON value in a single "value" field.</summary>
public static FlowItem Scalar(JsonNode? value) => new()
{
Json = new JsonObject { ["value"] = value?.DeepClone() },
};
}
/// <summary>Helpers for turning sets of items into and out of JSON.</summary>
public static class FlowItemJson
{
/// <summary>Serializes a batch of items into a JSON array (or null when empty).</summary>
public static string? Serialize(IReadOnlyList<FlowItem> items)
{
if (items.Count == 0)
return null;
var array = new JsonArray();
foreach (var item in items)
{
var entry = new JsonObject { ["json"] = item.Json.DeepClone() };
if (item.Binary is { Count: > 0 })
{
var binary = new JsonObject();
foreach (var (name, attachment) in item.Binary)
{
binary[name] = new JsonObject
{
["assetId"] = attachment.AssetId,
["fileName"] = attachment.FileName,
["mimeType"] = attachment.MimeType,
["sizeBytes"] = attachment.SizeBytes,
};
}
entry["binary"] = binary;
}
if (item.Origin != null)
{
entry["origin"] = new JsonObject
{
["nodeId"] = item.Origin.NodeId,
["outputIndex"] = item.Origin.OutputIndex,
["runIndex"] = item.Origin.RunIndex,
["itemIndex"] = item.Origin.ItemIndex,
};
}
array.Add(entry);
}
return array.ToJsonString();
}
/// <summary>
/// Parses a JSON payload into items. Accepts a JSON array of
/// <c>{ "json": … }</c> envelopes, a JSON array of bare objects, or a single
/// bare object (which becomes a one-item batch).
/// </summary>
public static List<FlowItem> Parse(string? payload)
{
var items = new List<FlowItem>();
if (string.IsNullOrWhiteSpace(payload))
return items;
JsonNode? node;
try
{
node = JsonNode.Parse(payload);
}
catch
{
return items;
}
switch (node)
{
case JsonArray array:
foreach (var element in array)
items.Add(FromElement(element));
break;
case JsonObject obj:
items.Add(FromObject(obj));
break;
}
return items;
}
private static FlowItem FromElement(JsonNode? element) => element switch
{
JsonObject obj => FromObject(obj),
null => new FlowItem { Json = new JsonObject() },
_ => FlowItem.Scalar(element),
};
private static FlowItem FromObject(JsonObject obj)
{
// Envelope form { json, binary, origin }: unwrap; otherwise treat the
// whole object as the payload.
if (obj.ContainsKey("json") && obj["json"] is JsonObject payload)
{
return new FlowItem
{
Json = (JsonObject)payload.DeepClone(),
Origin = ParseOrigin(obj["origin"] as JsonObject),
Binary = ParseBinary(obj["binary"] as JsonObject),
};
}
return new FlowItem { Json = (JsonObject)obj.DeepClone() };
}
private static FlowItemOrigin? ParseOrigin(JsonObject? origin)
{
if (origin == null)
return null;
return new FlowItemOrigin(
origin["nodeId"]?.GetValue<string>() ?? string.Empty,
origin["outputIndex"]?.GetValue<int>() ?? 0,
origin["runIndex"]?.GetValue<int>() ?? 0,
origin["itemIndex"]?.GetValue<int>() ?? 0);
}
private static Dictionary<string, BinaryAttachment>? ParseBinary(JsonObject? binary)
{
if (binary == null || binary.Count == 0)
return null;
var result = new Dictionary<string, BinaryAttachment>(StringComparer.Ordinal);
foreach (var (name, value) in binary)
{
if (value is not JsonObject entry)
continue;
result[name] = new BinaryAttachment(
entry["assetId"]?.GetValue<string>() ?? string.Empty,
entry["fileName"]?.GetValue<string>(),
entry["mimeType"]?.GetValue<string>(),
entry["sizeBytes"]?.GetValue<long>());
}
return result;
}
}