191 lines
6.3 KiB
C#
191 lines
6.3 KiB
C#
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;
|
|
}
|
|
}
|