diff --git a/Examples/Nodify.Calculator/APIOperationViewModel.cs b/Examples/Nodify.Calculator/APIOperationViewModel.cs index 00d9943..6d25745 100644 --- a/Examples/Nodify.Calculator/APIOperationViewModel.cs +++ b/Examples/Nodify.Calculator/APIOperationViewModel.cs @@ -20,5 +20,12 @@ namespace Nodify.Calculator get => _operationType; set => SetProperty(ref _operationType, value); } + + private string _responseModelClassName = string.Empty; + public string ResponseModelClassName + { + get => _responseModelClassName; + set => SetProperty(ref _responseModelClassName, value); + } } } diff --git a/Examples/Nodify.Calculator/CalculatorViewModel.cs b/Examples/Nodify.Calculator/CalculatorViewModel.cs index 99f9efa..7254cf0 100644 --- a/Examples/Nodify.Calculator/CalculatorViewModel.cs +++ b/Examples/Nodify.Calculator/CalculatorViewModel.cs @@ -245,7 +245,7 @@ namespace Nodify.Calculator var sourceConnector = (c.Input == splitInput) ? c.Output : c.Input; var sourceOp = sourceConnector.Operation; - // Determine class name from the source operation title (e.g. "GET ClassName" or "SET ClassName") + // Determine class name from the source operation or connector string className = null; if (sourceOp is SystemOperationViewModel srcSys) { @@ -253,6 +253,25 @@ namespace Nodify.Calculator if (title.StartsWith("GET ")) className = title.Substring(4).Trim(); else if (title.StartsWith("SET ")) className = title.Split(' ').ElementAtOrDefault(1); } + else if (sourceOp is APIOperationViewModel apiVm) + { + // API node: get response model class name + className = apiVm.ResponseModelClassName; + // Strip List<> wrapper if present + if (!string.IsNullOrEmpty(className) && className.StartsWith("List<") && className.EndsWith(">")) + className = className.Substring(5, className.Length - 6); + } + + // Fallback: try to get class name from the source connector's DataType + if (string.IsNullOrEmpty(className) && !string.IsNullOrEmpty(sourceConnector.DataType)) + { + var dt = sourceConnector.DataType; + if (dt.StartsWith("List<") && dt.EndsWith(">")) + dt = dt.Substring(5, dt.Length - 6); + // If it's not a primitive type, treat as class name + if (!new[] { "string", "int", "double", "bool", "object" }.Contains(dt.ToLower())) + className = dt; + } if (string.IsNullOrEmpty(className)) return; diff --git a/Examples/Nodify.Calculator/Executor.cs b/Examples/Nodify.Calculator/Executor.cs index 9d3ec6e..512bd3b 100644 --- a/Examples/Nodify.Calculator/Executor.cs +++ b/Examples/Nodify.Calculator/Executor.cs @@ -437,10 +437,23 @@ namespace Nodify.Calculator return; } OnLogMe?.Invoke($"Starting Execution : {url}"); - var res = GetResponse(url, "get"); + var httpMethod = (op is APIOperationViewModel apiVm) ? apiVm.OperationType?.ToLower() ?? "get" : "get"; + var res = GetResponse(url, httpMethod); if (!string.IsNullOrEmpty(res)) { - outputs.Add(op.NodeId, 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}"); } diff --git a/Examples/Nodify.Calculator/Models/SwaggerNodeModel.cs b/Examples/Nodify.Calculator/Models/SwaggerNodeModel.cs index e9001cc..dfb969b 100644 --- a/Examples/Nodify.Calculator/Models/SwaggerNodeModel.cs +++ b/Examples/Nodify.Calculator/Models/SwaggerNodeModel.cs @@ -9,5 +9,6 @@ namespace Nodify.Calculator.Models public string OPType { get; set; } = string.Empty; public List InputNames { get; set; } = new List(); public string SwaggerFileName { get; set; } = string.Empty; + public string ResponseModelClassName { get; set; } = string.Empty; } } diff --git a/Examples/Nodify.Calculator/OperationInfoViewModel.cs b/Examples/Nodify.Calculator/OperationInfoViewModel.cs index e038130..50b0a25 100644 --- a/Examples/Nodify.Calculator/OperationInfoViewModel.cs +++ b/Examples/Nodify.Calculator/OperationInfoViewModel.cs @@ -35,5 +35,6 @@ namespace Nodify.Calculator public bool IsFunction { get; set; } public List FunctionInputs { get; set; } = new List(); public List FunctionOutputs { get; set; } = new List(); + public string ResponseModelClassName { get; set; } = string.Empty; } } diff --git a/Examples/Nodify.Calculator/Operations/OperationFactory.cs b/Examples/Nodify.Calculator/Operations/OperationFactory.cs index b33bc3d..48e066b 100644 --- a/Examples/Nodify.Calculator/Operations/OperationFactory.cs +++ b/Examples/Nodify.Calculator/Operations/OperationFactory.cs @@ -249,7 +249,8 @@ namespace Nodify.Calculator var _o = new APIOperationViewModel { Title = info.Title, - OperationType = info.OPType.ToUpper() + OperationType = info.OPType.ToUpper(), + ResponseModelClassName = info.ResponseModelClassName ?? string.Empty }; var connectorViewModel = new ConnectorViewModel() { @@ -263,14 +264,43 @@ namespace Nodify.Calculator IsInput = false }; _o.Output.Add(connectorViewModel2); - _o.Output.Add(new ConnectorViewModel { ConnectorColor = Color.LimeGreen }); + + // Add typed response output based on ResponseModelClassName + if (!string.IsNullOrEmpty(info.ResponseModelClassName)) + { + var responseClassName = info.ResponseModelClassName; + bool isList = responseClassName.StartsWith("List<") && responseClassName.EndsWith(">"); + var innerType = isList ? responseClassName.Substring(5, responseClassName.Length - 6) : responseClassName; + + // Square connector for model output + _o.Output.Add(new ConnectorViewModel + { + Title = responseClassName, + IsInput = false, + Shape = ConnectorShape.Square, + ConnectorColor = Color.MediumPurple, + DataType = responseClassName.ToLower() + }); + } + else + { + // Default: generic object output + _o.Output.Add(new ConnectorViewModel + { + Title = "Response", + IsInput = false, + Shape = ConnectorShape.Circle, + ConnectorColor = Color.LimeGreen, + DataType = "object" + }); + } + _o.Input.Add(connectorViewModel); foreach (var item in input) { item.ConnectorColor = Color.LimeGreen; _o.Input.Add(item); } - //_o.Input.AddRange(input); return _o; case OperationType.System: if (info.sysOp == SystemOperations.FUNCTION) diff --git a/Examples/Nodify.Calculator/OperationsMenuViewModel.cs b/Examples/Nodify.Calculator/OperationsMenuViewModel.cs index 11ff820..7053583 100644 --- a/Examples/Nodify.Calculator/OperationsMenuViewModel.cs +++ b/Examples/Nodify.Calculator/OperationsMenuViewModel.cs @@ -356,6 +356,7 @@ namespace Nodify.Calculator { var operations = new List(); var openApiDocument = OpenApiDocument.FromFileAsync(jsonFilePath).Result; + var createdModels = new HashSet(); foreach (var path in openApiDocument.Paths) { @@ -377,6 +378,10 @@ namespace Nodify.Calculator } } + // Extract response model from 200/201 response schema + var responseModelName = ExtractResponseModel(method.Value, path.Key, method.Key, createdModels); + ovmodel.ResponseModelClassName = responseModelName; + ovmodel.Output.Add(""); operations.Add(ovmodel); } @@ -385,6 +390,138 @@ namespace Nodify.Calculator return operations; } + private string ExtractResponseModel(OpenApiOperation operation, string path, string httpMethod, HashSet createdModels) + { + try + { + // Check 200 and 201 responses for schema + foreach (var statusCode in new[] { "200", "201", "default" }) + { + if (!operation.Responses.ContainsKey(statusCode)) + continue; + + var response = operation.Responses[statusCode]; + + // Try OpenAPI 2.0 style (response.Schema) first, then OpenAPI 3.0 (response.Content) + NJsonSchema.JsonSchema schema = response.Schema; + if (schema == null && response.Content != null && response.Content.Count > 0) + { + // OpenAPI 3.0: look for application/json or take first content entry + if (response.Content.TryGetValue("application/json", out var jsonContent)) + schema = jsonContent.Schema; + else + schema = response.Content.Values.FirstOrDefault()?.Schema; + } + if (schema == null) + continue; + + // Resolve $ref references + var actualSchema = schema.ActualSchema ?? schema; + + // Resolve the actual item schema for arrays + var targetSchema = actualSchema; + bool isList = false; + if (actualSchema.Type == NJsonSchema.JsonObjectType.Array && actualSchema.Item != null) + { + targetSchema = actualSchema.Item.ActualSchema ?? actualSchema.Item; + isList = true; + } + + // Resolve again in case of nested $ref + targetSchema = targetSchema.ActualSchema ?? targetSchema; + + // Get or derive class name from schema title, definition name, or path + var className = !string.IsNullOrEmpty(targetSchema.Title) + ? targetSchema.Title + : !string.IsNullOrEmpty(targetSchema.DocumentPath) ? System.IO.Path.GetFileNameWithoutExtension(targetSchema.DocumentPath) + : DeriveClassName(path, httpMethod); + + // Also try to get name from the schema's Id (definition key) + if (string.IsNullOrEmpty(className) || className == "swagger" || className == "openapi") + className = DeriveClassName(path, httpMethod); + + className = SanitizeClassName(className); + + // Use ActualProperties which resolves inherited/allOf properties + var props = targetSchema.ActualProperties; + if (string.IsNullOrEmpty(className) || props == null || props.Count == 0) + continue; + + // Create model .cs file if not already created + if (createdModels.Add(className)) + { + GenerateModelFromSchema(className, targetSchema); + // Add to AvailableModels + var modelInfo = new OperationInfoViewModel + { + Title = className, + IsModelNode = true, + Type = OperationType.System, + sysOp = SystemOperations.GET_SET, + ClassName = className + }; + AddNewModel(modelInfo); + } + + return isList ? $"List<{className}>" : className; + } + } + catch { /* Response schema parsing is best-effort */ } + + return string.Empty; + } + + private void GenerateModelFromSchema(string className, NJsonSchema.JsonSchema schema) + { + var customModelDir = "CustomModels"; + Directory.CreateDirectory(customModelDir); + var sb = new System.Text.StringBuilder(); + sb.AppendLine($"public class {className}"); + sb.AppendLine("{"); + // Use ActualProperties to resolve $ref and allOf/inheritance + var props = schema.ActualProperties != null && schema.ActualProperties.Count > 0 + ? (IEnumerable>)schema.ActualProperties + : schema.Properties; + foreach (var prop in props) + { + var propType = MapJsonSchemaType(prop.Value); + var propName = Executor.ToPascalCase(prop.Key); + sb.AppendLine($"\tpublic {propType} {propName} {{ get; set; }}"); + } + sb.AppendLine("}"); + File.WriteAllText(Path.Combine(customModelDir, $"{className}.cs"), sb.ToString()); + } + + private static string MapJsonSchemaType(NJsonSchema.JsonSchemaProperty prop) + { + // Resolve the actual schema in case of $ref + var actual = prop.ActualSchema ?? prop; + var type = actual.Type; + if (type.HasFlag(NJsonSchema.JsonObjectType.String)) return "string"; + if (type.HasFlag(NJsonSchema.JsonObjectType.Integer)) return "int"; + if (type.HasFlag(NJsonSchema.JsonObjectType.Number)) return "double"; + if (type.HasFlag(NJsonSchema.JsonObjectType.Boolean)) return "bool"; + if (type.HasFlag(NJsonSchema.JsonObjectType.Array)) return "List"; + if (type.HasFlag(NJsonSchema.JsonObjectType.Object)) return "object"; + // If type is None but has properties, it's likely an object via $ref + if (type == NJsonSchema.JsonObjectType.None && actual.Properties.Count > 0) return "object"; + return "string"; + } + + private static string DeriveClassName(string path, string httpMethod) + { + // e.g. /api/users/{id} + get -> GetUsersResponse + var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries) + .Where(s => !s.StartsWith("{")) + .Select(s => Executor.ToPascalCase(s)); + return $"{Executor.ToPascalCase(httpMethod)}{string.Join("", segments)}Response"; + } + + private static string SanitizeClassName(string name) + { + return new string(name.Where(c => char.IsLetterOrDigit(c) || c == '_').ToArray()); + } + private void SaveSwaggerNodesToDb(List nodes, string swaggerFileName) { using var db = new LiteDbHelper("SwaggerNodes"); @@ -397,7 +534,8 @@ namespace Nodify.Calculator Title = node.Title ?? string.Empty, OPType = node.OPType ?? string.Empty, InputNames = new List(node.Input), - SwaggerFileName = swaggerFileName + SwaggerFileName = swaggerFileName, + ResponseModelClassName = node.ResponseModelClassName ?? string.Empty }); } } @@ -423,6 +561,7 @@ namespace Nodify.Calculator ovmodel.Input.Add(inputName); } + ovmodel.ResponseModelClassName = saved.ResponseModelClassName ?? string.Empty; ovmodel.Output.Add(""); SwaggerOperations.Add(ovmodel); }