Implemente parse json node having universal connector
Some checks failed
Build / build (push) Has been cancelled
Some checks failed
Build / build (push) Has been cancelled
This commit is contained in:
@@ -46,6 +46,9 @@ namespace Nodify.Calculator
|
||||
|
||||
// Dynamic Knot node: adapt shape/color to match connected wire
|
||||
if (!IsLoading) HandleKnotNodeConnected(c);
|
||||
|
||||
// Dynamic Parse Json node: adapt input to match source
|
||||
if (!IsLoading) HandleParseJsonNodeConnected(c);
|
||||
})
|
||||
.WhenRemoved(c =>
|
||||
{
|
||||
@@ -81,6 +84,9 @@ namespace Nodify.Calculator
|
||||
|
||||
// Dynamic Knot node: reset on disconnect
|
||||
HandleKnotNodeDisconnected(c);
|
||||
|
||||
// Dynamic Parse Json node: reset input on disconnect
|
||||
HandleParseJsonNodeDisconnected(c);
|
||||
});
|
||||
|
||||
Operations.WhenAdded(x =>
|
||||
@@ -854,6 +860,50 @@ namespace Nodify.Calculator
|
||||
DataType = "object"
|
||||
});
|
||||
}
|
||||
|
||||
private void HandleParseJsonNodeConnected(ConnectionViewModel c)
|
||||
{
|
||||
ConnectorViewModel parseInput = null;
|
||||
ConnectorViewModel sourceConnector = null;
|
||||
|
||||
if (c.Input.IsCopyConnector && c.Input.IsInput && c.Input.Operation is ParseJsonOperationViewModel)
|
||||
{
|
||||
parseInput = c.Input;
|
||||
sourceConnector = c.Output;
|
||||
}
|
||||
else if (c.Output.IsCopyConnector && c.Output.IsInput && c.Output.Operation is ParseJsonOperationViewModel)
|
||||
{
|
||||
parseInput = c.Output;
|
||||
sourceConnector = c.Input;
|
||||
}
|
||||
|
||||
if (parseInput == null || sourceConnector == null) return;
|
||||
|
||||
// Adapt input connector to match the source
|
||||
parseInput.Shape = sourceConnector.Shape;
|
||||
parseInput.ConnectorColor = sourceConnector.RawColor;
|
||||
parseInput.DataType = sourceConnector.DataType;
|
||||
}
|
||||
|
||||
private void HandleParseJsonNodeDisconnected(ConnectionViewModel c)
|
||||
{
|
||||
ConnectorViewModel parseInput = null;
|
||||
|
||||
if (c.Input.IsCopyConnector && c.Input.IsInput && c.Input.Operation is ParseJsonOperationViewModel)
|
||||
parseInput = c.Input;
|
||||
else if (c.Output.IsCopyConnector && c.Output.IsInput && c.Output.Operation is ParseJsonOperationViewModel)
|
||||
parseInput = c.Output;
|
||||
|
||||
if (parseInput == null) return;
|
||||
|
||||
var stillConnected = Connections.Any(con => con.Input == parseInput || con.Output == parseInput);
|
||||
if (stillConnected) return;
|
||||
|
||||
// Reset to universal
|
||||
parseInput.Shape = ConnectorShape.Circle;
|
||||
parseInput.ConnectorColor = System.Drawing.Color.White;
|
||||
parseInput.DataType = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -483,6 +483,25 @@
|
||||
</nodify:Node>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type local:ParseJsonOperationViewModel}">
|
||||
<nodify:Node Header="{Binding Title}"
|
||||
Content="{Binding}"
|
||||
Input="{Binding Input}"
|
||||
Output="{Binding Output}">
|
||||
<nodify:Node.ContentTemplate>
|
||||
<DataTemplate DataType="{x:Type local:ParseJsonOperationViewModel}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Target Type" FontSize="10" Foreground="Gray" Margin="0,0,0,2" />
|
||||
<ComboBox ItemsSource="{Binding AvailableTargetTypes}"
|
||||
SelectedItem="{Binding SelectedTargetType, Mode=TwoWay}"
|
||||
MinWidth="120"
|
||||
Margin="0,0,0,2" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</nodify:Node.ContentTemplate>
|
||||
</nodify:Node>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type local:StringConcatOperationViewModel}">
|
||||
<nodify:Node Header="{Binding Title}"
|
||||
Content="{Binding}"
|
||||
|
||||
@@ -135,18 +135,48 @@ namespace Nodify.Calculator.Execution.Resolvers
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// PARSEJSON: passthrough (output = parsed form of input). For value-reading
|
||||
// purposes we just forward the raw JSON downstream.
|
||||
// PARSEJSON: takes any input, ensures it is valid JSON, then parses it.
|
||||
// If the input is not valid JSON, it serializes it first, then forwards
|
||||
// the JSON string downstream for the selected target type.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
internal sealed class ParseJsonNodeValueResolver : INodeValueResolver
|
||||
{
|
||||
public bool CanResolve(OperationViewModel node, ConnectorViewModel c)
|
||||
=> node is SystemOperationViewModel sys && sys.SystemOperationType == SystemOperations.PARSEJSON;
|
||||
=> node is ParseJsonOperationViewModel
|
||||
|| (node is SystemOperationViewModel sys && sys.SystemOperationType == SystemOperations.PARSEJSON);
|
||||
|
||||
public string Resolve(OperationViewModel node, ConnectorViewModel outputConnector, IValueResolutionContext ctx)
|
||||
{
|
||||
var input = ResolverUtils.FirstDataInput(node);
|
||||
return input == null ? null : ctx.Read(input);
|
||||
if (input == null) return null;
|
||||
|
||||
var raw = ctx.Read(input);
|
||||
if (string.IsNullOrEmpty(raw)) return null;
|
||||
|
||||
// Ensure we have valid JSON — if not, serialize the raw value
|
||||
var trimmed = raw.Trim();
|
||||
bool isJson = (trimmed.StartsWith("{") && trimmed.EndsWith("}"))
|
||||
|| (trimmed.StartsWith("[") && trimmed.EndsWith("]"))
|
||||
|| (trimmed.StartsWith("\"") && trimmed.EndsWith("\""));
|
||||
if (!isJson)
|
||||
{
|
||||
try
|
||||
{
|
||||
raw = Newtonsoft.Json.JsonConvert.SerializeObject(raw);
|
||||
}
|
||||
catch { /* keep raw as-is */ }
|
||||
}
|
||||
|
||||
// Parse and re-serialize to ensure clean JSON output
|
||||
try
|
||||
{
|
||||
var parsed = Newtonsoft.Json.JsonConvert.DeserializeObject(raw);
|
||||
return Newtonsoft.Json.JsonConvert.SerializeObject(parsed);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,9 @@ namespace Nodify.Calculator.Models
|
||||
public string AuthBaseUrl { get; set; } = string.Empty;
|
||||
public string AuthType { get; set; } = string.Empty;
|
||||
|
||||
// Parse Json node properties
|
||||
public string ParseJsonTargetType { get; set; } = string.Empty;
|
||||
|
||||
// Take node properties
|
||||
public int NthIndex { get; set; }
|
||||
public bool IsRandom { get; set; }
|
||||
|
||||
@@ -42,6 +42,7 @@ namespace Nodify.Calculator.NodeHandlers
|
||||
_handlers.Add(new NewObjectHandler());
|
||||
_handlers.Add(new CopyHandler());
|
||||
_handlers.Add(new SplitHandler());
|
||||
_handlers.Add(new ParseJsonHandler());
|
||||
_handlers.Add(new GetSetVariableHandler());
|
||||
_handlers.Add(new GetSetModelHandler());
|
||||
_handlers.Add(new BeginEndHandler());
|
||||
@@ -81,6 +82,7 @@ namespace Nodify.Calculator.NodeHandlers
|
||||
{
|
||||
KnotOperationViewModel => _handlers.OfType<KnotHandler>().FirstOrDefault(),
|
||||
StringConcatOperationViewModel => _handlers.OfType<StringConcatHandler>().FirstOrDefault(),
|
||||
ParseJsonOperationViewModel => _handlers.OfType<ParseJsonHandler>().FirstOrDefault(),
|
||||
FunctionOperationViewModel => _handlers.OfType<FunctionHandler>().FirstOrDefault(),
|
||||
AuthOperationViewModel => _handlers.OfType<AuthHandler>().FirstOrDefault(),
|
||||
TakeOperationViewModel => _handlers.OfType<TakeHandler>().FirstOrDefault(),
|
||||
|
||||
97
Examples/Nodify.Calculator/NodeHandlers/ParseJsonHandler.cs
Normal file
97
Examples/Nodify.Calculator/NodeHandlers/ParseJsonHandler.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using Nodify.Calculator.Models;
|
||||
using System.Drawing;
|
||||
|
||||
namespace Nodify.Calculator.NodeHandlers
|
||||
{
|
||||
public class ParseJsonHandler : INodeHandler
|
||||
{
|
||||
public string NodeTypeKey => "System";
|
||||
|
||||
public bool CanCreate(OperationInfoViewModel info)
|
||||
=> info.Type == OperationType.System && info.sysOp == SystemOperations.PARSEJSON;
|
||||
|
||||
public bool CanRestore(NodeData data)
|
||||
=> data.NodeType == "System" && data.SystemOp == nameof(SystemOperations.PARSEJSON);
|
||||
|
||||
public OperationViewModel Create(OperationInfoViewModel info)
|
||||
{
|
||||
var op = new ParseJsonOperationViewModel();
|
||||
|
||||
// Input: universal connector that accepts any shape (Circle, Square, Grid)
|
||||
op.Input.Add(new ConnectorViewModel
|
||||
{
|
||||
Title = "Input",
|
||||
Shape = ConnectorShape.Circle,
|
||||
ConnectorColor = Color.White,
|
||||
IsCopyConnector = true
|
||||
});
|
||||
|
||||
// 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 ParseJsonOperationViewModel();
|
||||
|
||||
// Input: universal connector
|
||||
op.Input.Add(new ConnectorViewModel
|
||||
{
|
||||
Title = "Input",
|
||||
Shape = ConnectorShape.Circle,
|
||||
ConnectorColor = Color.White,
|
||||
IsCopyConnector = true
|
||||
});
|
||||
|
||||
// Restore adapted input connector from saved data
|
||||
if (data.InputConnectors.Count > 0 && data.InputConnectors[0].Shape != "Circle")
|
||||
{
|
||||
var saved = data.InputConnectors[0];
|
||||
var inp = op.Input[0];
|
||||
inp.Shape = NodeHandlerRegistry.ParseShape(saved.Shape);
|
||||
inp.ConnectorColor = Color.FromArgb(saved.ColorArgb);
|
||||
inp.DataType = saved.DataType;
|
||||
}
|
||||
|
||||
// Restore selected target type (this triggers UpdateOutputConnector via property setter)
|
||||
if (!string.IsNullOrEmpty(data.ParseJsonTargetType))
|
||||
{
|
||||
op.RefreshAvailableTypes();
|
||||
op.SelectedTargetType = data.ParseJsonTargetType;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Default output
|
||||
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.PARSEJSON);
|
||||
if (vm is ParseJsonOperationViewModel parseVm)
|
||||
{
|
||||
data.ParseJsonTargetType = parseVm.SelectedTargetType ?? "object";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
103
Examples/Nodify.Calculator/ParseJsonOperationViewModel.cs
Normal file
103
Examples/Nodify.Calculator/ParseJsonOperationViewModel.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace Nodify.Calculator
|
||||
{
|
||||
public class ParseJsonOperationViewModel : SystemOperationViewModel
|
||||
{
|
||||
private string _selectedTargetType = "object";
|
||||
|
||||
public ObservableCollection<string> AvailableTargetTypes { get; } = new ObservableCollection<string>();
|
||||
|
||||
public string SelectedTargetType
|
||||
{
|
||||
get => _selectedTargetType;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _selectedTargetType, value))
|
||||
{
|
||||
UpdateOutputConnector();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ParseJsonOperationViewModel()
|
||||
{
|
||||
Title = "Parse Json";
|
||||
SystemOperationType = SystemOperations.PARSEJSON;
|
||||
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}>");
|
||||
}
|
||||
}
|
||||
|
||||
// Restore selection if it still exists
|
||||
if (!string.IsNullOrEmpty(current) && AvailableTargetTypes.Contains(current))
|
||||
_selectedTargetType = current;
|
||||
else
|
||||
_selectedTargetType = "object";
|
||||
}
|
||||
|
||||
private void UpdateOutputConnector()
|
||||
{
|
||||
// Remove existing non-flow outputs
|
||||
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 = Color.MediumSpringGreen,
|
||||
DataType = targetType
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// Model type
|
||||
Output.Add(new ConnectorViewModel
|
||||
{
|
||||
Title = targetType,
|
||||
IsInput = false,
|
||||
Shape = ConnectorShape.Square,
|
||||
ConnectorColor = Color.MediumPurple,
|
||||
DataType = targetType
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user