w4c-workflows-api/Services/Nodes/Executors/NodeValueAccess.cs
2026-09-13 19:28:47 +03:00

145 lines
4.8 KiB
C#

using System.Globalization;
using System.Text.Json.Nodes;
namespace w4c_workflows.Services.Nodes.Executors;
/// <summary>
/// Typed readers over resolved node parameters. YAML scalars arrive as int,
/// long, decimal, double, bool or string, so the readers normalise the shapes
/// and apply a fallback instead of throwing. Shared so every executor reads a
/// bool/number/array the same way.
/// </summary>
internal static class NodeValueAccess
{
public static string? ReadString(JsonObject? parameters, string name)
=> parameters?[name] is JsonValue value && value.TryGetValue<string>(out var text) ? text : null;
public static bool ReadBool(JsonObject? parameters, string name, bool defaultValue = false)
=> parameters?[name] is JsonValue value && value.TryGetValue<bool>(out var flag) ? flag : defaultValue;
public static int ReadInt(JsonObject? parameters, string name, int defaultValue)
=> TryReadNumber(parameters?[name], out var number) ? (int)number : defaultValue;
/// <summary>Reads any JSON number shape (int/long/decimal/double) or a numeric string.</summary>
public static bool TryReadNumber(JsonNode? node, out double value)
{
value = 0;
if (node is not JsonValue jsonValue)
return false;
if (jsonValue.TryGetValue<double>(out var doubleValue))
{
value = doubleValue;
return true;
}
if (jsonValue.TryGetValue<decimal>(out var decimalValue))
{
value = (double)decimalValue;
return true;
}
if (jsonValue.TryGetValue<long>(out var longValue))
{
value = longValue;
return true;
}
if (jsonValue.TryGetValue<int>(out var intValue))
{
value = intValue;
return true;
}
return jsonValue.TryGetValue<string>(out var text)
&& double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out value);
}
/// <summary>Decimal-precision numeric read (numbers, numeric strings).</summary>
public static bool TryReadDecimal(JsonNode? node, out decimal value)
{
value = 0;
if (node is not JsonValue jsonValue)
return false;
if (jsonValue.TryGetValue<decimal>(out var decimalValue))
{
value = decimalValue;
return true;
}
if (jsonValue.TryGetValue<double>(out var doubleValue))
{
value = (decimal)doubleValue;
return true;
}
if (jsonValue.TryGetValue<long>(out var longValue))
{
value = longValue;
return true;
}
return jsonValue.TryGetValue<string>(out var text)
&& decimal.TryParse(text, NumberStyles.Number, CultureInfo.InvariantCulture, out value);
}
/// <summary>Boolean read (JSON booleans only).</summary>
public static bool TryReadBool(JsonNode? node, out bool flag)
{
flag = false;
return node is JsonValue value && value.TryGetValue<bool>(out flag);
}
/// <summary>
/// Renders a scalar node as text: strings verbatim, everything else as JSON
/// (objects/arrays), so callers can compare and interpolate uniformly.
/// </summary>
public static string ScalarText(JsonNode? node)
=> node switch
{
null => string.Empty,
JsonValue value when value.TryGetValue<string>(out var text) => text,
_ => node.ToJsonString(),
};
/// <summary>Shortens text for an error message, keeping the tail marker.</summary>
public static string Truncate(string text, int max = 512)
=> text.Length <= max ? text : text[..max] + "…";
/// <summary>Parses a JSON body, returning null for empty/invalid input instead of throwing.</summary>
public static JsonNode? TryParseJson(string body)
{
if (string.IsNullOrWhiteSpace(body))
return null;
try
{
return JsonNode.Parse(body);
}
catch (Exception)
{
return null;
}
}
/// <summary>
/// Renders a fixed-collection parameter as a list of objects. The
/// interpolator yields an array for the normal case, but a single entry may
/// arrive as a bare object; both are accepted.
/// </summary>
public static IReadOnlyList<JsonObject> ReadEntries(JsonObject parameters, string name)
{
var entries = new List<JsonObject>();
switch (parameters[name])
{
case JsonArray array:
foreach (var element in array)
{
if (element is JsonObject entry)
entries.Add(entry);
}
break;
case JsonObject single:
entries.Add(single);
break;
}
return entries;
}
}