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:
34
Examples/Nodify.Calculator/Execution/ExecutionContext.cs
Normal file
34
Examples/Nodify.Calculator/Execution/ExecutionContext.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Nodify.Calculator.Execution
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Shared state + callbacks passed to every <see cref="INodeExecutionHandler"/>.
|
||||||
|
/// Keeps handlers decoupled from the <see cref="Executor"/> internals.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ExecutionContext
|
||||||
|
{
|
||||||
|
public ExecutionContext(
|
||||||
|
Executor executor,
|
||||||
|
ICollection<ConnectionViewModel> connections,
|
||||||
|
Dictionary<string, string> outputs,
|
||||||
|
Dictionary<string, dynamic> variables)
|
||||||
|
{
|
||||||
|
Executor = executor;
|
||||||
|
Connections = connections;
|
||||||
|
Outputs = outputs;
|
||||||
|
Variables = variables;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Executor Executor { get; }
|
||||||
|
public ICollection<ConnectionViewModel> Connections { get; }
|
||||||
|
public Dictionary<string, string> Outputs { get; }
|
||||||
|
public Dictionary<string, dynamic> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace Nodify.Calculator.Execution
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Strategy contract for executing a single node. The <see cref="Executor"/>
|
||||||
|
/// owns an ordered list of handlers and dispatches to the first one whose
|
||||||
|
/// <see cref="CanHandle"/> returns true.
|
||||||
|
/// </summary>
|
||||||
|
public interface INodeExecutionHandler
|
||||||
|
{
|
||||||
|
bool CanHandle(OperationViewModel node, ExecutionContext context);
|
||||||
|
void Execute(OperationViewModel node, ExecutionContext context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
#if NET8_0_OR_GREATER
|
#if NET8_0_OR_GREATER
|
||||||
@@ -14,6 +14,8 @@ using System.Threading.Tasks;
|
|||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using Nodify.Calculator.Execution;
|
||||||
|
using Nodify.Calculator.Execution.Handlers;
|
||||||
|
|
||||||
namespace Nodify.Calculator
|
namespace Nodify.Calculator
|
||||||
{
|
{
|
||||||
@@ -30,13 +32,35 @@ namespace Nodify.Calculator
|
|||||||
private readonly EditorViewModel editorViewModel;
|
private readonly EditorViewModel editorViewModel;
|
||||||
public event LogMe OnLogMe;
|
public event LogMe OnLogMe;
|
||||||
|
|
||||||
static bool isAlreadyLogged = false;
|
// Per-execution state (instance, not static — prevents cross-run leakage)
|
||||||
static Dictionary<string, string> outputs = new Dictionary<string, string>();
|
private bool isAlreadyLogged = false;
|
||||||
static Dictionary<string, dynamic> variables = new Dictionary<string, dynamic>();
|
private readonly Dictionary<string, string> outputs = new Dictionary<string, string>();
|
||||||
|
private readonly Dictionary<string, dynamic> variables = new Dictionary<string, dynamic>();
|
||||||
|
|
||||||
|
// 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
|
// Animation delay between node steps (milliseconds) while executing
|
||||||
private const int AnimationDelayMs = 600;
|
private const int AnimationDelayMs = 600;
|
||||||
|
|
||||||
|
// Ordered dispatch table — first match wins. ApiRequestHandler is last (fallback).
|
||||||
|
private readonly List<INodeExecutionHandler> _handlers = new List<INodeExecutionHandler>
|
||||||
|
{
|
||||||
|
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
|
// Auth configuration populated from the Auth node
|
||||||
private string _authBaseUrl = string.Empty;
|
private string _authBaseUrl = string.Empty;
|
||||||
private string _authType = string.Empty;
|
private string _authType = string.Empty;
|
||||||
@@ -75,6 +99,8 @@ namespace Nodify.Calculator
|
|||||||
OnLogMe?.Invoke("Wish all the best to us :)");
|
OnLogMe?.Invoke("Wish all the best to us :)");
|
||||||
OnLogMe?.Invoke("Perorming chain check, it may take some time depending upon the number of nodes.");
|
OnLogMe?.Invoke("Perorming chain check, it may take some time depending upon the number of nodes.");
|
||||||
outputs.Clear();
|
outputs.Clear();
|
||||||
|
variables.Clear();
|
||||||
|
isAlreadyLogged = false;
|
||||||
var calcModel = editorViewModel.Calculator;
|
var calcModel = editorViewModel.Calculator;
|
||||||
var allnodes = calcModel.Operations;
|
var allnodes = calcModel.Operations;
|
||||||
var allConnections = calcModel.Connections;
|
var allConnections = calcModel.Connections;
|
||||||
@@ -133,6 +159,33 @@ namespace Nodify.Calculator
|
|||||||
RunOnUI(() => conn.IsActiveInExecution = active);
|
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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
private string TryReadInputValue(ConnectorViewModel input, ICollection<ConnectionViewModel> 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)
|
public static string GenerateClassFromJson(string json, string className)
|
||||||
{
|
{
|
||||||
// Parse the JSON to understand its structure
|
// Parse the JSON to understand its structure
|
||||||
@@ -336,382 +389,39 @@ namespace Nodify.Calculator
|
|||||||
|
|
||||||
private void StartExecution(OperationViewModel op, ICollection<ConnectionViewModel> connections)
|
private void StartExecution(OperationViewModel op, ICollection<ConnectionViewModel> connections)
|
||||||
{
|
{
|
||||||
var url = op.Title ?? "";
|
var ctx = new ExecutionContext(this, connections, outputs, variables);
|
||||||
if (url.ToLower().Contains("create model"))
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
var outputNodeId = flowConnection.InputNodeId;
|
// Dispatch to the first handler whose CanHandle returns true.
|
||||||
|
// ApiRequestHandler is the final fallback (its CanHandle always returns true).
|
||||||
|
foreach (var handler in _handlers)
|
||||||
|
{
|
||||||
|
if (!handler.CanHandle(op, ctx)) continue;
|
||||||
|
|
||||||
string outputValue = string.Empty;
|
OnLogMe?.Invoke($"Execution started : {op.Title}");
|
||||||
if (outputs.TryGetValue(outputNodeId, out outputValue))
|
handler.Execute(op, ctx);
|
||||||
{
|
|
||||||
//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;
|
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}");
|
// -- Bridge methods exposed for handlers (assembly-internal access) --
|
||||||
if (url.ToLower() == "begin" || url.ToLower() == "end")
|
internal void RaiseLog(string message, logType level) => OnLogMe?.Invoke(message, level);
|
||||||
{
|
|
||||||
OnLogMe?.Invoke($"Being or End node found. skipping it");
|
|
||||||
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
|
internal string TryReadInputValue_Internal(ConnectorViewModel input, ICollection<ConnectionViewModel> connections)
|
||||||
if (op is SystemOperationViewModel newObjSysOp && newObjSysOp.SystemOperationType == SystemOperations.NEW_OBJECT)
|
=> TryReadInputValue(input, connections);
|
||||||
{
|
|
||||||
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)"
|
internal void ExecuteFunctionPublic(FunctionOperationViewModel funcOp, ICollection<ConnectionViewModel> connections)
|
||||||
var propName = inp.Title ?? "";
|
=> ExecuteFunction(funcOp, connections);
|
||||||
var parenIdx = propName.IndexOf(" (");
|
|
||||||
if (parenIdx > 0) propName = propName.Substring(0, parenIdx);
|
|
||||||
|
|
||||||
// Find connection feeding this input
|
internal string GetResponsePublic(string url, string type) => GetResponse(url, type);
|
||||||
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)
|
internal bool TraverseChainPublic(OperationViewModel node, string endNodeTitle,
|
||||||
{
|
ICollection<ConnectionViewModel> connections,
|
||||||
// Try to parse as JSON token, fallback to string
|
HashSet<string> visited, bool isExecute)
|
||||||
try { jObj[propName] = JToken.Parse(val); }
|
=> TraverseChain(node, endNodeTitle, connections, visited, isExecute);
|
||||||
catch { jObj[propName] = val; }
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
jObj[propName] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var json = jObj.ToString(Formatting.None);
|
internal void AddNewModelPublic(OperationInfoViewModel model)
|
||||||
outputs[op.NodeId] = json;
|
=> editorViewModel.Calculator.OperationsMenu.AddNewModel(model);
|
||||||
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<string>();
|
|
||||||
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}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ResolveAuthNode(ICollection<OperationViewModel> allNodes)
|
private void ResolveAuthNode(ICollection<OperationViewModel> allNodes)
|
||||||
{
|
{
|
||||||
@@ -747,7 +457,7 @@ namespace Nodify.Calculator
|
|||||||
if (!string.IsNullOrWhiteSpace(authNode.AuthType))
|
if (!string.IsNullOrWhiteSpace(authNode.AuthType))
|
||||||
_authType = 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<OperationViewModel> allNodes, ICollection<ConnectionViewModel> connections)
|
private void ResolveGetVariableNodes(ICollection<OperationViewModel> allNodes, ICollection<ConnectionViewModel> connections)
|
||||||
@@ -868,58 +578,62 @@ namespace Nodify.Calculator
|
|||||||
string responseString = string.Empty;
|
string responseString = string.Empty;
|
||||||
|
|
||||||
#if NET8_0_OR_GREATER
|
#if NET8_0_OR_GREATER
|
||||||
using (HttpClient client = new HttpClient())
|
if (string.Equals(type, "get", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
client.BaseAddress = new Uri(baseURL);
|
var fullUri = new Uri(new Uri(baseURL.EndsWith("/") ? baseURL : baseURL + "/"), url);
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Get, fullUri);
|
||||||
// Apply authentication headers
|
ApplyAuthHeaders(request);
|
||||||
if (!string.IsNullOrWhiteSpace(_authType))
|
using var response = _httpClient.Send(request);
|
||||||
{
|
|
||||||
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();
|
response.EnsureSuccessStatusCode();
|
||||||
responseString = response.Content.ReadAsStringAsync().Result;
|
using var reader = new StreamReader(response.Content.ReadAsStream());
|
||||||
}
|
responseString = reader.ReadToEnd();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
return responseString;
|
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<OperationViewModel> allNodes, ICollection<ConnectionViewModel> connections, bool isExecute)
|
private void PerformChainCheck(ICollection<OperationViewModel> allNodes, ICollection<ConnectionViewModel> connections, bool isExecute)
|
||||||
{
|
{
|
||||||
string startNodeTitle = "begin";
|
string startNodeTitle = "begin";
|
||||||
string endNodeTitle = "end";
|
string endNodeTitle = "end";
|
||||||
|
|
||||||
// Find the starting node
|
// 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)
|
if (startNode == null)
|
||||||
{
|
{
|
||||||
OnLogMe?.Invoke("No Begin node found", logType.Error);
|
OnLogMe?.Invoke("No Begin node found", logType.Error);
|
||||||
throw new Exception("Begin node not found");
|
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
|
// Track visited nodes to prevent infinite loops or revisits
|
||||||
HashSet<string> visitedNodes = new HashSet<string>();
|
HashSet<string> visitedNodes = new HashSet<string>();
|
||||||
|
|
||||||
@@ -1023,7 +737,7 @@ namespace Nodify.Calculator
|
|||||||
|
|
||||||
private string ValidateRequiredNodes(string nodeName, ICollection<OperationViewModel> allnodes)
|
private string ValidateRequiredNodes(string nodeName, ICollection<OperationViewModel> 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)
|
if (requireNode.Count > 1)
|
||||||
{
|
{
|
||||||
return $"One or more {nodeName} node found. Only one allowed at a time.";
|
return $"One or more {nodeName} node found. Only one allowed at a time.";
|
||||||
|
|||||||
Reference in New Issue
Block a user