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.
// ---------------------------------------------------------------------------
/// A binary payload stored out-of-band; items only hold a reference.
public sealed record BinaryAttachment(
string AssetId,
string? FileName = null,
string? MimeType = null,
long? SizeBytes = null);
/// A pointer back to an item emitted by another node run.
public sealed record ItemOrigin(
string NodeId,
int OutputIndex = 0,
int RunIndex = 0,
int ItemIndex = 0);
/// Where a came from (for tracing and expressions).
public sealed record FlowItemOrigin(
string NodeId,
int OutputIndex = 0,
int RunIndex = 0,
int ItemIndex = 0,
List? PairedItems = null);
///
/// One data item. is the payload; holds
/// named binary references; links it to its source.
///
public sealed record FlowItem
{
public required JsonObject Json { get; init; }
public Dictionary? Binary { get; init; }
public FlowItemOrigin? Origin { get; init; }
/// Convenience factory for a plain JSON item.
public static FlowItem FromJson(JsonObject json) => new() { Json = json };
/// Wraps a bare JSON value in a single "value" field.
public static FlowItem Scalar(JsonNode? value) => new()
{
Json = new JsonObject { ["value"] = value?.DeepClone() },
};
}
/// Helpers for turning sets of items into and out of JSON.
public static class FlowItemJson
{
/// Serializes a batch of items into a JSON array (or null when empty).
public static string? Serialize(IReadOnlyList 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();
}
///
/// Parses a JSON payload into items. Accepts a JSON array of
/// { "json": … } envelopes, a JSON array of bare objects, or a single
/// bare object (which becomes a one-item batch).
///
public static List Parse(string? payload)
{
var items = new List();
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.Empty,
origin["outputIndex"]?.GetValue() ?? 0,
origin["runIndex"]?.GetValue() ?? 0,
origin["itemIndex"]?.GetValue() ?? 0);
}
private static Dictionary? ParseBinary(JsonObject? binary)
{
if (binary == null || binary.Count == 0)
return null;
var result = new Dictionary(StringComparer.Ordinal);
foreach (var (name, value) in binary)
{
if (value is not JsonObject entry)
continue;
result[name] = new BinaryAttachment(
entry["assetId"]?.GetValue() ?? string.Empty,
entry["fileName"]?.GetValue(),
entry["mimeType"]?.GetValue(),
entry["sizeBytes"]?.GetValue());
}
return result;
}
}