Added nested level class support for import swagger
Some checks failed
Build / build (push) Has been cancelled

This commit is contained in:
Ankitkumar Satapara
2026-04-23 13:47:43 +05:30
parent c8c556921f
commit 361c1bb8c4
3 changed files with 208 additions and 22 deletions

View File

@@ -356,14 +356,41 @@ namespace Nodify.Calculator
// Add property outputs // Add property outputs
foreach (var prop in properties) foreach (var prop in properties)
{ {
var propColor = ConnectorViewModel.GetColorForType(prop.Type); var propType = prop.Type;
bool isList = propType.StartsWith("List<") && propType.EndsWith(">");
var innerType = isList ? propType.Substring(5, propType.Length - 6) : propType;
// Check if the inner type is a model (has a .cs file in CustomModels)
var primitiveTypes = new[] { "string", "int", "double", "bool", "float", "decimal", "long", "object", "datetime" };
bool isModel = !primitiveTypes.Contains(innerType.ToLower())
&& File.Exists(Path.Combine(customModelDir, $"{innerType}.cs"));
ConnectorShape shape;
System.Drawing.Color color;
if (isList)
{
shape = ConnectorShape.Grid;
color = System.Drawing.Color.MediumSpringGreen;
}
else if (isModel)
{
shape = ConnectorShape.Square;
color = System.Drawing.Color.MediumPurple;
}
else
{
shape = ConnectorShape.Circle;
color = ConnectorViewModel.GetColorForType(propType);
}
splitOp.Output.Add(new ConnectorViewModel splitOp.Output.Add(new ConnectorViewModel
{ {
Title = $"{prop.Name} ({prop.Type})", Title = $"{prop.Name} ({propType})",
IsInput = false, IsInput = false,
Shape = ConnectorShape.Circle, Shape = shape,
ConnectorColor = propColor, ConnectorColor = color,
DataType = prop.Type.ToLower() DataType = isModel || isList ? propType : propType.ToLower()
}); });
} }
@@ -454,13 +481,39 @@ namespace Nodify.Calculator
// Add property inputs // Add property inputs
foreach (var prop in properties) foreach (var prop in properties)
{ {
var propColor = ConnectorViewModel.GetColorForType(prop.Type); var propType = prop.Type;
bool isList = propType.StartsWith("List<") && propType.EndsWith(">");
var innerType = isList ? propType.Substring(5, propType.Length - 6) : propType;
var primTypes = new[] { "string", "int", "double", "bool", "float", "decimal", "long", "object", "datetime" };
bool isModel = !primTypes.Contains(innerType.ToLower())
&& File.Exists(Path.Combine(customModelDir, $"{innerType}.cs"));
ConnectorShape shape;
System.Drawing.Color color;
if (isList)
{
shape = ConnectorShape.Grid;
color = System.Drawing.Color.MediumSpringGreen;
}
else if (isModel)
{
shape = ConnectorShape.Square;
color = System.Drawing.Color.MediumPurple;
}
else
{
shape = ConnectorShape.Circle;
color = ConnectorViewModel.GetColorForType(propType);
}
newObjOp.Input.Add(new ConnectorViewModel newObjOp.Input.Add(new ConnectorViewModel
{ {
Title = $"{prop.Name} ({prop.Type})", Title = $"{prop.Name} ({propType})",
Shape = ConnectorShape.Circle, Shape = shape,
ConnectorColor = propColor, ConnectorColor = color,
DataType = prop.Type.ToLower() DataType = isModel || isList ? propType : propType.ToLower()
}); });
} }

View File

@@ -393,6 +393,7 @@ namespace Nodify.Calculator
var operations = new List<OperationInfoViewModel>(); var operations = new List<OperationInfoViewModel>();
var openApiDocument = OpenApiDocument.FromFileAsync(jsonFilePath).Result; var openApiDocument = OpenApiDocument.FromFileAsync(jsonFilePath).Result;
var createdModels = new HashSet<string>(); var createdModels = new HashSet<string>();
var definitions = openApiDocument.Definitions;
foreach (var path in openApiDocument.Paths) foreach (var path in openApiDocument.Paths)
{ {
@@ -431,12 +432,12 @@ namespace Nodify.Calculator
var httpMethodLower = method.Key.ToLower(); var httpMethodLower = method.Key.ToLower();
if (httpMethodLower == "post" || httpMethodLower == "put" || httpMethodLower == "patch") if (httpMethodLower == "post" || httpMethodLower == "put" || httpMethodLower == "patch")
{ {
var bodyModelName = ExtractRequestBodyModel(method.Value, path.Key, method.Key, createdModels); var bodyModelName = ExtractRequestBodyModel(method.Value, path.Key, method.Key, createdModels, definitions);
ovmodel.RequestBodyModelClassName = bodyModelName; ovmodel.RequestBodyModelClassName = bodyModelName;
} }
// Extract response model from 200/201 response schema // Extract response model from 200/201 response schema
var responseModelName = ExtractResponseModel(method.Value, path.Key, method.Key, createdModels); var responseModelName = ExtractResponseModel(method.Value, path.Key, method.Key, createdModels, definitions);
// Fallback: if no explicit response model, use request body model for POST/PUT/PATCH // Fallback: if no explicit response model, use request body model for POST/PUT/PATCH
if (string.IsNullOrEmpty(responseModelName) && !string.IsNullOrEmpty(ovmodel.RequestBodyModelClassName)) if (string.IsNullOrEmpty(responseModelName) && !string.IsNullOrEmpty(ovmodel.RequestBodyModelClassName))
@@ -452,7 +453,7 @@ namespace Nodify.Calculator
return operations; return operations;
} }
private string ExtractResponseModel(OpenApiOperation operation, string path, string httpMethod, HashSet<string> createdModels) private string ExtractResponseModel(OpenApiOperation operation, string path, string httpMethod, HashSet<string> createdModels, IDictionary<string, NJsonSchema.JsonSchema> definitions)
{ {
try try
{ {
@@ -522,7 +523,7 @@ namespace Nodify.Calculator
// Create model .cs file if not already created // Create model .cs file if not already created
if (createdModels.Add(className)) if (createdModels.Add(className))
{ {
GenerateModelFromSchema(className, targetSchema); GenerateModelFromSchema(className, targetSchema, createdModels, definitions);
// Add to AvailableModels // Add to AvailableModels
var modelInfo = new OperationInfoViewModel var modelInfo = new OperationInfoViewModel
{ {
@@ -543,7 +544,7 @@ namespace Nodify.Calculator
return string.Empty; return string.Empty;
} }
private string ExtractRequestBodyModel(NSwag.OpenApiOperation operation, string path, string httpMethod, HashSet<string> createdModels) private string ExtractRequestBodyModel(NSwag.OpenApiOperation operation, string path, string httpMethod, HashSet<string> createdModels, IDictionary<string, NJsonSchema.JsonSchema> definitions)
{ {
try try
{ {
@@ -595,7 +596,7 @@ namespace Nodify.Calculator
// Generate model file if not already done // Generate model file if not already done
if (createdModels.Add(className)) if (createdModels.Add(className))
{ {
GenerateModelFromSchema(className, targetSchema); GenerateModelFromSchema(className, targetSchema, createdModels, definitions);
var modelInfo = new OperationInfoViewModel var modelInfo = new OperationInfoViewModel
{ {
Title = className, Title = className,
@@ -622,7 +623,7 @@ namespace Nodify.Calculator
return $"{Executor.ToPascalCase(httpMethod)}{string.Join("", segments)}Request"; return $"{Executor.ToPascalCase(httpMethod)}{string.Join("", segments)}Request";
} }
private void GenerateModelFromSchema(string className, NJsonSchema.JsonSchema schema) private void GenerateModelFromSchema(string className, NJsonSchema.JsonSchema schema, HashSet<string> createdModels, IDictionary<string, NJsonSchema.JsonSchema> definitions)
{ {
var customModelDir = Path.Combine(ProjectManager.ProjectDirectory, "CustomModels"); var customModelDir = Path.Combine(ProjectManager.ProjectDirectory, "CustomModels");
Directory.CreateDirectory(customModelDir); Directory.CreateDirectory(customModelDir);
@@ -635,7 +636,7 @@ namespace Nodify.Calculator
: schema.Properties; : schema.Properties;
foreach (var prop in props) foreach (var prop in props)
{ {
var propType = MapJsonSchemaType(prop.Value); var propType = ResolvePropertyType(prop.Value, createdModels, definitions);
var propName = Executor.ToPascalCase(prop.Key); var propName = Executor.ToPascalCase(prop.Key);
sb.AppendLine($"\tpublic {propType} {propName} {{ get; set; }}"); sb.AppendLine($"\tpublic {propType} {propName} {{ get; set; }}");
} }
@@ -643,6 +644,119 @@ namespace Nodify.Calculator
File.WriteAllText(Path.Combine(customModelDir, $"{className}.cs"), sb.ToString()); File.WriteAllText(Path.Combine(customModelDir, $"{className}.cs"), sb.ToString());
} }
/// <summary>
/// Resolves the C# type for a JSON schema property. For $ref objects and arrays of $ref,
/// recursively generates the nested model classes and returns the proper class name.
/// </summary>
private string ResolvePropertyType(NJsonSchema.JsonSchemaProperty prop, HashSet<string> createdModels, IDictionary<string, NJsonSchema.JsonSchema> definitions)
{
var actual = prop.ActualSchema ?? prop;
var type = actual.Type;
// Primitives
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";
// Array — resolve the item type recursively
if (type.HasFlag(NJsonSchema.JsonObjectType.Array))
{
var itemSchema = actual.Item?.ActualSchema ?? actual.Item;
if (itemSchema != null)
{
var itemType = ResolveSchemaAsType(itemSchema, createdModels, definitions);
return $"List<{itemType}>";
}
return "List<object>";
}
// Object or $ref — resolve the nested model
if (type.HasFlag(NJsonSchema.JsonObjectType.Object)
|| type == NJsonSchema.JsonObjectType.None)
{
bool hasNestedProps = (actual.ActualProperties != null && actual.ActualProperties.Count > 0)
|| (actual.Properties != null && actual.Properties.Count > 0);
if (hasNestedProps)
{
return ResolveSchemaAsType(actual, createdModels, definitions);
}
return "object";
}
return "string";
}
/// <summary>
/// Given a resolved JSON schema that represents a complex object, determines its class name,
/// generates the model file (recursively for nested types), and registers it. Returns the class name.
/// </summary>
private string ResolveSchemaAsType(NJsonSchema.JsonSchema schema, HashSet<string> createdModels, IDictionary<string, NJsonSchema.JsonSchema> definitions)
{
var actual = schema.ActualSchema ?? schema;
// Primitives don't need a model class
var primitive = MapPrimitiveSchemaType(actual);
if (!string.IsNullOrEmpty(primitive)) return primitive;
var hasProps = (actual.ActualProperties != null && actual.ActualProperties.Count > 0)
|| (actual.Properties != null && actual.Properties.Count > 0);
if (!hasProps)
return "object";
// Best approach: look up the schema in the document's Definitions by reference equality
string nestedName = null;
if (definitions != null)
{
foreach (var kvp in definitions)
{
if (ReferenceEquals(kvp.Value, actual) || ReferenceEquals(kvp.Value.ActualSchema, actual))
{
nestedName = kvp.Key;
break;
}
}
}
// Fallback: try schema Title, Id, or DocumentPath
if (string.IsNullOrEmpty(nestedName) && !string.IsNullOrEmpty(actual.Title))
nestedName = actual.Title;
if (string.IsNullOrEmpty(nestedName) && !string.IsNullOrEmpty(actual.Id))
nestedName = System.IO.Path.GetFileNameWithoutExtension(actual.Id);
if (string.IsNullOrEmpty(nestedName) && !string.IsNullOrEmpty(actual.DocumentPath))
{
var segments = actual.DocumentPath.Split('/');
nestedName = segments.LastOrDefault();
}
if (string.IsNullOrEmpty(nestedName))
return "object";
nestedName = SanitizeClassName(nestedName);
if (string.IsNullOrEmpty(nestedName))
return "object";
// Generate the nested model if not already created
if (createdModels.Add(nestedName))
{
GenerateModelFromSchema(nestedName, actual, createdModels, definitions);
var modelInfo = new OperationInfoViewModel
{
Title = nestedName,
IsModelNode = true,
Type = OperationType.System,
sysOp = SystemOperations.GET_SET,
ClassName = nestedName
};
AddNewModel(modelInfo);
}
return nestedName;
}
// Keep legacy static version for any code that doesn't need recursion
private static string MapJsonSchemaType(NJsonSchema.JsonSchemaProperty prop) private static string MapJsonSchemaType(NJsonSchema.JsonSchemaProperty prop)
{ {
// Resolve the actual schema in case of $ref // Resolve the actual schema in case of $ref

View File

@@ -311,6 +311,24 @@
}, },
"components": { "components": {
"schemas": { "schemas": {
"AuditHistory": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int32"
},
"createdBy": {
"type": "string",
"nullable": true
},
"createdDate": {
"type": "string",
"format": "date-time"
}
},
"additionalProperties": false
},
"Operation": { "Operation": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -347,7 +365,10 @@
}, },
"isGlutenFree": { "isGlutenFree": {
"type": "boolean", "type": "boolean",
"description": "Is this Pizza Gluten Free\r\n(bool true --\u003E yes)" "description": "Is this Pizza Gluten Free\r\n(bool true --> yes)"
},
"auditHistory": {
"$ref": "#/components/schemas/AuditHistory"
} }
}, },
"additionalProperties": false, "additionalProperties": false,
@@ -378,9 +399,7 @@
"nullable": true "nullable": true
} }
}, },
"additionalProperties": { "additionalProperties": { }
}
} }
} }
} }