Implemented Direct model creation when swagger file is imported to the list

This commit is contained in:
Ankitkumar Satapara
2026-04-18 23:16:21 +05:30
parent c14a2a730d
commit 863fc679ed
7 changed files with 217 additions and 7 deletions

View File

@@ -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);
}
}
}

View File

@@ -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;

View File

@@ -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}");
}

View File

@@ -9,5 +9,6 @@ namespace Nodify.Calculator.Models
public string OPType { get; set; } = string.Empty;
public List<string> InputNames { get; set; } = new List<string>();
public string SwaggerFileName { get; set; } = string.Empty;
public string ResponseModelClassName { get; set; } = string.Empty;
}
}

View File

@@ -35,5 +35,6 @@ namespace Nodify.Calculator
public bool IsFunction { get; set; }
public List<FunctionParameterInfo> FunctionInputs { get; set; } = new List<FunctionParameterInfo>();
public List<FunctionParameterInfo> FunctionOutputs { get; set; } = new List<FunctionParameterInfo>();
public string ResponseModelClassName { get; set; } = string.Empty;
}
}

View File

@@ -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)

View File

@@ -356,6 +356,7 @@ namespace Nodify.Calculator
{
var operations = new List<OperationInfoViewModel>();
var openApiDocument = OpenApiDocument.FromFileAsync(jsonFilePath).Result;
var createdModels = new HashSet<string>();
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<string> 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<KeyValuePair<string, NJsonSchema.JsonSchemaProperty>>)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<object>";
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<OperationInfoViewModel> nodes, string swaggerFileName)
{
using var db = new LiteDbHelper<SwaggerNodeModel>("SwaggerNodes");
@@ -397,7 +534,8 @@ namespace Nodify.Calculator
Title = node.Title ?? string.Empty,
OPType = node.OPType ?? string.Empty,
InputNames = new List<string>(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);
}