implemented input node resolver and major refactoring over execution function
Some checks failed
Build / build (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled

This commit is contained in:
Ankitkumar Satapara
2026-04-21 01:08:29 +05:30
parent 16a5c591f3
commit 8c04e6534a
4 changed files with 334 additions and 9 deletions

View File

@@ -0,0 +1,178 @@
using System;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Nodify.Calculator.Execution.Resolvers
{
/// <summary>Utilities shared by resolvers.</summary>
internal static class ResolverUtils
{
/// <summary>Strip " (type)" suffix from a connector title. "Id (int)" → "Id".</summary>
public static string NormalizeConnectorName(string title)
{
if (string.IsNullOrEmpty(title)) return string.Empty;
var parenIdx = title.IndexOf(" (", StringComparison.Ordinal);
return parenIdx > 0 ? title.Substring(0, parenIdx).Trim() : title.Trim();
}
/// <summary>Pick a named property from a JSON string (case-insensitive). Returns null if not parseable or not an object.</summary>
public static string ExtractJsonProperty(string json, string propertyName)
{
if (string.IsNullOrWhiteSpace(json) || string.IsNullOrEmpty(propertyName)) return null;
try
{
var token = JToken.Parse(json);
if (token is JObject obj)
{
var prop = obj.Properties()
.FirstOrDefault(p => string.Equals(p.Name, propertyName, StringComparison.OrdinalIgnoreCase));
if (prop != null)
{
return prop.Value.Type == JTokenType.String
? prop.Value.ToString()
: prop.Value.ToString(Formatting.None);
}
}
}
catch { /* not JSON */ }
return null;
}
/// <summary>Find the single non-flow (non-Triangle) input connector.</summary>
public static ConnectorViewModel FirstDataInput(OperationViewModel node)
=> node.Input.FirstOrDefault(i => i.Shape != ConnectorShape.Triangle);
}
// ─────────────────────────────────────────────────────────────────────────────
// SPLIT: expose properties of an incoming JSON object as individual outputs.
// ─────────────────────────────────────────────────────────────────────────────
internal sealed class SplitNodeValueResolver : INodeValueResolver
{
public bool CanResolve(OperationViewModel node, ConnectorViewModel c)
=> node is SystemOperationViewModel sys && sys.SystemOperationType == SystemOperations.SPLIT;
public string Resolve(OperationViewModel node, ConnectorViewModel outputConnector, IValueResolutionContext ctx)
{
// Split's model input is the Square-shaped connector
var modelInput = node.Input.FirstOrDefault(i => i.Shape == ConnectorShape.Square);
if (modelInput == null) return null;
var modelJson = ctx.Read(modelInput);
if (string.IsNullOrWhiteSpace(modelJson)) return null;
var propName = ResolverUtils.NormalizeConnectorName(outputConnector.Title);
return ResolverUtils.ExtractJsonProperty(modelJson, propName);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// COPY: N identical outputs = whatever feeds the single input.
// ─────────────────────────────────────────────────────────────────────────────
internal sealed class CopyNodeValueResolver : INodeValueResolver
{
public bool CanResolve(OperationViewModel node, ConnectorViewModel c)
=> node is SystemOperationViewModel sys && sys.SystemOperationType == SystemOperations.COPY;
public string Resolve(OperationViewModel node, ConnectorViewModel outputConnector, IValueResolutionContext ctx)
{
var input = ResolverUtils.FirstDataInput(node);
return input == null ? null : ctx.Read(input);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// KNOT / reroute: passthrough of the single data input.
// ─────────────────────────────────────────────────────────────────────────────
internal sealed class KnotNodeValueResolver : INodeValueResolver
{
public bool CanResolve(OperationViewModel node, ConnectorViewModel c)
=> node is KnotOperationViewModel;
public string Resolve(OperationViewModel node, ConnectorViewModel outputConnector, IValueResolutionContext ctx)
{
var input = ResolverUtils.FirstDataInput(node);
return input == null ? null : ctx.Read(input);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// TAKE: pick the Nth (or random) element from an incoming JSON array.
// ─────────────────────────────────────────────────────────────────────────────
internal sealed class TakeNodeValueResolver : INodeValueResolver
{
public bool CanResolve(OperationViewModel node, ConnectorViewModel c)
=> node is TakeOperationViewModel;
public string Resolve(OperationViewModel node, ConnectorViewModel outputConnector, IValueResolutionContext ctx)
{
var takeOp = (TakeOperationViewModel)node;
var listInput = node.Input.FirstOrDefault(i => i.IsTakeListConnector)
?? ResolverUtils.FirstDataInput(node);
if (listInput == null) return null;
var json = ctx.Read(listInput);
if (string.IsNullOrWhiteSpace(json)) return null;
try
{
var token = JToken.Parse(json);
if (token is JArray arr && arr.Count > 0)
{
int idx = takeOp.IsRandom
? new Random().Next(arr.Count)
: Math.Max(0, Math.Min(takeOp.NthIndex, arr.Count - 1));
var item = arr[idx];
return item.Type == JTokenType.String ? item.ToString() : item.ToString(Formatting.None);
}
}
catch (Exception ex)
{
ctx.Log($"[TAKE] Failed to parse list JSON: {ex.Message}", logType.Warning);
}
return null;
}
}
// ─────────────────────────────────────────────────────────────────────────────
// PARSEJSON: passthrough (output = parsed form of input). For value-reading
// purposes we just forward the raw JSON downstream.
// ─────────────────────────────────────────────────────────────────────────────
internal sealed class ParseJsonNodeValueResolver : INodeValueResolver
{
public bool CanResolve(OperationViewModel node, ConnectorViewModel c)
=> node is SystemOperationViewModel sys && sys.SystemOperationType == SystemOperations.PARSEJSON;
public string Resolve(OperationViewModel node, ConnectorViewModel outputConnector, IValueResolutionContext ctx)
{
var input = ResolverUtils.FirstDataInput(node);
return input == null ? null : ctx.Read(input);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// GET variable (GET_SET non-flow node): resolve from the variables dictionary.
// Titles look like "GET myVar (string)" or "GET ClassName".
// ─────────────────────────────────────────────────────────────────────────────
internal sealed class GetVariableNodeValueResolver : INodeValueResolver
{
public bool CanResolve(OperationViewModel node, ConnectorViewModel c)
{
if (!(node is SystemOperationViewModel sys) || sys.SystemOperationType != SystemOperations.GET_SET)
return false;
var t = node.Title ?? string.Empty;
return t.StartsWith("GET ", StringComparison.OrdinalIgnoreCase);
}
public string Resolve(OperationViewModel node, ConnectorViewModel outputConnector, IValueResolutionContext ctx)
{
var parts = (node.Title ?? string.Empty)
.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 2) return null;
var varName = parts[1];
if (ctx.Variables.TryGetValue(varName, out var val))
return val?.ToString();
return null;
}
}
}