diff --git a/Examples/Nodify.Calculator/DeserializeOperationViewModel.cs b/Examples/Nodify.Calculator/DeserializeOperationViewModel.cs new file mode 100644 index 0000000..7b6caf0 --- /dev/null +++ b/Examples/Nodify.Calculator/DeserializeOperationViewModel.cs @@ -0,0 +1,100 @@ +using System.Collections.ObjectModel; +using System.Drawing; +using System.IO; +using System.Linq; + +namespace Nodify.Calculator +{ + public class DeserializeOperationViewModel : SystemOperationViewModel + { + private string _selectedTargetType = "object"; + + public ObservableCollection AvailableTargetTypes { get; } = new ObservableCollection(); + + public string SelectedTargetType + { + get => _selectedTargetType; + set + { + if (SetProperty(ref _selectedTargetType, value)) + { + UpdateOutputConnector(); + } + } + } + + public DeserializeOperationViewModel() + { + Title = "Deserialize"; + SystemOperationType = SystemOperations.DESERIALIZE; + RefreshAvailableTypes(); + } + + public void RefreshAvailableTypes() + { + var current = _selectedTargetType; + AvailableTargetTypes.Clear(); + AvailableTargetTypes.Add("object"); + + var customModelDir = Path.Combine(ProjectManager.ProjectDirectory, "CustomModels"); + if (Directory.Exists(customModelDir)) + { + var modelFiles = Directory.GetFiles(customModelDir, "*.cs"); + foreach (var file in modelFiles) + { + var name = Path.GetFileNameWithoutExtension(file); + AvailableTargetTypes.Add(name); + AvailableTargetTypes.Add($"List<{name}>"); + } + } + + if (!string.IsNullOrEmpty(current) && AvailableTargetTypes.Contains(current)) + _selectedTargetType = current; + else + _selectedTargetType = "object"; + } + + private void UpdateOutputConnector() + { + var toRemove = Output.Where(o => o.Shape != ConnectorShape.Triangle).ToList(); + foreach (var o in toRemove) + Output.Remove(o); + + var targetType = _selectedTargetType ?? "object"; + + if (targetType == "object") + { + Output.Add(new ConnectorViewModel + { + Title = "Result", + IsInput = false, + Shape = ConnectorShape.Circle, + ConnectorColor = Color.LimeGreen, + DataType = "object" + }); + } + else if (targetType.StartsWith("List<") && targetType.EndsWith(">")) + { + Output.Add(new ConnectorViewModel + { + Title = targetType, + IsInput = false, + Shape = ConnectorShape.Grid, + ConnectorColor = NodeColors.List, + DataType = targetType + }); + } + else + { + Output.Add(new ConnectorViewModel + { + Title = targetType, + IsInput = false, + Shape = ConnectorShape.Square, + ConnectorColor = NodeColors.Model, + DataType = targetType + }); + } + } + } +} diff --git a/Examples/Nodify.Calculator/EditorView.xaml b/Examples/Nodify.Calculator/EditorView.xaml index d1a833c..c40acde 100644 --- a/Examples/Nodify.Calculator/EditorView.xaml +++ b/Examples/Nodify.Calculator/EditorView.xaml @@ -502,6 +502,25 @@ + + + + + + + + + + + + + node is DeserializeOperationViewModel; + + public void Execute(OperationViewModel node, ExecutionContext ctx) + { + var desVm = (DeserializeOperationViewModel)node; + var targetType = desVm.SelectedTargetType ?? "object"; + + // Read the Json input (first non-Triangle input) + var jsonInput = node.Input.FirstOrDefault(i => i.Shape != ConnectorShape.Triangle); + var raw = ctx.ReadInput(jsonInput) ?? ""; + + ctx.Log($"[DESERIALIZE] Input: {(raw.Length > 100 ? raw.Substring(0, 100) + "..." : raw)}"); + ctx.Log($"[DESERIALIZE] Target type: {targetType}"); + + if (string.IsNullOrWhiteSpace(raw)) + { + ctx.Log("[DESERIALIZE] ERROR: No JSON input provided.", logType.Error); + return; + } + + // Ensure valid JSON + var trimmed = raw.Trim(); + bool isJson = (trimmed.StartsWith("{") && trimmed.EndsWith("}")) + || (trimmed.StartsWith("[") && trimmed.EndsWith("]")) + || (trimmed.StartsWith("\"") && trimmed.EndsWith("\"")); + if (!isJson) + { + try { raw = JsonConvert.SerializeObject(raw); } + catch { /* keep raw */ } + } + + try + { + var token = JToken.Parse(raw); + + if (targetType == "object") + { + var result = token.ToString(Formatting.None); + ctx.Outputs[node.NodeId] = result; + ctx.Log($"[DESERIALIZE] Output (object): {(result.Length > 100 ? result.Substring(0, 100) + "..." : result)}"); + return; + } + + if (targetType.StartsWith("List<") && targetType.EndsWith(">")) + { + var modelName = targetType.Substring(5, targetType.Length - 6); + var modelProps = LoadModelProperties(modelName, ctx); + var array = FindArray(token); + if (array == null) + { + ctx.Log($"[DESERIALIZE] ERROR: Could not find an array to convert to {targetType}.", logType.Error); + return; + } + + if (modelProps != null && modelProps.Length > 0) + { + var projected = new JArray(); + foreach (var item in array) + { + var proj = ProjectToModel(item, modelProps); + if (proj != null) projected.Add(proj); + } + var result = projected.ToString(Formatting.None); + ctx.Outputs[node.NodeId] = result; + ctx.Log($"[DESERIALIZE] Deserialized {projected.Count} items as {targetType}."); + } + else + { + var result = array.ToString(Formatting.None); + ctx.Outputs[node.NodeId] = result; + ctx.Log($"[DESERIALIZE] Model \"{modelName}\" not found, returning raw array ({array.Count} items)."); + } + return; + } + + // Single model + { + var modelProps = LoadModelProperties(targetType, ctx); + var obj = FindObject(token); + if (obj == null) + { + ctx.Log($"[DESERIALIZE] ERROR: Could not find an object to convert to {targetType}.", logType.Error); + return; + } + + if (modelProps != null && modelProps.Length > 0) + { + var projected = ProjectToModel(obj, modelProps); + if (projected == null) + { + ctx.Log($"[DESERIALIZE] ERROR: Failed to project to {targetType}.", logType.Error); + return; + } + var result = projected.ToString(Formatting.None); + ctx.Outputs[node.NodeId] = result; + ctx.Log($"[DESERIALIZE] Deserialized as {targetType} with {projected.Count} properties."); + } + else + { + var result = obj.ToString(Formatting.None); + ctx.Outputs[node.NodeId] = result; + ctx.Log($"[DESERIALIZE] Model \"{targetType}\" not found, returning raw object."); + } + } + } + catch (Exception ex) + { + ctx.Log($"[DESERIALIZE] ERROR: {ex.Message}", logType.Error); + } + } + + private static string[] LoadModelProperties(string modelName, ExecutionContext ctx) + { + var modelPath = Path.Combine(ProjectManager.ProjectDirectory, "CustomModels", modelName + ".cs"); + if (!File.Exists(modelPath)) return null; + + var lines = File.ReadAllLines(modelPath); + var props = lines + .Select(l => l.Trim()) + .Where(l => l.StartsWith("public ") && l.Contains("{ get;")) + .Select(l => + { + var parts = l.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + return parts.Length >= 3 ? parts[2] : null; + }) + .Where(p => p != null) + .ToArray(); + + ctx.Log($"[DESERIALIZE] Loaded model \"{modelName}\": {string.Join(", ", props)}"); + return props; + } + + private static JObject ProjectToModel(JToken token, string[] modelProps) + { + var source = token as JObject; + if (source == null) return null; + + if (source.Count == 1) + { + var first = source.Properties().First(); + if (first.Value is JObject inner) + source = inner; + } + + var result = new JObject(); + foreach (var propName in modelProps) + { + var match = source.Properties() + .FirstOrDefault(p => string.Equals(p.Name, propName, StringComparison.OrdinalIgnoreCase)); + result[propName] = match != null ? match.Value.DeepClone() : JValue.CreateNull(); + } + return result; + } + + private static JArray FindArray(JToken token) + { + if (token is JArray arr) return arr; + if (token is JObject obj) + { + foreach (var prop in obj.Properties()) + if (prop.Value is JArray innerArr) return innerArr; + foreach (var prop in obj.Properties()) + if (prop.Value is JObject inner) + foreach (var innerProp in inner.Properties()) + if (innerProp.Value is JArray deepArr) return deepArr; + } + return null; + } + + private static JObject FindObject(JToken token) + { + if (token is JObject obj) + { + var props = obj.Properties().ToList(); + if (props.Count == 1 && props[0].Value is JObject inner) + return inner; + return obj; + } + return null; + } + } + internal sealed class ForEachHandler : INodeExecutionHandler { public bool CanHandle(OperationViewModel node, ExecutionContext ctx) diff --git a/Examples/Nodify.Calculator/Executor.cs b/Examples/Nodify.Calculator/Executor.cs index c68a3d0..4111544 100644 --- a/Examples/Nodify.Calculator/Executor.cs +++ b/Examples/Nodify.Calculator/Executor.cs @@ -58,6 +58,7 @@ namespace Nodify.Calculator new NewObjectHandler(), new AssertHandler(), new ForEachHandler(), + new Execution.Handlers.DeserializeHandler(), new FunctionHandler(), new ListAddExecutionHandler(), new ListRemoveExecutionHandler(), diff --git a/Examples/Nodify.Calculator/NodeHandlers/DeserializeHandler.cs b/Examples/Nodify.Calculator/NodeHandlers/DeserializeHandler.cs new file mode 100644 index 0000000..8b105c6 --- /dev/null +++ b/Examples/Nodify.Calculator/NodeHandlers/DeserializeHandler.cs @@ -0,0 +1,130 @@ +using Nodify.Calculator.Models; +using System.Drawing; + +namespace Nodify.Calculator.NodeHandlers +{ + public class DeserializeHandler : INodeHandler + { + public string NodeTypeKey => "System"; + + public bool CanCreate(OperationInfoViewModel info) + => info.Type == OperationType.System && info.sysOp == SystemOperations.DESERIALIZE; + + public bool CanRestore(NodeData data) + => data.NodeType == "System" && data.SystemOp == nameof(SystemOperations.DESERIALIZE); + + public OperationViewModel Create(OperationInfoViewModel info) + { + var op = new DeserializeOperationViewModel(); + + // Flow in + op.Input.Add(new ConnectorViewModel + { + Title = "", + Shape = ConnectorShape.Triangle, + ConnectorColor = NodeColors.Exec + }); + + // String input (accepts any data connection) + op.Input.Add(new ConnectorViewModel + { + Title = "Json", + Shape = ConnectorShape.Circle, + ConnectorColor = NodeColors.String, + IsCopyConnector = true + }); + + // Flow out + op.Output.Add(new ConnectorViewModel + { + Title = "", + IsInput = false, + Shape = ConnectorShape.Triangle, + ConnectorColor = NodeColors.Exec + }); + + // Default output (will be updated when user selects target type) + op.Output.Add(new ConnectorViewModel + { + Title = "Result", + IsInput = false, + Shape = ConnectorShape.Circle, + ConnectorColor = Color.LimeGreen, + DataType = "object" + }); + + return op; + } + + public OperationViewModel Restore(NodeData data) + { + var op = new DeserializeOperationViewModel(); + + // Flow in + op.Input.Add(new ConnectorViewModel + { + Title = "", + Shape = ConnectorShape.Triangle, + ConnectorColor = NodeColors.Exec + }); + + // String input + op.Input.Add(new ConnectorViewModel + { + Title = "Json", + Shape = ConnectorShape.Circle, + ConnectorColor = NodeColors.String, + IsCopyConnector = true + }); + + // Restore adapted input connector from saved data + if (data.InputConnectors.Count > 1 && data.InputConnectors[1].Shape != "Circle") + { + var saved = data.InputConnectors[1]; + var inp = op.Input[1]; // the Json input (index 1, after flow) + inp.Shape = NodeHandlerRegistry.ParseShape(saved.Shape); + inp.ConnectorColor = Color.FromArgb(saved.ColorArgb); + inp.DataType = saved.DataType; + } + + // Flow out + op.Output.Add(new ConnectorViewModel + { + Title = "", + IsInput = false, + Shape = ConnectorShape.Triangle, + ConnectorColor = NodeColors.Exec + }); + + // Restore selected target type + if (!string.IsNullOrEmpty(data.ParseJsonTargetType)) + { + op.RefreshAvailableTypes(); + op.SelectedTargetType = data.ParseJsonTargetType; + } + else + { + op.Output.Add(new ConnectorViewModel + { + Title = "Result", + IsInput = false, + Shape = ConnectorShape.Circle, + ConnectorColor = Color.LimeGreen, + DataType = "object" + }); + } + + return op; + } + + public void Save(OperationViewModel vm, NodeData data) + { + data.NodeType = "System"; + data.SystemOp = nameof(SystemOperations.DESERIALIZE); + if (vm is DeserializeOperationViewModel desVm) + { + data.ParseJsonTargetType = desVm.SelectedTargetType ?? "object"; + } + } + } +} diff --git a/Examples/Nodify.Calculator/NodeHandlers/NodeHandlerRegistry.cs b/Examples/Nodify.Calculator/NodeHandlers/NodeHandlerRegistry.cs index 7279242..129c7c8 100644 --- a/Examples/Nodify.Calculator/NodeHandlers/NodeHandlerRegistry.cs +++ b/Examples/Nodify.Calculator/NodeHandlers/NodeHandlerRegistry.cs @@ -43,6 +43,7 @@ namespace Nodify.Calculator.NodeHandlers _handlers.Add(new CopyHandler()); _handlers.Add(new SplitHandler()); _handlers.Add(new ParseJsonHandler()); + _handlers.Add(new DeserializeHandler()); _handlers.Add(new ListAddHandler()); _handlers.Add(new ListRemoveHandler()); _handlers.Add(new ListUpdateHandler()); @@ -90,6 +91,7 @@ namespace Nodify.Calculator.NodeHandlers KnotOperationViewModel => _handlers.OfType().FirstOrDefault(), StringConcatOperationViewModel => _handlers.OfType().FirstOrDefault(), ParseJsonOperationViewModel => _handlers.OfType().FirstOrDefault(), + DeserializeOperationViewModel => _handlers.OfType().FirstOrDefault(), FunctionOperationViewModel => _handlers.OfType().FirstOrDefault(), AuthOperationViewModel => _handlers.OfType().FirstOrDefault(), TakeOperationViewModel => _handlers.OfType().FirstOrDefault(), diff --git a/Examples/Nodify.Calculator/Operations/OperationFactory.cs b/Examples/Nodify.Calculator/Operations/OperationFactory.cs index e65fae1..390cc50 100644 --- a/Examples/Nodify.Calculator/Operations/OperationFactory.cs +++ b/Examples/Nodify.Calculator/Operations/OperationFactory.cs @@ -144,6 +144,16 @@ namespace Nodify.Calculator IsFlowNode = false }; + var deserializeNode = new OperationInfoViewModel() + { + Title = "Deserialize", + Type = OperationType.System, + sysOp = SystemOperations.DESERIALIZE, + IsFlowNode = true + }; + deserializeNode.Input.Add("Json"); + deserializeNode.Output.Add(""); + systemNodes.Add(authNode); systemNodes.Add(copynode); systemNodes.Add(debugNode); @@ -155,6 +165,7 @@ namespace Nodify.Calculator systemNodes.Add(splitNode); systemNodes.Add(assertNode); systemNodes.Add(stringConcatNode); + systemNodes.Add(deserializeNode); return systemNodes; } diff --git a/Examples/Nodify.Calculator/SystemOperationViewModel.cs b/Examples/Nodify.Calculator/SystemOperationViewModel.cs index 0d66824..4f66585 100644 --- a/Examples/Nodify.Calculator/SystemOperationViewModel.cs +++ b/Examples/Nodify.Calculator/SystemOperationViewModel.cs @@ -31,7 +31,8 @@ namespace Nodify.Calculator LIST_COUNT, LIST_CONTAINS, LIST_CLEAR, - LIST_GET + LIST_GET, + DESERIALIZE } public class SystemOperationViewModel : OperationViewModel