Added optimization in the execution context not it is handler driven approach, event driven approach added
Some checks failed
Build / build (push) Has been cancelled
Some checks failed
Build / build (push) Has been cancelled
This commit is contained in:
373
Examples/Nodify.Calculator/Execution/Handlers/NodeHandlers.cs
Normal file
373
Examples/Nodify.Calculator/Execution/Handlers/NodeHandlers.cs
Normal file
@@ -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
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Skips Begin/End sentinel nodes. </summary>
|
||||
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");
|
||||
}
|
||||
|
||||
/// <summary>Auth node: configuration is resolved before execution, so this is a no-op log.</summary>
|
||||
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}");
|
||||
}
|
||||
|
||||
/// <summary>Knot/Reroute: forwards the single data input to the node's output.</summary>
|
||||
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<string>(), 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles SET nodes — both "SET myVar (type)" simple variables and
|
||||
/// legacy "SET ClassName" parse-JSON variants.
|
||||
/// </summary>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user