From 361c1bb8c48a1c087726720e8e6c8b26873782ff Mon Sep 17 00:00:00 2001 From: Ankitkumar Satapara Date: Thu, 23 Apr 2026 13:47:43 +0530 Subject: [PATCH] Added nested level class support for import swagger --- .../Nodify.Calculator/CalculatorViewModel.cs | 73 ++++++++-- .../OperationsMenuViewModel.cs | 130 ++++++++++++++++-- Examples/Nodify.Calculator/swagger.json | 27 +++- 3 files changed, 208 insertions(+), 22 deletions(-) diff --git a/Examples/Nodify.Calculator/CalculatorViewModel.cs b/Examples/Nodify.Calculator/CalculatorViewModel.cs index 63c9a46..239ffd6 100644 --- a/Examples/Nodify.Calculator/CalculatorViewModel.cs +++ b/Examples/Nodify.Calculator/CalculatorViewModel.cs @@ -356,14 +356,41 @@ namespace Nodify.Calculator // Add property outputs 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 { - Title = $"{prop.Name} ({prop.Type})", + Title = $"{prop.Name} ({propType})", IsInput = false, - Shape = ConnectorShape.Circle, - ConnectorColor = propColor, - DataType = prop.Type.ToLower() + Shape = shape, + ConnectorColor = color, + DataType = isModel || isList ? propType : propType.ToLower() }); } @@ -454,13 +481,39 @@ namespace Nodify.Calculator // Add property inputs 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 { - Title = $"{prop.Name} ({prop.Type})", - Shape = ConnectorShape.Circle, - ConnectorColor = propColor, - DataType = prop.Type.ToLower() + Title = $"{prop.Name} ({propType})", + Shape = shape, + ConnectorColor = color, + DataType = isModel || isList ? propType : propType.ToLower() }); } diff --git a/Examples/Nodify.Calculator/OperationsMenuViewModel.cs b/Examples/Nodify.Calculator/OperationsMenuViewModel.cs index fe92cc9..8f11679 100644 --- a/Examples/Nodify.Calculator/OperationsMenuViewModel.cs +++ b/Examples/Nodify.Calculator/OperationsMenuViewModel.cs @@ -393,6 +393,7 @@ namespace Nodify.Calculator var operations = new List(); var openApiDocument = OpenApiDocument.FromFileAsync(jsonFilePath).Result; var createdModels = new HashSet(); + var definitions = openApiDocument.Definitions; foreach (var path in openApiDocument.Paths) { @@ -431,12 +432,12 @@ namespace Nodify.Calculator var httpMethodLower = method.Key.ToLower(); 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; } // 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 if (string.IsNullOrEmpty(responseModelName) && !string.IsNullOrEmpty(ovmodel.RequestBodyModelClassName)) @@ -452,7 +453,7 @@ namespace Nodify.Calculator return operations; } - private string ExtractResponseModel(OpenApiOperation operation, string path, string httpMethod, HashSet createdModels) + private string ExtractResponseModel(OpenApiOperation operation, string path, string httpMethod, HashSet createdModels, IDictionary definitions) { try { @@ -522,7 +523,7 @@ namespace Nodify.Calculator // Create model .cs file if not already created if (createdModels.Add(className)) { - GenerateModelFromSchema(className, targetSchema); + GenerateModelFromSchema(className, targetSchema, createdModels, definitions); // Add to AvailableModels var modelInfo = new OperationInfoViewModel { @@ -543,7 +544,7 @@ namespace Nodify.Calculator return string.Empty; } - private string ExtractRequestBodyModel(NSwag.OpenApiOperation operation, string path, string httpMethod, HashSet createdModels) + private string ExtractRequestBodyModel(NSwag.OpenApiOperation operation, string path, string httpMethod, HashSet createdModels, IDictionary definitions) { try { @@ -595,7 +596,7 @@ namespace Nodify.Calculator // Generate model file if not already done if (createdModels.Add(className)) { - GenerateModelFromSchema(className, targetSchema); + GenerateModelFromSchema(className, targetSchema, createdModels, definitions); var modelInfo = new OperationInfoViewModel { Title = className, @@ -622,7 +623,7 @@ namespace Nodify.Calculator 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 createdModels, IDictionary definitions) { var customModelDir = Path.Combine(ProjectManager.ProjectDirectory, "CustomModels"); Directory.CreateDirectory(customModelDir); @@ -635,7 +636,7 @@ namespace Nodify.Calculator : schema.Properties; foreach (var prop in props) { - var propType = MapJsonSchemaType(prop.Value); + var propType = ResolvePropertyType(prop.Value, createdModels, definitions); var propName = Executor.ToPascalCase(prop.Key); sb.AppendLine($"\tpublic {propType} {propName} {{ get; set; }}"); } @@ -643,6 +644,119 @@ namespace Nodify.Calculator File.WriteAllText(Path.Combine(customModelDir, $"{className}.cs"), sb.ToString()); } + /// + /// 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. + /// + private string ResolvePropertyType(NJsonSchema.JsonSchemaProperty prop, HashSet createdModels, IDictionary 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 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"; + } + + /// + /// 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. + /// + private string ResolveSchemaAsType(NJsonSchema.JsonSchema schema, HashSet createdModels, IDictionary 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) { // Resolve the actual schema in case of $ref diff --git a/Examples/Nodify.Calculator/swagger.json b/Examples/Nodify.Calculator/swagger.json index de37d65..6fe60a9 100644 --- a/Examples/Nodify.Calculator/swagger.json +++ b/Examples/Nodify.Calculator/swagger.json @@ -311,6 +311,24 @@ }, "components": { "schemas": { + "AuditHistory": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "createdBy": { + "type": "string", + "nullable": true + }, + "createdDate": { + "type": "string", + "format": "date-time" + } + }, + "additionalProperties": false + }, "Operation": { "type": "object", "properties": { @@ -347,7 +365,10 @@ }, "isGlutenFree": { "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, @@ -378,9 +399,7 @@ "nullable": true } }, - "additionalProperties": { - - } + "additionalProperties": { } } } }