diff --git a/Examples/Nodify.Calculator/Execution/ExecutionContext.cs b/Examples/Nodify.Calculator/Execution/ExecutionContext.cs
new file mode 100644
index 0000000..f9b9615
--- /dev/null
+++ b/Examples/Nodify.Calculator/Execution/ExecutionContext.cs
@@ -0,0 +1,34 @@
+using System.Collections.Generic;
+
+namespace Nodify.Calculator.Execution
+{
+ ///
+ /// Shared state + callbacks passed to every .
+ /// Keeps handlers decoupled from the internals.
+ ///
+ public sealed class ExecutionContext
+ {
+ public ExecutionContext(
+ Executor executor,
+ ICollection connections,
+ Dictionary outputs,
+ Dictionary variables)
+ {
+ Executor = executor;
+ Connections = connections;
+ Outputs = outputs;
+ Variables = variables;
+ }
+
+ public Executor Executor { get; }
+ public ICollection Connections { get; }
+ public Dictionary Outputs { get; }
+ public Dictionary Variables { get; }
+
+ public void Log(string message, logType level = logType.Information)
+ => Executor.RaiseLog(message, level);
+
+ public string ReadInput(ConnectorViewModel input)
+ => Executor.TryReadInputValue_Internal(input, Connections);
+ }
+}
diff --git a/Examples/Nodify.Calculator/Execution/Handlers/NodeHandlers.cs b/Examples/Nodify.Calculator/Execution/Handlers/NodeHandlers.cs
new file mode 100644
index 0000000..aa89bba
--- /dev/null
+++ b/Examples/Nodify.Calculator/Execution/Handlers/NodeHandlers.cs
@@ -0,0 +1,373 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Nodify.Calculator.Execution.Handlers
+{
+ // ─────────────────────────────────────────────────────────────────────────────
+ // Sentinel / trivial handlers
+ // ─────────────────────────────────────────────────────────────────────────────
+
+ /// Skips Begin/End sentinel nodes.
+ internal sealed class BeginEndHandler : INodeExecutionHandler
+ {
+ public bool CanHandle(OperationViewModel node, ExecutionContext ctx)
+ => string.Equals(node.Title, "begin", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(node.Title, "end", StringComparison.OrdinalIgnoreCase);
+
+ public void Execute(OperationViewModel node, ExecutionContext ctx)
+ => ctx.Log("Begin or End node found. skipping it");
+ }
+
+ /// Auth node: configuration is resolved before execution, so this is a no-op log.
+ internal sealed class AuthHandler : INodeExecutionHandler
+ {
+ public bool CanHandle(OperationViewModel node, ExecutionContext ctx)
+ => node is AuthOperationViewModel
+ || string.Equals(node.Title, "auth", StringComparison.OrdinalIgnoreCase);
+
+ public void Execute(OperationViewModel node, ExecutionContext ctx)
+ => ctx.Log($"Auth node reached: {node.Title}");
+ }
+
+ /// Knot/Reroute: forwards the single data input to the node's output.
+ internal sealed class KnotHandler : INodeExecutionHandler
+ {
+ public bool CanHandle(OperationViewModel node, ExecutionContext ctx)
+ => node is KnotOperationViewModel;
+
+ public void Execute(OperationViewModel node, ExecutionContext ctx)
+ {
+ var inputCon = ctx.Connections.FirstOrDefault(
+ c => c.Input.Operation == node && c.Input.Shape != ConnectorShape.Triangle);
+ if (inputCon?.Output?.Operation?.NodeId is string srcId
+ && ctx.Outputs.TryGetValue(srcId, out var srcVal))
+ {
+ ctx.Outputs[node.NodeId] = srcVal;
+ }
+ }
+ }
+
+ // ─────────────────────────────────────────────────────────────────────────────
+ // System-op handlers
+ // ─────────────────────────────────────────────────────────────────────────────
+
+ internal sealed class DebugHandler : INodeExecutionHandler
+ {
+ public bool CanHandle(OperationViewModel node, ExecutionContext ctx)
+ => node is SystemOperationViewModel sys && sys.SystemOperationType == SystemOperations.DEBUG;
+
+ public void Execute(OperationViewModel node, ExecutionContext ctx)
+ {
+ var input = node.Input.FirstOrDefault(i => i.Shape != ConnectorShape.Triangle);
+ var hasConnection = input != null && ctx.Connections.Any(c => c.Input == input);
+ var val = ctx.ReadInput(input);
+
+ if (!hasConnection)
+ ctx.Log("[DEBUG] No input connected", logType.Warning);
+ else if (val != null)
+ ctx.Log($"[DEBUG] {val}");
+ else
+ ctx.Log("[DEBUG] (no value)", logType.Warning);
+ }
+ }
+
+ internal sealed class NewObjectHandler : INodeExecutionHandler
+ {
+ public bool CanHandle(OperationViewModel node, ExecutionContext ctx)
+ => node is SystemOperationViewModel sys && sys.SystemOperationType == SystemOperations.NEW_OBJECT;
+
+ public void Execute(OperationViewModel node, ExecutionContext ctx)
+ {
+ ctx.Log($"Constructing new object: {node.Title}");
+ var jObj = new JObject();
+
+ foreach (var inp in node.Input)
+ {
+ if (inp.Shape == ConnectorShape.Triangle || inp.Shape == ConnectorShape.Square) continue;
+
+ var propName = inp.Title ?? "";
+ var parenIdx = propName.IndexOf(" (", StringComparison.Ordinal);
+ if (parenIdx > 0) propName = propName.Substring(0, parenIdx);
+
+ var val = ctx.ReadInput(inp);
+ if (val != null)
+ {
+ try { jObj[propName] = JToken.Parse(val); }
+ catch { jObj[propName] = val; }
+ }
+ else
+ {
+ jObj[propName] = null;
+ }
+ }
+
+ var json = jObj.ToString(Formatting.None);
+ ctx.Outputs[node.NodeId] = json;
+ ctx.Variables[node.NodeId] = json;
+ ctx.Log($"Object constructed: {json}");
+ }
+ }
+
+ internal sealed class AssertHandler : INodeExecutionHandler
+ {
+ public bool CanHandle(OperationViewModel node, ExecutionContext ctx)
+ => node is AssertOperationViewModel;
+
+ public void Execute(OperationViewModel node, ExecutionContext ctx)
+ {
+ ctx.Log($"[ASSERT] Evaluating assertion: {node.Title}");
+ string actualVal = "", expectedVal = "";
+
+ foreach (var inp in node.Input)
+ {
+ if (inp.Shape == ConnectorShape.Triangle) continue;
+ var title = inp.Title?.Trim() ?? "";
+ var val = ctx.ReadInput(inp) ?? "";
+
+ if (title.StartsWith("Actual", StringComparison.OrdinalIgnoreCase)) actualVal = val;
+ else if (title.StartsWith("Expected", StringComparison.OrdinalIgnoreCase)) expectedVal = val;
+ }
+
+ var passed = string.Equals(actualVal.Trim(), expectedVal.Trim(), StringComparison.OrdinalIgnoreCase);
+ ctx.Outputs[node.NodeId] = passed.ToString();
+
+ if (passed)
+ ctx.Log($"[ASSERT] ✅ PASSED — Actual: \"{actualVal}\" == Expected: \"{expectedVal}\"");
+ else
+ ctx.Log($"[ASSERT] ❌ FAILED — Actual: \"{actualVal}\" != Expected: \"{expectedVal}\"", logType.Error);
+ }
+ }
+
+ internal sealed class ForEachHandler : INodeExecutionHandler
+ {
+ public bool CanHandle(OperationViewModel node, ExecutionContext ctx)
+ => node is ForEachOperationViewModel;
+
+ public void Execute(OperationViewModel node, ExecutionContext ctx)
+ {
+ ctx.Log($"[FOREACH] Starting loop: {node.Title}");
+
+ var listInput = node.Input.FirstOrDefault(i => i.Shape != ConnectorShape.Triangle);
+ var listJson = ctx.ReadInput(listInput) ?? "";
+ if (string.IsNullOrEmpty(listJson))
+ {
+ ctx.Log("[FOREACH] No list data found. Skipping loop.", logType.Warning);
+ return;
+ }
+
+ JArray array;
+ try
+ {
+ var token = JToken.Parse(listJson);
+ if (token is JArray arr) array = arr;
+ else
+ {
+ ctx.Log("[FOREACH] Input is not an array. Wrapping as single-element array.", logType.Warning);
+ array = new JArray(token);
+ }
+ }
+ catch
+ {
+ ctx.Log("[FOREACH] Failed to parse input as JSON array.", logType.Error);
+ return;
+ }
+
+ var loopBodyOutput = node.Output.FirstOrDefault(o => o.Title == "Loop Body");
+ var currentItemOutput = node.Output.FirstOrDefault(o => o.Title == "Current Item");
+ var indexOutput = node.Output.FirstOrDefault(o => o.Title == "Index");
+ var loopBodyConnection = loopBodyOutput != null
+ ? ctx.Connections.FirstOrDefault(c => c.Output == loopBodyOutput)
+ : null;
+
+ ctx.Log($"[FOREACH] Iterating over {array.Count} items...");
+ for (int i = 0; i < array.Count; i++)
+ {
+ var itemStr = array[i].ToString(Formatting.None);
+ var preview = itemStr.Length > 80 ? itemStr.Substring(0, 80) + "..." : itemStr;
+ ctx.Log($"[FOREACH] Iteration {i}: {preview}");
+
+ ctx.Outputs[node.NodeId] = itemStr;
+ if (currentItemOutput != null)
+ currentItemOutput.Value = double.TryParse(itemStr, out var dv) ? dv : 0;
+ if (indexOutput != null)
+ indexOutput.Value = i;
+
+ var bodyNode = loopBodyConnection?.Input?.Operation;
+ if (bodyNode != null)
+ ctx.Executor.TraverseChainPublic(bodyNode, "end", ctx.Connections, new HashSet(), true);
+ }
+ ctx.Log($"[FOREACH] Loop completed. {array.Count} iterations executed.");
+ }
+ }
+
+ internal sealed class FunctionHandler : INodeExecutionHandler
+ {
+ public bool CanHandle(OperationViewModel node, ExecutionContext ctx)
+ => node is FunctionOperationViewModel;
+
+ public void Execute(OperationViewModel node, ExecutionContext ctx)
+ {
+ var funcOp = (FunctionOperationViewModel)node;
+ ctx.Log($"Executing function: {funcOp.FunctionName}");
+ ctx.Executor.ExecuteFunctionPublic(funcOp, ctx.Connections);
+ }
+ }
+
+ // ─────────────────────────────────────────────────────────────────────────────
+ // Title-matched handlers (legacy string-based behavior preserved)
+ // ─────────────────────────────────────────────────────────────────────────────
+
+ internal sealed class CreateModelHandler : INodeExecutionHandler
+ {
+ public bool CanHandle(OperationViewModel node, ExecutionContext ctx)
+ => node.Title != null
+ && node.Title.IndexOf("create model", StringComparison.OrdinalIgnoreCase) >= 0;
+
+ public void Execute(OperationViewModel node, ExecutionContext ctx)
+ {
+ var url = node.Title ?? "";
+ var conByTitle = ctx.Connections
+ .Where(c => c.Input.Operation.Title != null
+ && c.Input.Operation.Title.IndexOf("create model", StringComparison.OrdinalIgnoreCase) >= 0)
+ .ToList();
+ var flowConnection = conByTitle.FirstOrDefault(c => c.Input.Shape != ConnectorShape.Triangle);
+ if (flowConnection == null)
+ {
+ ctx.Log("No input found", logType.Error);
+ throw new Exception("Input connection missig for : " + url);
+ }
+
+ if (!ctx.Outputs.TryGetValue(flowConnection.InputNodeId, out var outputValue))
+ return;
+
+ var customModelDir = Path.Combine(ProjectManager.ProjectDirectory, "CustomModels");
+ Directory.CreateDirectory(customModelDir);
+ const string className = "Class1";
+ var classStruct = Executor.GenerateClassFromJson(outputValue, className);
+ File.WriteAllText(Path.Combine(customModelDir, $"{className}.cs"), classStruct);
+
+ ctx.Executor.AddNewModelPublic(new OperationInfoViewModel
+ {
+ Title = className,
+ IsModelNode = true,
+ Type = OperationType.System,
+ sysOp = SystemOperations.GET_SET,
+ ClassName = className,
+ });
+ }
+ }
+
+ ///
+ /// Handles SET nodes — both "SET myVar (type)" simple variables and
+ /// legacy "SET ClassName" parse-JSON variants.
+ ///
+ internal sealed class SetHandler : INodeExecutionHandler
+ {
+ public bool CanHandle(OperationViewModel node, ExecutionContext ctx)
+ => node.Title != null
+ && node.Title.IndexOf("set", StringComparison.OrdinalIgnoreCase) >= 0;
+
+ public void Execute(OperationViewModel node, ExecutionContext ctx)
+ {
+ var url = node.Title ?? "";
+
+ // Simple variable SET: "SET myVar (string)"
+ if (node is SystemOperationViewModel && url.Contains("(") && url.Contains(")"))
+ {
+ var parts = url.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
+ if (parts.Length < 2) return;
+ var varName = parts[1];
+
+ var sourceConn = ctx.Connections.FirstOrDefault(
+ c => c.Input.Operation == node && c.Input.Shape != ConnectorShape.Triangle);
+ if (sourceConn != null)
+ {
+ var sourceNodeId = sourceConn.Output.Operation.NodeId;
+ if (ctx.Outputs.TryGetValue(sourceNodeId, out var sourceVal))
+ {
+ ctx.Variables[varName] = sourceVal;
+ ctx.Log($"Variable '{varName}' SET to: {sourceVal}");
+ }
+ else
+ {
+ ctx.Variables[varName] = sourceConn.Output.Value.ToString();
+ ctx.Log($"Variable '{varName}' SET to connector value: {sourceConn.Output.Value}");
+ }
+ }
+ else
+ {
+ var valueInput = node.Input.FirstOrDefault(i => i.Title == "Value");
+ if (valueInput != null)
+ {
+ ctx.Variables[varName] = valueInput.Value.ToString();
+ ctx.Log($"Variable '{varName}' SET to default: {valueInput.Value}");
+ }
+ }
+ ctx.Outputs[node.NodeId] = ctx.Variables.TryGetValue(varName, out var v)
+ ? v?.ToString() ?? "" : "";
+ return;
+ }
+
+ // Legacy "SET ClassName" parse-json variant
+ var inputConnection = ctx.Connections.FirstOrDefault(
+ c => c.Input.Operation.Title == url && c.Input.Shape != ConnectorShape.Triangle);
+ if (inputConnection == null)
+ {
+ ctx.Log("No input found", logType.Error);
+ throw new Exception("Input connection missig for : " + url);
+ }
+ var outPutNode = inputConnection.Output
+ ?? throw new Exception("Output connection missig for : " + url);
+
+ if (outPutNode.Operation.Title?.IndexOf("parse json", StringComparison.OrdinalIgnoreCase) >= 0)
+ {
+ var inputNodeForParseJson = ctx.Connections.FirstOrDefault(c => c.Input.Operation.Title == outPutNode.Title);
+ if (inputNodeForParseJson == null)
+ {
+ ctx.Log("No input found", logType.Error);
+ throw new Exception("Input connection missig for : " + url);
+ }
+ if (ctx.Outputs.TryGetValue(inputNodeForParseJson.InputNodeId, out var inputNodeOutput)
+ && inputNodeOutput != null)
+ {
+ ctx.Variables[node.NodeId] = JsonConvert.DeserializeObject(inputNodeOutput);
+ }
+ }
+ }
+ }
+
+ // ─────────────────────────────────────────────────────────────────────────────
+ // Default / fallback: API call
+ // ─────────────────────────────────────────────────────────────────────────────
+
+ internal sealed class ApiRequestHandler : INodeExecutionHandler
+ {
+ public bool CanHandle(OperationViewModel node, ExecutionContext ctx) => true; // final fallback
+
+ public void Execute(OperationViewModel node, ExecutionContext ctx)
+ {
+ var url = node.Title ?? "";
+ ctx.Log($"Starting Execution : {url}");
+
+ var httpMethod = (node is APIOperationViewModel apiVm)
+ ? apiVm.OperationType?.ToLower() ?? "get"
+ : "get";
+
+ var res = ctx.Executor.GetResponsePublic(url, httpMethod);
+ if (!string.IsNullOrEmpty(res))
+ {
+ ctx.Outputs[node.NodeId] = res;
+ if (node is APIOperationViewModel apiOp && !string.IsNullOrEmpty(apiOp.ResponseModelClassName))
+ {
+ ctx.Variables[node.NodeId] = res;
+ ctx.Log($"Response auto-parsed as {apiOp.ResponseModelClassName}");
+ }
+ }
+ ctx.Log($"Response Result : {res}");
+ }
+ }
+}
diff --git a/Examples/Nodify.Calculator/Execution/INodeExecutionHandler.cs b/Examples/Nodify.Calculator/Execution/INodeExecutionHandler.cs
new file mode 100644
index 0000000..de38f82
--- /dev/null
+++ b/Examples/Nodify.Calculator/Execution/INodeExecutionHandler.cs
@@ -0,0 +1,13 @@
+namespace Nodify.Calculator.Execution
+{
+ ///
+ /// Strategy contract for executing a single node. The
+ /// owns an ordered list of handlers and dispatches to the first one whose
+ /// returns true.
+ ///
+ public interface INodeExecutionHandler
+ {
+ bool CanHandle(OperationViewModel node, ExecutionContext context);
+ void Execute(OperationViewModel node, ExecutionContext context);
+ }
+}
diff --git a/Examples/Nodify.Calculator/Executor.cs b/Examples/Nodify.Calculator/Executor.cs
index 3ed74d1..6963f99 100644
--- a/Examples/Nodify.Calculator/Executor.cs
+++ b/Examples/Nodify.Calculator/Executor.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
#if NET8_0_OR_GREATER
@@ -14,6 +14,8 @@ using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using System.IO;
using Newtonsoft.Json;
+using Nodify.Calculator.Execution;
+using Nodify.Calculator.Execution.Handlers;
namespace Nodify.Calculator
{
@@ -30,13 +32,35 @@ namespace Nodify.Calculator
private readonly EditorViewModel editorViewModel;
public event LogMe OnLogMe;
- static bool isAlreadyLogged = false;
- static Dictionary outputs = new Dictionary();
- static Dictionary variables = new Dictionary();
+ // Per-execution state (instance, not static prevents cross-run leakage)
+ private bool isAlreadyLogged = false;
+ private readonly Dictionary outputs = new Dictionary();
+ private readonly Dictionary variables = new Dictionary();
+
+ // Single shared HttpClient (avoids socket exhaustion from per-call creation)
+#if NET8_0_OR_GREATER
+ private static readonly HttpClient _httpClient = new HttpClient();
+#endif
// Animation delay between node steps (milliseconds) while executing
private const int AnimationDelayMs = 600;
+ // Ordered dispatch table first match wins. ApiRequestHandler is last (fallback).
+ private readonly List _handlers = new List
+ {
+ new CreateModelHandler(),
+ new SetHandler(),
+ new BeginEndHandler(),
+ new AuthHandler(),
+ new KnotHandler(),
+ new DebugHandler(),
+ new NewObjectHandler(),
+ new AssertHandler(),
+ new ForEachHandler(),
+ new FunctionHandler(),
+ new ApiRequestHandler(), // default / fallback
+ };
+
// Auth configuration populated from the Auth node
private string _authBaseUrl = string.Empty;
private string _authType = string.Empty;
@@ -75,6 +99,8 @@ namespace Nodify.Calculator
OnLogMe?.Invoke("Wish all the best to us :)");
OnLogMe?.Invoke("Perorming chain check, it may take some time depending upon the number of nodes.");
outputs.Clear();
+ variables.Clear();
+ isAlreadyLogged = false;
var calcModel = editorViewModel.Calculator;
var allnodes = calcModel.Operations;
var allConnections = calcModel.Connections;
@@ -133,6 +159,33 @@ namespace Nodify.Calculator
RunOnUI(() => conn.IsActiveInExecution = active);
}
+ // Helpers to reduce duplication across node-type handlers
+ private static bool TitleEquals(OperationViewModel op, string text) =>
+ string.Equals(op?.Title, text, StringComparison.OrdinalIgnoreCase);
+
+ private static bool TitleContains(OperationViewModel op, string text) =>
+ op?.Title != null && op.Title.IndexOf(text, StringComparison.OrdinalIgnoreCase) >= 0;
+
+ ///
+ /// Reads the value feeding the given input connector: upstream node output, then
+ /// connected source's connector value, then falls back to the input's own default value.
+ /// Returns null when nothing is available.
+ ///
+ private string TryReadInputValue(ConnectorViewModel input, ICollection connections)
+ {
+ if (input == null) return null;
+ var inputCon = connections.FirstOrDefault(c => c.Input == input);
+ if (inputCon != null)
+ {
+ var srcNodeId = inputCon.Output?.Operation?.NodeId;
+ if (srcNodeId != null && outputs.TryGetValue(srcNodeId, out var srcVal))
+ return srcVal;
+ if (inputCon.Output?.Value != null)
+ return inputCon.Output.Value.ToString();
+ }
+ return input.Value.ToString();
+ }
+
public static string GenerateClassFromJson(string json, string className)
{
// Parse the JSON to understand its structure
@@ -336,383 +389,40 @@ namespace Nodify.Calculator
private void StartExecution(OperationViewModel op, ICollection connections)
{
- var url = op.Title ?? "";
- if (url.ToLower().Contains("create model"))
+ var ctx = new ExecutionContext(this, connections, outputs, variables);
+
+ // Dispatch to the first handler whose CanHandle returns true.
+ // ApiRequestHandler is the final fallback (its CanHandle always returns true).
+ foreach (var handler in _handlers)
{
- var conByTitle = connections.Where(c => c.Input.Operation.Title.ToLower().Contains("create model")).ToList();
- if (conByTitle.Count <= 0)
- {
- OnLogMe?.Invoke("No input found", logType.Error);
- throw new Exception("Input connection missig for : " + url);
- }
- var flowConnection = conByTitle.Where(c=>c.Input.Shape != ConnectorShape.Triangle).FirstOrDefault();
- if (flowConnection == null)
- {
- OnLogMe?.Invoke("No input found", logType.Error);
- throw new Exception("Input connection missig for : " + url);
- }
+ if (!handler.CanHandle(op, ctx)) continue;
- var outputNodeId = flowConnection.InputNodeId;
-
- string outputValue = string.Empty;
- if (outputs.TryGetValue(outputNodeId, out outputValue))
- {
- //Convert model here
- //CreateModelsFromString(outputValue);
- var customModelDir = Path.Combine(ProjectManager.ProjectDirectory, "CustomModels");
- Directory.CreateDirectory(customModelDir);
- string className = "Class 1";
- className = className.Replace(" ", "");
- var classStruct = GenerateClassFromJson(outputValue, className);
- string flPath = Path.Join(customModelDir, $"{className}.cs");
- File.WriteAllText(flPath, classStruct);
- OperationInfoViewModel opv = new OperationInfoViewModel()
- {
- Title = className,
- IsModelNode = true,
- Type = OperationType.System,
- sysOp = SystemOperations.GET_SET,
- ClassName = className
- };
- this.editorViewModel.Calculator.OperationsMenu.AddNewModel(opv);
- }
- }
-
- if (url.ToLower().Contains("set"))
- {
- // Handle simple variable SET (e.g., "SET myVar (string)")
- if (op is SystemOperationViewModel sysVarOp && url.Contains("(") && url.Contains(")"))
- {
- var parts = url.Split(' ', StringSplitOptions.RemoveEmptyEntries);
- if (parts.Length >= 2)
- {
- var varName = parts[1];
- // Find the data input connection (non-triangle)
- var inputCons = connections.Where(c => c.Input.Operation == op && c.Input.Shape != ConnectorShape.Triangle).ToList();
- if (inputCons.Any())
- {
- var sourceConn = inputCons.First();
- var sourceNodeId = sourceConn.Output.Operation.NodeId;
- if (outputs.TryGetValue(sourceNodeId, out var sourceVal))
- {
- variables[varName] = sourceVal;
- OnLogMe?.Invoke($"Variable '{varName}' SET to: {sourceVal}");
- }
- else
- {
- // Use the connector value directly
- variables[varName] = sourceConn.Output.Value.ToString();
- OnLogMe?.Invoke($"Variable '{varName}' SET to connector value: {sourceConn.Output.Value}");
- }
- }
- else
- {
- // No connection — use default value from the Value input connector
- var valueInput = op.Input.FirstOrDefault(i => i.Title == "Value");
- if (valueInput != null)
- {
- variables[varName] = valueInput.Value.ToString();
- OnLogMe?.Invoke($"Variable '{varName}' SET to default: {valueInput.Value}");
- }
- }
- outputs[op.NodeId] = variables.ContainsKey(varName) ? variables[varName]?.ToString() ?? "" : "";
- }
- return;
- }
-
- var fullTitle = url;
- var classNameToSet = fullTitle.Split(" ", StringSplitOptions.RemoveEmptyEntries)[1];
- var inputConnectionList = connections.Where(c => c.Input.Operation.Title == url).ToList();
- var inputConnection = inputConnectionList.Where(c => c.Input.Shape != ConnectorShape.Triangle).FirstOrDefault();
- if (inputConnection == null)
- {
- OnLogMe?.Invoke("No input found", logType.Error);
- throw new Exception("Input connection missig for : " + url);
- }
- var outPutNode = inputConnection.Output;
- if (outPutNode == null)
- {
- OnLogMe?.Invoke("No output found", logType.Error);
- throw new Exception("Output connection missig for : " + url);
- }
- if (outPutNode.Operation.Title.ToLower().Contains("parse json"))
- {
- var inputNodeForParseJson = connections.Where(c => c.Input.Operation.Title == outPutNode.Title).FirstOrDefault();
- if (inputNodeForParseJson == null)
- {
- OnLogMe?.Invoke("No input found", logType.Error);
- throw new Exception("Input connection missig for : " + url);
- }
- var inputNodeOutput = outputs[inputNodeForParseJson.InputNodeId];
- if (inputNodeOutput != null)
- {
- var obj = JsonConvert.DeserializeObject(inputNodeOutput);
- variables[op.NodeId] = obj;
- }
- }
- }
-
- OnLogMe?.Invoke($"Execution started : {url}");
- if (url.ToLower() == "begin" || url.ToLower() == "end")
- {
- OnLogMe?.Invoke($"Being or End node found. skipping it");
+ OnLogMe?.Invoke($"Execution started : {op.Title}");
+ handler.Execute(op, ctx);
return;
}
- if (url.ToLower() == "auth")
- {
- ResolveAuthNode(editorViewModel.Calculator.Operations);
- OnLogMe?.Invoke($"Auth node resolved. Base URL: {_authBaseUrl}, Auth Type: {_authType}");
- return;
- }
- // Knot/Reroute node: passthrough - forward input data to output
- if (op is KnotOperationViewModel)
- {
- var knotInputCon = connections.Where(c => c.Input.Operation == op && c.Input.Shape != ConnectorShape.Triangle).FirstOrDefault();
- if (knotInputCon != null)
- {
- var srcNodeId = knotInputCon.Output?.Operation?.NodeId;
- if (srcNodeId != null && outputs.TryGetValue(srcNodeId, out var srcVal))
- {
- outputs[op.NodeId] = srcVal;
- }
- }
- return;
- }
- if (op is SystemOperationViewModel debugSysOp && debugSysOp.SystemOperationType == SystemOperations.DEBUG)
- {
- // Find the data input connection (non-triangle)
- var debugInputCons = connections.Where(c => c.Input.Operation == op && c.Input.Shape != ConnectorShape.Triangle).ToList();
- if (debugInputCons.Any())
- {
- var sourceConn = debugInputCons.First();
- var sourceNodeId = sourceConn.Output.Operation.NodeId;
- if (outputs.TryGetValue(sourceNodeId, out var debugVal))
- {
- OnLogMe?.Invoke($"[DEBUG] {debugVal}");
- }
- else if (sourceConn.Output.Value != null)
- {
- OnLogMe?.Invoke($"[DEBUG] {sourceConn.Output.Value}");
- }
- else
- {
- OnLogMe?.Invoke($"[DEBUG] (no value)", logType.Warning);
- }
- }
- else
- {
- OnLogMe?.Invoke($"[DEBUG] No input connected", logType.Warning);
- }
- return;
- }
-
- // Handle New Object nodes — construct JSON from property inputs
- if (op is SystemOperationViewModel newObjSysOp && newObjSysOp.SystemOperationType == SystemOperations.NEW_OBJECT)
- {
- OnLogMe?.Invoke($"Constructing new object: {newObjSysOp.Title}");
- var jObj = new JObject();
- // Iterate property inputs (skip triangle flow + square model connectors)
- foreach (var inp in op.Input)
- {
- if (inp.Shape == ConnectorShape.Triangle || inp.Shape == ConnectorShape.Square) continue;
-
- // Extract property name from title like "Name (string)"
- var propName = inp.Title ?? "";
- var parenIdx = propName.IndexOf(" (");
- if (parenIdx > 0) propName = propName.Substring(0, parenIdx);
-
- // Find connection feeding this input
- var inputCon = connections.FirstOrDefault(cn => cn.Input == inp);
- string val = null;
- if (inputCon != null)
- {
- var srcNodeId = inputCon.Output.Operation.NodeId;
- if (outputs.TryGetValue(srcNodeId, out var srcVal))
- val = srcVal;
- else if (inputCon.Output.Value != null)
- val = inputCon.Output.Value.ToString();
- }
-
- if (val != null)
- {
- // Try to parse as JSON token, fallback to string
- try { jObj[propName] = JToken.Parse(val); }
- catch { jObj[propName] = val; }
- }
- else
- {
- jObj[propName] = null;
- }
- }
-
- var json = jObj.ToString(Formatting.None);
- outputs[op.NodeId] = json;
- variables[op.NodeId] = json;
- OnLogMe?.Invoke($"Object constructed: {json}");
- return;
- }
-
- // Handle Assert nodes
- if (op is AssertOperationViewModel assertSysOp)
- {
- OnLogMe?.Invoke($"[ASSERT] Evaluating assertion: {op.Title}");
- string actualVal = "";
- string expectedVal = "";
-
- // Find "Actual" and "Expected" input values
- foreach (var inp in op.Input)
- {
- if (inp.Shape == ConnectorShape.Triangle) continue;
- var title = inp.Title?.Trim() ?? "";
-
- // Check if there's a connection feeding this input
- var inputCon = connections.FirstOrDefault(c => c.Input == inp);
- string val = "";
- if (inputCon != null)
- {
- var srcNodeId = inputCon.Output.Operation.NodeId;
- if (outputs.TryGetValue(srcNodeId, out var srcVal))
- val = srcVal;
- else if (inputCon.Output.Value != null)
- val = inputCon.Output.Value.ToString();
- }
- else
- {
- val = inp.Value.ToString();
- }
-
- if (title.StartsWith("Actual")) actualVal = val;
- else if (title.StartsWith("Expected")) expectedVal = val;
- }
-
- bool passed = string.Equals(actualVal?.Trim(), expectedVal?.Trim(), StringComparison.OrdinalIgnoreCase);
- outputs[op.NodeId] = passed.ToString();
-
- if (passed)
- {
- OnLogMe?.Invoke($"[ASSERT] ✅ PASSED — Actual: \"{actualVal}\" == Expected: \"{expectedVal}\"");
- }
- else
- {
- OnLogMe?.Invoke($"[ASSERT] ❌ FAILED — Actual: \"{actualVal}\" != Expected: \"{expectedVal}\"", logType.Error);
- }
- return;
- }
-
- // Handle ForEach loop nodes
- if (op is ForEachOperationViewModel forEachSysOp)
- {
- OnLogMe?.Invoke($"[FOREACH] Starting loop: {op.Title}");
-
- // Get the list input value
- string listJson = "";
- var listInput = op.Input.FirstOrDefault(i => i.Shape != ConnectorShape.Triangle);
- if (listInput != null)
- {
- var listCon = connections.FirstOrDefault(c => c.Input == listInput);
- if (listCon != null)
- {
- var srcNodeId = listCon.Output.Operation.NodeId;
- if (outputs.TryGetValue(srcNodeId, out var srcVal))
- listJson = srcVal;
- }
- }
-
- if (string.IsNullOrEmpty(listJson))
- {
- OnLogMe?.Invoke($"[FOREACH] No list data found. Skipping loop.", logType.Warning);
- return;
- }
-
- // Parse the list
- Newtonsoft.Json.Linq.JArray array;
- try
- {
- var token = Newtonsoft.Json.Linq.JToken.Parse(listJson);
- if (token is Newtonsoft.Json.Linq.JArray arr)
- array = arr;
- else
- {
- OnLogMe?.Invoke($"[FOREACH] Input is not an array. Wrapping as single-element array.", logType.Warning);
- array = new Newtonsoft.Json.Linq.JArray(token);
- }
- }
- catch
- {
- OnLogMe?.Invoke($"[FOREACH] Failed to parse input as JSON array.", logType.Error);
- return;
- }
-
- // Find the "Loop Body" flow output connection and "Current Item"/"Index" data outputs
- var loopBodyOutput = op.Output.FirstOrDefault(o => o.Title == "Loop Body");
- var currentItemOutput = op.Output.FirstOrDefault(o => o.Title == "Current Item");
- var indexOutput = op.Output.FirstOrDefault(o => o.Title == "Index");
-
- var loopBodyConnection = loopBodyOutput != null
- ? connections.FirstOrDefault(c => c.Output == loopBodyOutput)
- : null;
-
- OnLogMe?.Invoke($"[FOREACH] Iterating over {array.Count} items...");
-
- for (int i = 0; i < array.Count; i++)
- {
- var item = array[i];
- var itemStr = item.ToString(Newtonsoft.Json.Formatting.None);
-
- OnLogMe?.Invoke($"[FOREACH] Iteration {i}: {(itemStr.Length > 80 ? itemStr.Substring(0, 80) + "..." : itemStr)}");
-
- // Set Current Item and Index output values
- outputs[op.NodeId] = itemStr;
- if (currentItemOutput != null)
- currentItemOutput.Value = double.TryParse(itemStr, out var dv) ? dv : 0;
- if (indexOutput != null)
- indexOutput.Value = i;
-
- // Execute the loop body chain if connected
- if (loopBodyConnection != null)
- {
- var bodyNode = loopBodyConnection.Input?.Operation;
- if (bodyNode != null)
- {
- var visited = new HashSet();
- TraverseChain(bodyNode, "end", connections, visited, true);
- }
- }
- }
-
- OnLogMe?.Invoke($"[FOREACH] Loop completed. {array.Count} iterations executed.");
- return;
- }
-
- // Handle Function nodes — execute the inner flow
- if (op is FunctionOperationViewModel funcOp)
- {
- OnLogMe?.Invoke($"Executing function: {funcOp.FunctionName}");
- ExecuteFunction(funcOp, connections);
- return;
- }
- OnLogMe?.Invoke($"Starting Execution : {url}");
- var httpMethod = (op is APIOperationViewModel apiVm) ? apiVm.OperationType?.ToLower() ?? "get" : "get";
- var res = GetResponse(url, httpMethod);
- if (!string.IsNullOrEmpty(res))
- {
- outputs[op.NodeId] = res;
-
- // Auto-parse response to model if ResponseModelClassName is specified
- if (op is APIOperationViewModel apiOp && !string.IsNullOrEmpty(apiOp.ResponseModelClassName))
- {
- var modelClassName = apiOp.ResponseModelClassName;
- bool isList = modelClassName.StartsWith("List<") && modelClassName.EndsWith(">");
- var innerClassName = isList ? modelClassName.Substring(5, modelClassName.Length - 6) : modelClassName;
-
- // Store the parsed JSON as variable keyed by node ID for downstream SET/Split nodes
- variables[op.NodeId] = res;
- OnLogMe?.Invoke($"Response auto-parsed as {modelClassName}");
- }
- }
- OnLogMe?.Invoke($"Response Result : {res}");
}
+ // -- Bridge methods exposed for handlers (assembly-internal access) --
+ internal void RaiseLog(string message, logType level) => OnLogMe?.Invoke(message, level);
+
+ internal string TryReadInputValue_Internal(ConnectorViewModel input, ICollection connections)
+ => TryReadInputValue(input, connections);
+
+ internal void ExecuteFunctionPublic(FunctionOperationViewModel funcOp, ICollection connections)
+ => ExecuteFunction(funcOp, connections);
+
+ internal string GetResponsePublic(string url, string type) => GetResponse(url, type);
+
+ internal bool TraverseChainPublic(OperationViewModel node, string endNodeTitle,
+ ICollection connections,
+ HashSet visited, bool isExecute)
+ => TraverseChain(node, endNodeTitle, connections, visited, isExecute);
+
+ internal void AddNewModelPublic(OperationInfoViewModel model)
+ => editorViewModel.Calculator.OperationsMenu.AddNewModel(model);
+
+
private void ResolveAuthNode(ICollection allNodes)
{
var authNode = allNodes.OfType().FirstOrDefault();
@@ -747,7 +457,7 @@ namespace Nodify.Calculator
if (!string.IsNullOrWhiteSpace(authNode.AuthType))
_authType = authNode.AuthType;
- OnLogMe?.Invoke($"Auth configured — Base URL: {_authBaseUrl}, Auth Type: {_authType}");
+ OnLogMe?.Invoke($"Auth configured Base URL: {_authBaseUrl}, Auth Type: {_authType}");
}
private void ResolveGetVariableNodes(ICollection allNodes, ICollection connections)
@@ -868,58 +578,62 @@ namespace Nodify.Calculator
string responseString = string.Empty;
#if NET8_0_OR_GREATER
- using (HttpClient client = new HttpClient())
+ if (string.Equals(type, "get", StringComparison.OrdinalIgnoreCase))
{
- client.BaseAddress = new Uri(baseURL);
-
- // Apply authentication headers
- if (!string.IsNullOrWhiteSpace(_authType))
- {
- var authTypeLower = _authType.Trim().ToLower();
- if (authTypeLower.Contains("bearer") && !string.IsNullOrWhiteSpace(_authToken))
- {
- client.DefaultRequestHeaders.Authorization =
- new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _authToken);
- }
- else if (authTypeLower.Contains("basic") &&
- !string.IsNullOrWhiteSpace(_authUsername))
- {
- var credentials = Convert.ToBase64String(
- Encoding.UTF8.GetBytes($"{_authUsername}:{_authPassword}"));
- client.DefaultRequestHeaders.Authorization =
- new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", credentials);
- }
- else if (authTypeLower.Contains("api") && !string.IsNullOrWhiteSpace(_authApiKey))
- {
- client.DefaultRequestHeaders.Add("X-Api-Key", _authApiKey);
- }
- }
-
- if (type == "get")
- {
- HttpResponseMessage response = client.GetAsync(url).Result;
- response.EnsureSuccessStatusCode();
- responseString = response.Content.ReadAsStringAsync().Result;
- }
+ var fullUri = new Uri(new Uri(baseURL.EndsWith("/") ? baseURL : baseURL + "/"), url);
+ using var request = new HttpRequestMessage(HttpMethod.Get, fullUri);
+ ApplyAuthHeaders(request);
+ using var response = _httpClient.Send(request);
+ response.EnsureSuccessStatusCode();
+ using var reader = new StreamReader(response.Content.ReadAsStream());
+ responseString = reader.ReadToEnd();
}
#endif
return responseString;
}
+#if NET8_0_OR_GREATER
+ private void ApplyAuthHeaders(HttpRequestMessage request)
+ {
+ if (string.IsNullOrWhiteSpace(_authType)) return;
+
+ var authTypeLower = _authType.Trim().ToLowerInvariant();
+ if (authTypeLower.Contains("bearer") && !string.IsNullOrWhiteSpace(_authToken))
+ {
+ request.Headers.Authorization =
+ new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _authToken);
+ }
+ else if (authTypeLower.Contains("basic") && !string.IsNullOrWhiteSpace(_authUsername))
+ {
+ var credentials = Convert.ToBase64String(
+ Encoding.UTF8.GetBytes($"{_authUsername}:{_authPassword}"));
+ request.Headers.Authorization =
+ new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", credentials);
+ }
+ else if (authTypeLower.Contains("api") && !string.IsNullOrWhiteSpace(_authApiKey))
+ {
+ request.Headers.Add("X-Api-Key", _authApiKey);
+ }
+ }
+#endif
+
private void PerformChainCheck(ICollection allNodes, ICollection connections, bool isExecute)
{
string startNodeTitle = "begin";
string endNodeTitle = "end";
// Find the starting node
- var startNode = allNodes.FirstOrDefault(node => node.Title?.ToLower() == startNodeTitle);
+ var startNode = allNodes.FirstOrDefault(node => string.Equals(node.Title, startNodeTitle, StringComparison.OrdinalIgnoreCase));
if (startNode == null)
{
OnLogMe?.Invoke("No Begin node found", logType.Error);
throw new Exception("Begin node not found");
}
+ // Reset per-phase broken-chain log latch
+ isAlreadyLogged = false;
+
// Track visited nodes to prevent infinite loops or revisits
HashSet visitedNodes = new HashSet();
@@ -1023,7 +737,7 @@ namespace Nodify.Calculator
private string ValidateRequiredNodes(string nodeName, ICollection allnodes)
{
- var requireNode = allnodes.Where(c => c.Title.ToLower() == nodeName).ToList();
+ var requireNode = allnodes.Where(c => string.Equals(c.Title, nodeName, StringComparison.OrdinalIgnoreCase)).ToList();
if (requireNode.Count > 1)
{
return $"One or more {nodeName} node found. Only one allowed at a time.";