From eb2a353694dc170bed4cff5c46ac7d78bdb8ee0d Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 21 Mar 2024 14:53:07 -0400 Subject: [PATCH 01/16] - fixes a bug where multiple allOf entries type would not get merged if they were part of a discriminator --- CHANGELOG.md | 1 + .../Extensions/OpenApiSchemaExtensions.cs | 4 +- src/Kiota.Builder/KiotaBuilder.cs | 18 +++- .../Kiota.Builder.Tests/KiotaBuilderTests.cs | 92 +++++++++++++++++-- 4 files changed, 101 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dc9a45aea..b9cac3fa91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed Go generation bug when dealing with scalar collection body. [#4380](https://github.com/microsoft/kiota/issues/4380) - Fixed a bug where multiple Visual Studio Code instances would make the extension install/update fail. [#3686](https://github.com/microsoft/kiota/issues/3686) - Fixed a bug where models properties named "additionalData" or "backingstore" would be ignored. [#4224](https://github.com/microsoft/kiota/issues/4224) +- Fixed a bug where multiple allOf entries type would not get merged if they were part of a discriminator. [#4325](https://github.com/microsoft/kiota/issues/4325) - PREVIEW: Renamed the config commands to workspace. [#4310](https://github.com/microsoft/kiota/issues/4310) - PREVIEW: Moved preview configuration files to the .kiota directory. [#4310](https://github.com/microsoft/kiota/issues/4310) - PREVIEW: Moved the copy descriptions to dedicated folders. [#4310](https://github.com/microsoft/kiota/issues/4310) diff --git a/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs b/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs index 4c705bdeeb..b0bba5e344 100644 --- a/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs +++ b/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs @@ -78,7 +78,9 @@ public static bool IsInherited(this OpenApiSchema? schema) { if (schema is null) return false; var meaningfulSchemas = schema.AllOf.FlattenSchemaIfRequired(static x => x.AllOf).Where(static x => x.IsSemanticallyMeaningful()).ToArray(); - return meaningfulSchemas.Count(static x => !string.IsNullOrEmpty(x.Reference?.Id)) == 1 && meaningfulSchemas.Count(static x => string.IsNullOrEmpty(x.Reference?.Id)) == 1; + return meaningfulSchemas.Count(static x => !string.IsNullOrEmpty(x.Reference?.Id)) == 1 && + (meaningfulSchemas.Count(static x => string.IsNullOrEmpty(x.Reference?.Id)) == 1 || + schema.IsSemanticallyMeaningful()); } internal static OpenApiSchema? MergeIntersectionSchemaEntries(this OpenApiSchema? schema) diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index ef2920287b..24f8aa9c88 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -1548,9 +1548,9 @@ private CodeType CreateModelDeclarationAndType(OpenApiUrlTreeNode currentNode, O TypeDefinition = codeDeclaration, }; } - private CodeType CreateInheritedModelDeclaration(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, OpenApiOperation? operation, string classNameSuffix, CodeNamespace codeNamespace, bool isRequestBody) + private CodeType CreateInheritedModelDeclaration(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, OpenApiOperation? operation, string classNameSuffix, CodeNamespace codeNamespace, bool isRequestBody, string typeNameForInlineSchema) { - var allOfs = schema.AllOf.FlattenSchemaIfRequired(static x => x.AllOf); + var allOfs = (schema.IsSemanticallyMeaningful() ? new OpenApiSchema[] { schema } : []).Union(schema.AllOf.FlattenSchemaIfRequired(static x => x.AllOf)); CodeElement? codeDeclaration = null; var className = string.Empty; var codeNamespaceFromParent = GetShortestNamespace(codeNamespace, schema); @@ -1561,7 +1561,9 @@ private CodeType CreateInheritedModelDeclaration(OpenApiUrlTreeNode currentNode, var shortestNamespace = string.IsNullOrEmpty(referenceId) ? codeNamespaceFromParent : rootNamespace?.FindOrAddNamespace(shortestNamespaceName); className = (currentSchema.GetSchemaName() is string cName && !string.IsNullOrEmpty(cName) ? cName : - currentNode.GetClassName(config.StructuredMimeTypes, operation: operation, suffix: classNameSuffix, schema: schema, requestBody: isRequestBody)) + (string.IsNullOrEmpty(className) && !string.IsNullOrEmpty(typeNameForInlineSchema) ? + typeNameForInlineSchema : + currentNode.GetClassName(config.StructuredMimeTypes, operation: operation, suffix: classNameSuffix, schema: currentSchema, requestBody: isRequestBody))) .CleanupSymbolName(); if (shortestNamespace != null) codeDeclaration = AddModelDeclarationIfDoesntExist(currentNode, currentSchema, className, shortestNamespace, codeDeclaration as CodeClass); @@ -1672,7 +1674,7 @@ private CodeTypeBase CreateModelDeclarations(OpenApiUrlTreeNode currentNode, Ope if (schema.IsInherited()) { - return CreateInheritedModelDeclaration(currentNode, schema, operation, suffix, codeNamespace, isRequestBody); + return CreateInheritedModelDeclaration(currentNode, schema, operation, suffix, codeNamespace, isRequestBody, typeNameForInlineSchema); } if (schema.IsIntersection() && schema.MergeIntersectionSchemaEntries() is OpenApiSchema mergedSchema) @@ -1821,8 +1823,9 @@ private CodeNamespace GetShortestNamespace(CodeNamespace currentNamespace, OpenA } private CodeClass AddModelClass(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, string declarationName, CodeNamespace currentNamespace, CodeClass? inheritsFrom = null) { - if (inheritsFrom == null && schema.AllOf.FirstOrDefault(static x => x.Reference != null) is OpenApiSchema parentSchema) + if (inheritsFrom == null && schema.AllOf.Where(static x => x.Reference != null).ToArray() is { Length: 1 } referencedSchemas) {// any non-reference would be the current class in some description styles + var parentSchema = referencedSchemas[0]; var parentClassNamespace = GetShortestNamespace(currentNamespace, parentSchema); inheritsFrom = (CodeClass)AddModelDeclarationIfDoesntExist(currentNode, parentSchema, parentSchema.GetSchemaName().CleanupSymbolName(), parentClassNamespace); } @@ -2065,6 +2068,11 @@ internal static void AddDiscriminatorMethod(CodeClass newClass, string discrimin } var className = currentNode.GetClassName(config.StructuredMimeTypes, schema: discriminatorSchema).CleanupSymbolName(); var shouldInherit = discriminatorSchema.AllOf.Any(x => currentSchema.Reference?.Id.Equals(x.Reference?.Id, StringComparison.OrdinalIgnoreCase) ?? false); + if (baseClass is not null && !discriminatorSchema.IsInherited()) + { + logger.LogWarning("Discriminator {ComponentKey} is not inherited from {ClassName}.", componentKey, baseClass.Name); + return null; + } var codeClass = AddModelDeclarationIfDoesntExist(currentNode, discriminatorSchema, className, GetShortestNamespace(currentNamespace, discriminatorSchema), shouldInherit ? baseClass : null); return new CodeType { diff --git a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs index 82dabd2d6a..ca1a16ec12 100644 --- a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs +++ b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs @@ -1878,7 +1878,7 @@ public void Inline_Property_Inheritance_Is_Supported() Type = "object", Properties = new Dictionary { { - "info", new OpenApiSchema { + "info2", new OpenApiSchema { Type = "object", Properties = new Dictionary { { @@ -1923,12 +1923,12 @@ public void Inline_Property_Inheritance_Is_Supported() var itemsNS = codeModel.FindNamespaceByName("ApiSdk.resource.item"); var responseClass = itemsNS.FindChildByName("ResourceGetResponse"); var derivedResourceClass = itemsNS.FindChildByName("ResourceGetResponse_derivedResource"); - var derivedResourceInfoClass = itemsNS.FindChildByName("ResourceGetResponse_derivedResource_info"); + var derivedResourceInfoClass = itemsNS.FindChildByName("ResourceGetResponse_derivedResource_info2"); Assert.NotNull(resourceClass); Assert.NotNull(derivedResourceClass); - Assert.NotNull(derivedResourceClass.StartBlock); + Assert.NotNull(derivedResourceClass.StartBlock.Inherits); Assert.Equal(derivedResourceClass.StartBlock.Inherits.TypeDefinition, resourceClass); Assert.NotNull(derivedResourceInfoClass); Assert.NotNull(responseClass); @@ -7396,7 +7396,83 @@ public async Task InheritanceWithAllOfInBaseType() Assert.NotNull(codeModel.FindChildByName("Group")); } [Fact] - public async Task InheritanceWithAllOfWith3Parts() + public async Task InheritanceWithAllOfWith3Parts3Schema() + { + var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + await using var fs = await GetDocumentStream(@"openapi: 3.0.1 +info: + title: OData Service for namespace microsoft.graph + description: This OData service is located at https://graph.microsoft.com/v1.0 + version: 1.0.1 +servers: + - url: https://graph.microsoft.com/v1.0 +paths: + /directoryObject: + get: + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/microsoft.graph.directoryObject' + /group: + get: + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/microsoft.graph.group' +components: + schemas: + microsoft.graph.directoryObject: + title: 'directoryObject' + required: ['@odata.type'] + type: 'object' + properties: + '@odata.type': + type: 'string' + default: '#microsoft.graph.directoryObject' + discriminator: + propertyName: '@odata.type' + mapping: + '#microsoft.graph.group': '#/components/schemas/microsoft.graph.group' + microsoft.graph.groupFacet1: + title: 'group part 1' + type: 'object' + properties: + groupprop1: + type: 'string' + microsoft.graph.groupFacet2: + title: 'group part 2' + type: 'object' + properties: + groupprop2: + type: 'string' + microsoft.graph.group: + title: 'group' + allOf: + - '$ref': '#/components/schemas/microsoft.graph.directoryObject' + - '$ref': '#/components/schemas/microsoft.graph.groupFacet1' + - '$ref': '#/components/schemas/microsoft.graph.groupFacet2'"); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath }, _httpClient); + var document = await builder.CreateOpenApiDocumentAsync(fs); + var node = builder.CreateUriSpace(document); + var codeModel = builder.CreateSourceModel(node); + var directoryObjectClass = codeModel.FindChildByName("DirectoryObject"); + Assert.NotNull(directoryObjectClass); + var resultClass = codeModel.FindChildByName("Group"); + Assert.NotNull(resultClass); + Assert.Equal(4, resultClass.Properties.Count()); + Assert.Null(resultClass.StartBlock.Inherits); + Assert.Single(resultClass.Properties.Where(static x => x.Kind is CodePropertyKind.AdditionalData)); + Assert.Single(resultClass.Properties.Where(static x => x.Name.Equals("oDataType", StringComparison.OrdinalIgnoreCase))); + Assert.Single(resultClass.Properties.Where(static x => x.Name.Equals("groupprop1", StringComparison.OrdinalIgnoreCase))); + Assert.Single(resultClass.Properties.Where(static x => x.Name.Equals("groupprop2", StringComparison.OrdinalIgnoreCase))); + } + [Fact] + public async Task InheritanceWithAllOfWith3Parts1Schema2Inline() { var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); await using var fs = await GetDocumentStream(@"openapi: 3.0.1 @@ -7411,7 +7487,6 @@ public async Task InheritanceWithAllOfWith3Parts() get: responses: '200': - description: Example response content: application/json: schema: @@ -7429,7 +7504,6 @@ public async Task InheritanceWithAllOfWith3Parts() discriminator: propertyName: '@odata.type' mapping: - '#microsoft.graph.user': '#/components/schemas/microsoft.graph.user' '#microsoft.graph.group': '#/components/schemas/microsoft.graph.group' microsoft.graph.group: allOf: @@ -7451,9 +7525,11 @@ public async Task InheritanceWithAllOfWith3Parts() var codeModel = builder.CreateSourceModel(node); var resultClass = codeModel.FindChildByName("Group"); Assert.NotNull(resultClass); + Assert.Equal("directoryObject", resultClass.StartBlock.Inherits?.Name, StringComparer.OrdinalIgnoreCase); Assert.Equal(2, resultClass.Properties.Count()); - Assert.Single(resultClass.Properties.Where(x => x.Name.Equals("groupprop1", StringComparison.OrdinalIgnoreCase))); - Assert.Single(resultClass.Properties.Where(x => x.Name.Equals("groupprop2", StringComparison.OrdinalIgnoreCase))); + Assert.Empty(resultClass.Properties.Where(static x => x.Name.Equals("oDataType", StringComparison.OrdinalIgnoreCase))); + Assert.Single(resultClass.Properties.Where(static x => x.Name.Equals("groupprop1", StringComparison.OrdinalIgnoreCase))); + Assert.Single(resultClass.Properties.Where(static x => x.Name.Equals("groupprop2", StringComparison.OrdinalIgnoreCase))); } [Fact] public async Task EnumsWithNullableDoesNotResultInInlineType() From ce1cec96d4c3fdbe3d82b5130cc2f68de353161f Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 21 Mar 2024 17:10:01 -0400 Subject: [PATCH 02/16] - fixes a bug where allOf structure with one entry and properties would not be considered as inheritance case Signed-off-by: Vincent Biret --- CHANGELOG.md | 1 + .../Extensions/OpenApiSchemaExtensions.cs | 32 ++- src/Kiota.Builder/KiotaBuilder.cs | 9 +- .../OpenApiSchemaExtensionsTests.cs | 187 +++++++++++++++--- .../Kiota.Builder.Tests/KiotaBuilderTests.cs | 98 +++++++++ 5 files changed, 273 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9cac3fa91..e3d512eb02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed a bug where multiple Visual Studio Code instances would make the extension install/update fail. [#3686](https://github.com/microsoft/kiota/issues/3686) - Fixed a bug where models properties named "additionalData" or "backingstore" would be ignored. [#4224](https://github.com/microsoft/kiota/issues/4224) - Fixed a bug where multiple allOf entries type would not get merged if they were part of a discriminator. [#4325](https://github.com/microsoft/kiota/issues/4325) +- Fixed a bug where allOf structure with one entry and properties would not be considered as inheritance case. [#4346](https://github.com/microsoft/kiota/issues/4346) - PREVIEW: Renamed the config commands to workspace. [#4310](https://github.com/microsoft/kiota/issues/4310) - PREVIEW: Moved preview configuration files to the .kiota directory. [#4310](https://github.com/microsoft/kiota/issues/4310) - PREVIEW: Moved the copy descriptions to dedicated folders. [#4310](https://github.com/microsoft/kiota/issues/4310) diff --git a/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs b/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs index b0bba5e344..d1e647a223 100644 --- a/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs +++ b/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs @@ -10,41 +10,37 @@ public static class OpenApiSchemaExtensions { private static readonly Func> classNamesFlattener = x => (x.AnyOf ?? Enumerable.Empty()).Union(x.AllOf).Union(x.OneOf).ToList(); - public static IEnumerable GetSchemaNames(this OpenApiSchema schema) + public static IEnumerable GetSchemaNames(this OpenApiSchema schema, bool directOnly = false) { if (schema == null) - return Enumerable.Empty(); - if (schema.Items != null) + return []; + if (!directOnly && schema.Items != null) return schema.Items.GetSchemaNames(); if (!string.IsNullOrEmpty(schema.Reference?.Id)) - return new[] { schema.Reference.Id.Split('/')[^1].Split('.')[^1] }; - if (schema.AnyOf.Any()) + return [schema.Reference.Id.Split('/')[^1].Split('.')[^1]]; + if (!directOnly && schema.AnyOf.Any()) return schema.AnyOf.FlattenIfRequired(classNamesFlattener); - if (schema.AllOf.Any()) + if (!directOnly && schema.AllOf.Any()) return schema.AllOf.FlattenIfRequired(classNamesFlattener); - if (schema.OneOf.Any()) + if (!directOnly && schema.OneOf.Any()) return schema.OneOf.FlattenIfRequired(classNamesFlattener); - if (!string.IsNullOrEmpty(schema.Title)) - return new[] { schema.Title }; - if (!string.IsNullOrEmpty(schema.Xml?.Name)) - return new[] { schema.Xml.Name }; - return Enumerable.Empty(); + return []; } internal static IEnumerable FlattenSchemaIfRequired(this IList schemas, Func> subsequentGetter) { - if (schemas is null) return Enumerable.Empty(); + if (schemas is null) return []; return schemas.Count == 1 ? schemas.FlattenEmptyEntries(subsequentGetter, 1) : schemas; } private static IEnumerable FlattenIfRequired(this IList schemas, Func> subsequentGetter) { - return schemas.FlattenSchemaIfRequired(subsequentGetter).Where(static x => !string.IsNullOrEmpty(x.Title)).Select(static x => x.Title); + return schemas.FlattenSchemaIfRequired(subsequentGetter).SelectMany(static x => x.GetSchemaNames()); } - public static string GetSchemaName(this OpenApiSchema schema) + public static string GetSchemaName(this OpenApiSchema schema, bool directOnly = false) { - return schema.GetSchemaNames().LastOrDefault()?.TrimStart('$') ?? string.Empty;// OData $ref + return schema.GetSchemaNames(directOnly).LastOrDefault()?.TrimStart('$') ?? string.Empty;// OData $ref } public static bool IsReferencedSchema(this OpenApiSchema schema) @@ -173,7 +169,7 @@ public static IEnumerable GetSchemaReferenceIds(this OpenApiSchema schem return result.Distinct(); } - return Enumerable.Empty(); + return []; } private static IEnumerable FlattenEmptyEntries(this IEnumerable schemas, Func> subsequentGetter, int? maxDepth = default) { @@ -188,7 +184,7 @@ private static IEnumerable FlattenEmptyEntries(this IEnumerable 0) diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index 24f8aa9c88..cdd8849783 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -1550,18 +1550,18 @@ private CodeType CreateModelDeclarationAndType(OpenApiUrlTreeNode currentNode, O } private CodeType CreateInheritedModelDeclaration(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, OpenApiOperation? operation, string classNameSuffix, CodeNamespace codeNamespace, bool isRequestBody, string typeNameForInlineSchema) { - var allOfs = (schema.IsSemanticallyMeaningful() ? new OpenApiSchema[] { schema } : []).Union(schema.AllOf.FlattenSchemaIfRequired(static x => x.AllOf)); + var rootSchemaIsMeaningful = schema.IsSemanticallyMeaningful(); + var allOfs = (rootSchemaIsMeaningful ? new OpenApiSchema[] { schema } : []).Union(schema.AllOf.FlattenSchemaIfRequired(static x => x.AllOf)); CodeElement? codeDeclaration = null; - var className = string.Empty; var codeNamespaceFromParent = GetShortestNamespace(codeNamespace, schema); foreach (var currentSchema in allOfs) { var referenceId = GetReferenceIdFromOriginalSchema(currentSchema, schema); var shortestNamespaceName = GetModelsNamespaceNameFromReferenceId(referenceId); var shortestNamespace = string.IsNullOrEmpty(referenceId) ? codeNamespaceFromParent : rootNamespace?.FindOrAddNamespace(shortestNamespaceName); - className = (currentSchema.GetSchemaName() is string cName && !string.IsNullOrEmpty(cName) ? + var className = (currentSchema.GetSchemaName(rootSchemaIsMeaningful && currentSchema == schema) is string cName && !string.IsNullOrEmpty(cName) ? cName : - (string.IsNullOrEmpty(className) && !string.IsNullOrEmpty(typeNameForInlineSchema) ? + (!string.IsNullOrEmpty(typeNameForInlineSchema) ? typeNameForInlineSchema : currentNode.GetClassName(config.StructuredMimeTypes, operation: operation, suffix: classNameSuffix, schema: currentSchema, requestBody: isRequestBody))) .CleanupSymbolName(); @@ -1919,6 +1919,7 @@ private void TrimInheritedModels() if (relatedModels.Contains(x) || classesInUse.Contains(x)) return; if (x is CodeClass currentClass) { + //TODO this is trimming mailboxsettingsbase when it shouldn't. Most likely because one of the indices is now broken var parents = currentClass.GetInheritanceTree(false, false); if (parents.Any(y => classesDirectlyInUse.Contains(y))) return; // to support the inheritance recursive downcast foreach (var baseClass in parents) // discriminator might also be in grand parent types diff --git a/tests/Kiota.Builder.Tests/Extensions/OpenApiSchemaExtensionsTests.cs b/tests/Kiota.Builder.Tests/Extensions/OpenApiSchemaExtensionsTests.cs index 19accd89df..41732417ba 100644 --- a/tests/Kiota.Builder.Tests/Extensions/OpenApiSchemaExtensionsTests.cs +++ b/tests/Kiota.Builder.Tests/Extensions/OpenApiSchemaExtensionsTests.cs @@ -84,87 +84,179 @@ public void LocalReferencesAreSupported() Assert.True(mockSchema.IsReferencedSchema()); } [Fact] - public void GetSchemaNameAllOf() + public void GetSchemaNameAllOfTitleEmpty() { var schema = new OpenApiSchema { - AllOf = new List { + AllOf = [ new() { Title = "microsoft.graph.entity" }, new() { Title = "microsoft.graph.user" } - } + ] }; var names = schema.GetSchemaNames(); - Assert.Contains("microsoft.graph.entity", names); - Assert.Contains("microsoft.graph.user", names); - Assert.Equal("microsoft.graph.user", schema.GetSchemaName()); + Assert.Empty(names); + Assert.Empty(schema.GetSchemaName()); } [Fact] - public void GetSchemaNameAllOfNested() + public void GetSchemaNameAllOfReference() { var schema = new OpenApiSchema { - AllOf = new List { + AllOf = [ new() { - AllOf = new List { + Reference = new() { + Id = "microsoft.graph.entity" + } + }, + new() { + Reference = new() { + Id = "microsoft.graph.user" + } + } + ] + }; + var names = schema.GetSchemaNames(); + Assert.Contains("entity", names); + Assert.Contains("user", names); + Assert.Equal("user", schema.GetSchemaName()); + } + [Fact] + public void GetSchemaNameAllOfNestedTitleEmpty() + { + var schema = new OpenApiSchema + { + AllOf = [ + new() { + AllOf = [ new() { Title = "microsoft.graph.entity" }, new() { Title = "microsoft.graph.user" } - } + ] } - } + ] }; var names = schema.GetSchemaNames(); - Assert.Contains("microsoft.graph.entity", names); - Assert.Contains("microsoft.graph.user", names); - Assert.Equal("microsoft.graph.user", schema.GetSchemaName()); + Assert.Empty(names); + Assert.Empty(schema.GetSchemaName()); } [Fact] - public void GetSchemaNameAnyOf() + public void GetSchemaNameAllOfNestedReference() { var schema = new OpenApiSchema { - AnyOf = new List { + AllOf = [ + new() { + AllOf = [ + new() { + Reference = new() { + Id = "microsoft.graph.entity" + } + }, + new() { + Reference = new() { + Id = "microsoft.graph.user" + } + } + ] + } + ] + }; + var names = schema.GetSchemaNames(); + Assert.Contains("entity", names); + Assert.Contains("user", names); + Assert.Equal("user", schema.GetSchemaName()); + } + [Fact] + public void GetSchemaNameAnyOfTitleEmpty() + { + var schema = new OpenApiSchema + { + AnyOf = [ new() { Title = "microsoft.graph.entity" }, new() { Title = "microsoft.graph.user" } - } + ] }; var names = schema.GetSchemaNames(); - Assert.Contains("microsoft.graph.entity", names); - Assert.Contains("microsoft.graph.user", names); - Assert.Equal("microsoft.graph.user", schema.GetSchemaName()); + Assert.Empty(names); + Assert.Empty(schema.GetSchemaName()); } [Fact] - public void GetSchemaNameOneOf() + public void GetSchemaNameAnyOfReference() { var schema = new OpenApiSchema { - OneOf = new List { + AnyOf = [ + new() { + Reference = new() { + Id = "microsoft.graph.entity" + } + }, + new() { + Reference = new() { + Id = "microsoft.graph.user" + } + } + ] + }; + var names = schema.GetSchemaNames(); + Assert.Contains("entity", names); + Assert.Contains("user", names); + Assert.Equal("user", schema.GetSchemaName()); + } + [Fact] + public void GetSchemaNameOneOfTitleEmpty() + { + var schema = new OpenApiSchema + { + OneOf = [ new() { Title = "microsoft.graph.entity" }, new() { Title = "microsoft.graph.user" } - } + ] }; var names = schema.GetSchemaNames(); - Assert.Contains("microsoft.graph.entity", names); - Assert.Contains("microsoft.graph.user", names); - Assert.Equal("microsoft.graph.user", schema.GetSchemaName()); + Assert.Empty(names); + Assert.Empty(schema.GetSchemaName()); + } + [Fact] + public void GetSchemaNameOneOfReference() + { + var schema = new OpenApiSchema + { + OneOf = [ + new() { + Reference = new() { + Id = "microsoft.graph.entity" + } + }, + new() { + Reference = new() { + Id = "microsoft.graph.user" + } + } + ] + }; + var names = schema.GetSchemaNames(); + Assert.Contains("entity", names); + Assert.Contains("user", names); + Assert.Equal("user", schema.GetSchemaName()); } [Fact] - public void GetSchemaNameItems() + public void GetSchemaNameItemsTitleEmpty() { var schema = new OpenApiSchema { @@ -174,20 +266,51 @@ public void GetSchemaNameItems() }, }; var names = schema.GetSchemaNames(); - Assert.Contains("microsoft.graph.entity", names); - Assert.Equal("microsoft.graph.entity", schema.GetSchemaName()); + Assert.Empty(names); + Assert.Empty(schema.GetSchemaName()); + } + [Fact] + public void GetSchemaNameItemsReference() + { + var schema = new OpenApiSchema + { + Items = new() + { + Reference = new() + { + Id = "microsoft.graph.entity" + } + }, + }; + var names = schema.GetSchemaNames(); + Assert.Contains("entity", names); + Assert.Equal("entity", schema.GetSchemaName()); Assert.Single(names); } [Fact] - public void GetSchemaNameTitle() + public void GetSchemaNameTitleEmpty() { var schema = new OpenApiSchema { Title = "microsoft.graph.entity" }; var names = schema.GetSchemaNames(); - Assert.Contains("microsoft.graph.entity", names); - Assert.Equal("microsoft.graph.entity", schema.GetSchemaName()); + Assert.Empty(names); + Assert.Empty(schema.GetSchemaName()); + } + [Fact] + public void GetSchemaNameReference() + { + var schema = new OpenApiSchema + { + Reference = new() + { + Id = "microsoft.graph.entity" + } + }; + var names = schema.GetSchemaNames(); + Assert.Contains("entity", names); + Assert.Equal("entity", schema.GetSchemaName()); Assert.Single(names); } [Fact] diff --git a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs index ca1a16ec12..6dec359fd0 100644 --- a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs +++ b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs @@ -7472,6 +7472,104 @@ public async Task InheritanceWithAllOfWith3Parts3Schema() Assert.Single(resultClass.Properties.Where(static x => x.Name.Equals("groupprop2", StringComparison.OrdinalIgnoreCase))); } [Fact] + public async Task InheritanceWithAllOfWith2Parts1Schema1InlineNoDiscriminator() + { + var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + await using var fs = await GetDocumentStream(@"openapi: 3.0.1 +info: + title: OData Service for namespace microsoft.graph + description: This OData service is located at https://graph.microsoft.com/v1.0 + version: 1.0.1 +servers: + - url: https://graph.microsoft.com/v1.0 +paths: + /group: + get: + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/microsoft.graph.group' +components: + schemas: + microsoft.graph.directoryObject: + title: 'directoryObject' + required: ['@odata.type'] + type: 'object' + properties: + '@odata.type': + type: 'string' + default: '#microsoft.graph.directoryObject' + microsoft.graph.group: + type: 'object' + allOf: + - '$ref': '#/components/schemas/microsoft.graph.directoryObject' + - title: 'group part 1' + type: 'object' + properties: + groupprop1: + type: 'string'"); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath }, _httpClient); + var document = await builder.CreateOpenApiDocumentAsync(fs); + var node = builder.CreateUriSpace(document); + var codeModel = builder.CreateSourceModel(node); + var resultClass = codeModel.FindChildByName("Group"); + Assert.NotNull(resultClass); + Assert.Equal("directoryObject", resultClass.StartBlock.Inherits?.Name, StringComparer.OrdinalIgnoreCase); + Assert.Single(resultClass.Properties); + Assert.Single(resultClass.Properties.Where(static x => x.Name.Equals("groupprop1", StringComparison.OrdinalIgnoreCase))); + } + [Fact] + public async Task InheritanceWithAllOfWith1Part1SchemaAndPropertiesNoDiscriminator() + { + var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + await using var fs = await GetDocumentStream(@"openapi: 3.0.1 +info: + title: OData Service for namespace microsoft.graph + description: This OData service is located at https://graph.microsoft.com/v1.0 + version: 1.0.1 +servers: + - url: https://graph.microsoft.com/v1.0 +paths: + /group: + get: + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/microsoft.graph.group' +components: + schemas: + microsoft.graph.directoryObject: + title: 'directoryObject' + required: ['@odata.type'] + type: 'object' + properties: + '@odata.type': + type: 'string' + default: '#microsoft.graph.directoryObject' + microsoft.graph.group: + allOf: + - '$ref': '#/components/schemas/microsoft.graph.directoryObject' + type: 'object' + properties: + groupprop1: + type: 'string'"); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath }, _httpClient); + var document = await builder.CreateOpenApiDocumentAsync(fs); + var node = builder.CreateUriSpace(document); + var codeModel = builder.CreateSourceModel(node); + var resultClass = codeModel.FindChildByName("Group"); + Assert.NotNull(resultClass); + Assert.Equal("directoryObject", resultClass.StartBlock.Inherits?.Name, StringComparer.OrdinalIgnoreCase); + Assert.Single(resultClass.Properties); + Assert.Single(resultClass.Properties.Where(static x => x.Name.Equals("groupprop1", StringComparison.OrdinalIgnoreCase))); + } + [Fact] public async Task InheritanceWithAllOfWith3Parts1Schema2Inline() { var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); From 596073b1ca08d602b8dc3cbf924a2cd05ee071ad Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 21 Mar 2024 17:12:44 -0400 Subject: [PATCH 03/16] - fixes formatting Signed-off-by: Vincent Biret --- .../OpenApiSchemaExtensionsTests.cs | 78 ++++++++++++------- 1 file changed, 52 insertions(+), 26 deletions(-) diff --git a/tests/Kiota.Builder.Tests/Extensions/OpenApiSchemaExtensionsTests.cs b/tests/Kiota.Builder.Tests/Extensions/OpenApiSchemaExtensionsTests.cs index 41732417ba..9077ecbe49 100644 --- a/tests/Kiota.Builder.Tests/Extensions/OpenApiSchemaExtensionsTests.cs +++ b/tests/Kiota.Builder.Tests/Extensions/OpenApiSchemaExtensionsTests.cs @@ -89,10 +89,12 @@ public void GetSchemaNameAllOfTitleEmpty() var schema = new OpenApiSchema { AllOf = [ - new() { + new() + { Title = "microsoft.graph.entity" }, - new() { + new() + { Title = "microsoft.graph.user" } ] @@ -107,13 +109,17 @@ public void GetSchemaNameAllOfReference() var schema = new OpenApiSchema { AllOf = [ - new() { - Reference = new() { + new() + { + Reference = new() + { Id = "microsoft.graph.entity" } }, - new() { - Reference = new() { + new() + { + Reference = new() + { Id = "microsoft.graph.user" } } @@ -130,12 +136,15 @@ public void GetSchemaNameAllOfNestedTitleEmpty() var schema = new OpenApiSchema { AllOf = [ - new() { + new() + { AllOf = [ - new() { + new() + { Title = "microsoft.graph.entity" }, - new() { + new() + { Title = "microsoft.graph.user" } ] @@ -152,15 +161,20 @@ public void GetSchemaNameAllOfNestedReference() var schema = new OpenApiSchema { AllOf = [ - new() { + new() + { AllOf = [ - new() { - Reference = new() { + new() + { + Reference = new() + { Id = "microsoft.graph.entity" } }, - new() { - Reference = new() { + new() + { + Reference = new() + { Id = "microsoft.graph.user" } } @@ -179,10 +193,12 @@ public void GetSchemaNameAnyOfTitleEmpty() var schema = new OpenApiSchema { AnyOf = [ - new() { + new() + { Title = "microsoft.graph.entity" }, - new() { + new() + { Title = "microsoft.graph.user" } ] @@ -197,13 +213,17 @@ public void GetSchemaNameAnyOfReference() var schema = new OpenApiSchema { AnyOf = [ - new() { - Reference = new() { + new() + { + Reference = new() + { Id = "microsoft.graph.entity" } }, - new() { - Reference = new() { + new() + { + Reference = new() + { Id = "microsoft.graph.user" } } @@ -220,10 +240,12 @@ public void GetSchemaNameOneOfTitleEmpty() var schema = new OpenApiSchema { OneOf = [ - new() { + new() + { Title = "microsoft.graph.entity" }, - new() { + new() + { Title = "microsoft.graph.user" } ] @@ -238,13 +260,17 @@ public void GetSchemaNameOneOfReference() var schema = new OpenApiSchema { OneOf = [ - new() { - Reference = new() { + new() + { + Reference = new() + { Id = "microsoft.graph.entity" } }, - new() { - Reference = new() { + new() + { + Reference = new() + { Id = "microsoft.graph.user" } } From 3d164a4deb50e0e54c3260f975ae4affb2c66069 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 2 May 2024 14:09:15 -0400 Subject: [PATCH 04/16] - moves changelog entry to the right level Signed-off-by: Vincent Biret --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3d512eb02..290e62705f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Put opening brace after property definition on new line, if property has getter and setter [#4625](https://github.com/microsoft/kiota/issues/4625) - Put spaces correctly around dictionary entries [#4625](https://github.com/microsoft/kiota/issues/4625) - Remove trailing space after class definition [#4625](https://github.com/microsoft/kiota/issues/4625) +- Fixed a bug where multiple allOf entries type would not get merged if they were part of a discriminator. [#4325](https://github.com/microsoft/kiota/issues/4325) +- Fixed a bug where allOf structure with one entry and properties would not be considered as inheritance case. [#4346](https://github.com/microsoft/kiota/issues/4346) ## [1.14.0] - 2024-05-02 @@ -64,8 +66,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed Go generation bug when dealing with scalar collection body. [#4380](https://github.com/microsoft/kiota/issues/4380) - Fixed a bug where multiple Visual Studio Code instances would make the extension install/update fail. [#3686](https://github.com/microsoft/kiota/issues/3686) - Fixed a bug where models properties named "additionalData" or "backingstore" would be ignored. [#4224](https://github.com/microsoft/kiota/issues/4224) -- Fixed a bug where multiple allOf entries type would not get merged if they were part of a discriminator. [#4325](https://github.com/microsoft/kiota/issues/4325) -- Fixed a bug where allOf structure with one entry and properties would not be considered as inheritance case. [#4346](https://github.com/microsoft/kiota/issues/4346) - PREVIEW: Renamed the config commands to workspace. [#4310](https://github.com/microsoft/kiota/issues/4310) - PREVIEW: Moved preview configuration files to the .kiota directory. [#4310](https://github.com/microsoft/kiota/issues/4310) - PREVIEW: Moved the copy descriptions to dedicated folders. [#4310](https://github.com/microsoft/kiota/issues/4310) From c1e20a98da5671844e10afea7768aa793b942ace Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 2 May 2024 14:56:32 -0400 Subject: [PATCH 05/16] - fixes inheritance trimming Signed-off-by: Vincent Biret --- .../Extensions/OpenApiSchemaExtensions.cs | 13 +++++++------ src/Kiota.Builder/KiotaBuilder.cs | 9 ++++++--- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs b/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs index d1e647a223..2cf9b5eb75 100644 --- a/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs +++ b/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs @@ -29,7 +29,7 @@ public static IEnumerable GetSchemaNames(this OpenApiSchema schema, bool internal static IEnumerable FlattenSchemaIfRequired(this IList schemas, Func> subsequentGetter) { if (schemas is null) return []; - return schemas.Count == 1 ? + return schemas.Count == 1 && !schemas[0].Properties.Any() ? schemas.FlattenEmptyEntries(subsequentGetter, 1) : schemas; } @@ -57,7 +57,7 @@ public static bool IsArray(this OpenApiSchema? schema) (schema.Items.IsComposedEnum() || schema.Items.IsEnum() || schema.Items.IsSemanticallyMeaningful() || - FlattenEmptyEntries(new OpenApiSchema[] { schema.Items }, static x => x.AnyOf.Union(x.AllOf).Union(x.OneOf).ToList(), 1).FirstOrDefault() is OpenApiSchema flat && flat.IsSemanticallyMeaningful()); + FlattenEmptyEntries([schema.Items], static x => x.AnyOf.Union(x.AllOf).Union(x.OneOf).ToList(), 1).FirstOrDefault() is OpenApiSchema flat && flat.IsSemanticallyMeaningful()); } public static bool IsObject(this OpenApiSchema? schema) @@ -73,10 +73,11 @@ public static bool IsInclusiveUnion(this OpenApiSchema? schema) public static bool IsInherited(this OpenApiSchema? schema) { if (schema is null) return false; - var meaningfulSchemas = schema.AllOf.FlattenSchemaIfRequired(static x => x.AllOf).Where(static x => x.IsSemanticallyMeaningful()).ToArray(); - return meaningfulSchemas.Count(static x => !string.IsNullOrEmpty(x.Reference?.Id)) == 1 && - (meaningfulSchemas.Count(static x => string.IsNullOrEmpty(x.Reference?.Id)) == 1 || - schema.IsSemanticallyMeaningful()); + var meaningfulMemberSchemas = schema.AllOf.FlattenSchemaIfRequired(static x => x.AllOf).Where(static x => x.IsSemanticallyMeaningful()).ToArray(); + var isRootSchemaMeaningful = schema.IsSemanticallyMeaningful(); + return meaningfulMemberSchemas.Count(static x => !string.IsNullOrEmpty(x.Reference?.Id)) == 1 && + (meaningfulMemberSchemas.Count(static x => string.IsNullOrEmpty(x.Reference?.Id)) == 1 || + isRootSchemaMeaningful); } internal static OpenApiSchema? MergeIntersectionSchemaEntries(this OpenApiSchema? schema) diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index cdd8849783..55d2399e12 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -1550,8 +1550,11 @@ private CodeType CreateModelDeclarationAndType(OpenApiUrlTreeNode currentNode, O } private CodeType CreateInheritedModelDeclaration(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, OpenApiOperation? operation, string classNameSuffix, CodeNamespace codeNamespace, bool isRequestBody, string typeNameForInlineSchema) { - var rootSchemaIsMeaningful = schema.IsSemanticallyMeaningful(); - var allOfs = (rootSchemaIsMeaningful ? new OpenApiSchema[] { schema } : []).Union(schema.AllOf.FlattenSchemaIfRequired(static x => x.AllOf)); + var isRootSchemaMeaningful = schema.IsSemanticallyMeaningful(); + var allOfs = schema.AllOf.FlattenSchemaIfRequired(static x => x.AllOf) + .Union(isRootSchemaMeaningful ? + [schema] : + Array.Empty()); CodeElement? codeDeclaration = null; var codeNamespaceFromParent = GetShortestNamespace(codeNamespace, schema); foreach (var currentSchema in allOfs) @@ -1559,7 +1562,7 @@ private CodeType CreateInheritedModelDeclaration(OpenApiUrlTreeNode currentNode, var referenceId = GetReferenceIdFromOriginalSchema(currentSchema, schema); var shortestNamespaceName = GetModelsNamespaceNameFromReferenceId(referenceId); var shortestNamespace = string.IsNullOrEmpty(referenceId) ? codeNamespaceFromParent : rootNamespace?.FindOrAddNamespace(shortestNamespaceName); - var className = (currentSchema.GetSchemaName(rootSchemaIsMeaningful && currentSchema == schema) is string cName && !string.IsNullOrEmpty(cName) ? + var className = (currentSchema.GetSchemaName(isRootSchemaMeaningful && currentSchema == schema) is string cName && !string.IsNullOrEmpty(cName) ? cName : (!string.IsNullOrEmpty(typeNameForInlineSchema) ? typeNameForInlineSchema : From bd7e3ea6d9b77dc5a41c8aeff38bf9c6631bab37 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Tue, 7 May 2024 13:28:14 -0400 Subject: [PATCH 06/16] - fixes inheritance from intersection type Signed-off-by: Vincent Biret --- .../Extensions/OpenApiSchemaExtensions.cs | 8 +- src/Kiota.Builder/KiotaBuilder.cs | 29 +++- .../Kiota.Builder.Tests/KiotaBuilderTests.cs | 132 ++++++++++++++---- 3 files changed, 130 insertions(+), 39 deletions(-) diff --git a/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs b/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs index 2cf9b5eb75..4db49e2874 100644 --- a/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs +++ b/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs @@ -80,13 +80,17 @@ public static bool IsInherited(this OpenApiSchema? schema) isRootSchemaMeaningful); } - internal static OpenApiSchema? MergeIntersectionSchemaEntries(this OpenApiSchema? schema) + internal static OpenApiSchema? MergeIntersectionSchemaEntries(this OpenApiSchema? schema, HashSet? schemasToExclude = default) { if (schema is null) return null; if (!schema.IsIntersection()) return schema; var result = new OpenApiSchema(schema); result.AllOf.Clear(); - var meaningfulSchemas = schema.AllOf.Where(static x => x.IsSemanticallyMeaningful()).Select(MergeIntersectionSchemaEntries).Where(x => x is not null).OfType(); + var meaningfulSchemas = schema.AllOf + .Where(static x => x.IsSemanticallyMeaningful()) + .Select(x => MergeIntersectionSchemaEntries(x, schemasToExclude)) + .Where(x => x is not null && (schemasToExclude is null || !schemasToExclude.Contains(x))) + .OfType(); meaningfulSchemas.SelectMany(static x => x.Properties).ToList().ForEach(x => result.Properties.TryAdd(x.Key, x.Value)); return result; } diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index 55d2399e12..ebf216a63f 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -1554,9 +1554,11 @@ private CodeType CreateInheritedModelDeclaration(OpenApiUrlTreeNode currentNode, var allOfs = schema.AllOf.FlattenSchemaIfRequired(static x => x.AllOf) .Union(isRootSchemaMeaningful ? [schema] : - Array.Empty()); - CodeElement? codeDeclaration = null; + Array.Empty()) + .ToArray(); + CodeClass? codeDeclaration = null; var codeNamespaceFromParent = GetShortestNamespace(codeNamespace, schema); + OpenApiSchema? previousSchema = null; foreach (var currentSchema in allOfs) { var referenceId = GetReferenceIdFromOriginalSchema(currentSchema, schema); @@ -1568,10 +1570,14 @@ private CodeType CreateInheritedModelDeclaration(OpenApiUrlTreeNode currentNode, typeNameForInlineSchema : currentNode.GetClassName(config.StructuredMimeTypes, operation: operation, suffix: classNameSuffix, schema: currentSchema, requestBody: isRequestBody))) .CleanupSymbolName(); - if (shortestNamespace != null) - codeDeclaration = AddModelDeclarationIfDoesntExist(currentNode, currentSchema, className, shortestNamespace, codeDeclaration as CodeClass); + if (shortestNamespace != null && + AddModelDeclarationIfDoesntExist(currentNode, currentSchema, className, shortestNamespace, codeDeclaration, previousSchema) is CodeClass addedDeclaration) + { + codeDeclaration = addedDeclaration; + previousSchema = currentSchema; + } } - if (codeDeclaration is CodeClass currentClass && + if (codeDeclaration is { } currentClass && !currentClass.Documentation.DescriptionAvailable && string.IsNullOrEmpty(schema.AllOf.LastOrDefault()?.Description) && !string.IsNullOrEmpty(schema.Description)) @@ -1741,13 +1747,22 @@ private CodeNamespace GetSearchNamespace(OpenApiUrlTreeNode currentNode, CodeNam return currentNamespace; } private ConcurrentDictionary classLifecycles = new(StringComparer.OrdinalIgnoreCase); - private CodeElement AddModelDeclarationIfDoesntExist(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, string declarationName, CodeNamespace currentNamespace, CodeClass? inheritsFrom = null) + private CodeElement AddModelDeclarationIfDoesntExist(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, string declarationName, CodeNamespace currentNamespace, CodeClass? inheritsFrom = null, OpenApiSchema? parentSchemaToExcludeForIntersections = null) { if (GetExistingDeclaration(currentNamespace, currentNode, declarationName) is not CodeElement existingDeclaration) // we can find it in the components { if (AddEnumDeclaration(currentNode, schema, declarationName, currentNamespace) is CodeEnum enumDeclaration) return enumDeclaration; + if (schema.IsIntersection() && + (parentSchemaToExcludeForIntersections is null ? + schema.MergeIntersectionSchemaEntries() : + schema.MergeIntersectionSchemaEntries([parentSchemaToExcludeForIntersections])) is OpenApiSchema mergedSchema && + AddModelDeclarationIfDoesntExist(currentNode, mergedSchema, declarationName, currentNamespace, inheritsFrom) is CodeClass createdClass) + { + // multiple allOf entries that do not translate to inheritance + return createdClass; + } return AddModelClass(currentNode, schema, declarationName, currentNamespace, inheritsFrom); } return existingDeclaration; @@ -2077,7 +2092,7 @@ internal static void AddDiscriminatorMethod(CodeClass newClass, string discrimin logger.LogWarning("Discriminator {ComponentKey} is not inherited from {ClassName}.", componentKey, baseClass.Name); return null; } - var codeClass = AddModelDeclarationIfDoesntExist(currentNode, discriminatorSchema, className, GetShortestNamespace(currentNamespace, discriminatorSchema), shouldInherit ? baseClass : null); + var codeClass = AddModelDeclarationIfDoesntExist(currentNode, discriminatorSchema, className, GetShortestNamespace(currentNamespace, discriminatorSchema), shouldInherit ? baseClass : null, currentSchema); return new CodeType { TypeDefinition = codeClass, diff --git a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs index 6dec359fd0..a093746039 100644 --- a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs +++ b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs @@ -7396,7 +7396,7 @@ public async Task InheritanceWithAllOfInBaseType() Assert.NotNull(codeModel.FindChildByName("Group")); } [Fact] - public async Task InheritanceWithAllOfWith3Parts3Schema() + public async Task InheritanceWithAllOfWith3Parts3SchemaChildClass() { var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); await using var fs = await GetDocumentStream(@"openapi: 3.0.1 @@ -7426,9 +7426,7 @@ public async Task InheritanceWithAllOfWith3Parts3Schema() components: schemas: microsoft.graph.directoryObject: - title: 'directoryObject' required: ['@odata.type'] - type: 'object' properties: '@odata.type': type: 'string' @@ -7438,16 +7436,12 @@ public async Task InheritanceWithAllOfWith3Parts3Schema() mapping: '#microsoft.graph.group': '#/components/schemas/microsoft.graph.group' microsoft.graph.groupFacet1: - title: 'group part 1' - type: 'object' properties: - groupprop1: + facetprop1: type: 'string' microsoft.graph.groupFacet2: - title: 'group part 2' - type: 'object' properties: - groupprop2: + facetprop2: type: 'string' microsoft.graph.group: title: 'group' @@ -7461,15 +7455,95 @@ public async Task InheritanceWithAllOfWith3Parts3Schema() var node = builder.CreateUriSpace(document); var codeModel = builder.CreateSourceModel(node); var directoryObjectClass = codeModel.FindChildByName("DirectoryObject"); + Assert.Null(directoryObjectClass.StartBlock.Inherits); Assert.NotNull(directoryObjectClass); - var resultClass = codeModel.FindChildByName("Group"); - Assert.NotNull(resultClass); - Assert.Equal(4, resultClass.Properties.Count()); - Assert.Null(resultClass.StartBlock.Inherits); - Assert.Single(resultClass.Properties.Where(static x => x.Kind is CodePropertyKind.AdditionalData)); - Assert.Single(resultClass.Properties.Where(static x => x.Name.Equals("oDataType", StringComparison.OrdinalIgnoreCase))); - Assert.Single(resultClass.Properties.Where(static x => x.Name.Equals("groupprop1", StringComparison.OrdinalIgnoreCase))); - Assert.Single(resultClass.Properties.Where(static x => x.Name.Equals("groupprop2", StringComparison.OrdinalIgnoreCase))); + var groupClass = codeModel.FindChildByName("Group"); + Assert.NotNull(groupClass); + Assert.Equal(4, groupClass.Properties.Count()); + Assert.Null(groupClass.StartBlock.Inherits); + Assert.Single(groupClass.Properties.Where(static x => x.Kind is CodePropertyKind.AdditionalData)); + Assert.Single(groupClass.Properties.Where(static x => x.Name.Equals("oDataType", StringComparison.OrdinalIgnoreCase))); + Assert.Single(groupClass.Properties.Where(static x => x.Name.Equals("facetprop1", StringComparison.OrdinalIgnoreCase))); + Assert.Single(groupClass.Properties.Where(static x => x.Name.Equals("facetprop2", StringComparison.OrdinalIgnoreCase))); + } + [Fact] + public async Task InheritanceWithAllOfWith3Parts3SchemaParentClass() + { + var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + await using var fs = await GetDocumentStream(@"openapi: 3.0.1 +info: + title: OData Service for namespace microsoft.graph + description: This OData service is located at https://graph.microsoft.com/v1.0 + version: 1.0.1 +servers: + - url: https://graph.microsoft.com/v1.0 +paths: + /directoryObject: + get: + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/microsoft.graph.directoryObject' + /group: + get: + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/microsoft.graph.group' +components: + schemas: + microsoft.graph.directoryObject: + required: ['@odata.type'] + properties: + '@odata.type': + type: 'string' + default: '#microsoft.graph.directoryObject' + allOf: + - '$ref': '#/components/schemas/microsoft.graph.directoryObjectFacet1' + - '$ref': '#/components/schemas/microsoft.graph.directoryObjectFacet2' + discriminator: + propertyName: '@odata.type' + mapping: + '#microsoft.graph.group': '#/components/schemas/microsoft.graph.group' + microsoft.graph.directoryObjectFacet1: + properties: + facetprop1: + type: 'string' + microsoft.graph.directoryObjectFacet2: + properties: + facetprop2: + type: 'string' + microsoft.graph.group: + allOf: + - '$ref': '#/components/schemas/microsoft.graph.directoryObject' + properties: + groupprop1: + type: 'string'"); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath }, _httpClient); + var document = await builder.CreateOpenApiDocumentAsync(fs); + var node = builder.CreateUriSpace(document); + var codeModel = builder.CreateSourceModel(node); + var directoryObjectClass = codeModel.FindChildByName("DirectoryObject"); + Assert.NotNull(directoryObjectClass); + Assert.Null(directoryObjectClass.StartBlock.Inherits); + Assert.Single(directoryObjectClass.Properties.Where(static x => x.Kind is CodePropertyKind.AdditionalData)); + Assert.Single(directoryObjectClass.Properties.Where(static x => x.Name.Equals("oDataType", StringComparison.OrdinalIgnoreCase))); + Assert.Single(directoryObjectClass.Properties.Where(static x => x.Name.Equals("facetprop1", StringComparison.OrdinalIgnoreCase))); + Assert.Single(directoryObjectClass.Properties.Where(static x => x.Name.Equals("facetprop2", StringComparison.OrdinalIgnoreCase))); + var groupClass = codeModel.FindChildByName("Group"); + Assert.NotNull(groupClass); + Assert.Single(groupClass.Properties); + Assert.NotNull(groupClass.StartBlock.Inherits); + Assert.Empty(groupClass.Properties.Where(static x => x.Kind is CodePropertyKind.AdditionalData)); + Assert.Empty(groupClass.Properties.Where(static x => x.Name.Equals("oDataType", StringComparison.OrdinalIgnoreCase))); + Assert.Empty(groupClass.Properties.Where(static x => x.Name.Equals("facetprop1", StringComparison.OrdinalIgnoreCase))); + Assert.Empty(groupClass.Properties.Where(static x => x.Name.Equals("facetprop2", StringComparison.OrdinalIgnoreCase))); + Assert.Single(groupClass.Properties.Where(static x => x.Name.Equals("groupprop1", StringComparison.OrdinalIgnoreCase))); } [Fact] public async Task InheritanceWithAllOfWith2Parts1Schema1InlineNoDiscriminator() @@ -7569,8 +7643,10 @@ public async Task InheritanceWithAllOfWith1Part1SchemaAndPropertiesNoDiscriminat Assert.Single(resultClass.Properties); Assert.Single(resultClass.Properties.Where(static x => x.Name.Equals("groupprop1", StringComparison.OrdinalIgnoreCase))); } - [Fact] - public async Task InheritanceWithAllOfWith3Parts1Schema2Inline() + [InlineData(true)] + [InlineData(false)] + [Theory] + public async Task InheritanceWithAllOfWith3Parts1Schema2Inline(bool reverseOrder) { var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); await using var fs = await GetDocumentStream(@"openapi: 3.0.1 @@ -7592,9 +7668,7 @@ public async Task InheritanceWithAllOfWith3Parts1Schema2Inline() components: schemas: microsoft.graph.directoryObject: - title: 'directoryObject' required: ['@odata.type'] - type: 'object' properties: '@odata.type': type: 'string' @@ -7604,18 +7678,16 @@ public async Task InheritanceWithAllOfWith3Parts1Schema2Inline() mapping: '#microsoft.graph.group': '#/components/schemas/microsoft.graph.group' microsoft.graph.group: - allOf: - - '$ref': '#/components/schemas/microsoft.graph.directoryObject' - - title: 'group part 1' - type: 'object' - properties: + allOf:" + + (reverseOrder ? "" : @" + - '$ref': '#/components/schemas/microsoft.graph.directoryObject'") + @" + - properties: groupprop1: type: 'string' - - title: 'group part 2' - type: 'object' - properties: + - properties: groupprop2: - type: 'string'"); + type: 'string'" + (!reverseOrder ? "" : @" + - '$ref': '#/components/schemas/microsoft.graph.directoryObject'")); var mockLogger = new Mock>(); var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath }, _httpClient); var document = await builder.CreateOpenApiDocumentAsync(fs); From 860abd8d4b31ee06541ccf0938e4ea279fa2cc0f Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Tue, 7 May 2024 15:13:41 -0400 Subject: [PATCH 07/16] - fixes #4074 a bug where allof would miss properties without object type Signed-off-by: Vincent Biret --- CHANGELOG.md | 1 + .../Extensions/OpenApiSchemaExtensions.cs | 8 ++- src/Kiota.Builder/KiotaBuilder.cs | 4 +- .../Validation/MissingDiscriminator.cs | 2 +- .../Validation/UrlFormEncodedComplex.cs | 4 +- .../OpenApiSchemaExtensionsTests.cs | 6 ++- .../Kiota.Builder.Tests/KiotaBuilderTests.cs | 49 +++++++++++++++++++ 7 files changed, 65 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 290e62705f..b413a0ef8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Remove trailing space after class definition [#4625](https://github.com/microsoft/kiota/issues/4625) - Fixed a bug where multiple allOf entries type would not get merged if they were part of a discriminator. [#4325](https://github.com/microsoft/kiota/issues/4325) - Fixed a bug where allOf structure with one entry and properties would not be considered as inheritance case. [#4346](https://github.com/microsoft/kiota/issues/4346) +- Fixed a bug where some allOf scenarios would be missing properties if type object wasn't set on the schema. [#4074](https://github.com/microsoft/kiota/issues/4074) ## [1.14.0] - 2024-05-02 diff --git a/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs b/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs index 4db49e2874..be677c9e5c 100644 --- a/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs +++ b/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs @@ -60,10 +60,14 @@ public static bool IsArray(this OpenApiSchema? schema) FlattenEmptyEntries([schema.Items], static x => x.AnyOf.Union(x.AllOf).Union(x.OneOf).ToList(), 1).FirstOrDefault() is OpenApiSchema flat && flat.IsSemanticallyMeaningful()); } - public static bool IsObject(this OpenApiSchema? schema) + public static bool IsObjectType(this OpenApiSchema? schema) { return "object".Equals(schema?.Type, StringComparison.OrdinalIgnoreCase); } + public static bool HasAnyProperty(this OpenApiSchema? schema) + { + return schema?.Properties is { Count: > 0 }; + } public static bool IsInclusiveUnion(this OpenApiSchema? schema) { return schema?.AnyOf?.Count(static x => IsSemanticallyMeaningful(x, true)) > 1; @@ -178,7 +182,7 @@ public static IEnumerable GetSchemaReferenceIds(this OpenApiSchema schem } private static IEnumerable FlattenEmptyEntries(this IEnumerable schemas, Func> subsequentGetter, int? maxDepth = default) { - if (schemas == null) return Enumerable.Empty(); + if (schemas == null) return []; ArgumentNullException.ThrowIfNull(subsequentGetter); if ((maxDepth ?? 1) <= 0) diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index ebf216a63f..26d3ec95d9 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -1698,7 +1698,7 @@ private CodeTypeBase CreateModelDeclarations(OpenApiUrlTreeNode currentNode, Ope return CreateComposedModelDeclaration(currentNode, schema, operation, suffix, codeNamespace, isRequestBody, typeNameForInlineSchema); } - if (schema.IsObject() || schema.Properties.Any() || schema.IsEnum() || !string.IsNullOrEmpty(schema.AdditionalProperties?.Type)) + if (schema.IsObjectType() || schema.HasAnyProperty() || schema.IsEnum() || !string.IsNullOrEmpty(schema.AdditionalProperties?.Type)) { // no inheritance or union type, often empty definitions with only additional properties are used as property bags. return CreateModelDeclarationAndType(currentNode, schema, operation, codeNamespace, suffix, response: responseValue, typeNameForInlineSchema: typeNameForInlineSchema, isRequestBody); @@ -2129,7 +2129,7 @@ private Dictionary CollectAllProperties(OpenApiSchema sch Dictionary result = schema.Properties?.ToDictionary(static x => x.Key, static x => x.Value, StringComparer.Ordinal) ?? new(StringComparer.Ordinal); if (schema.AllOf?.Any() ?? false) { - foreach (var supProperty in schema.AllOf.Where(static x => x.IsObject() && !x.IsReferencedSchema() && x.Properties is not null).SelectMany(static x => x.Properties)) + foreach (var supProperty in schema.AllOf.Where(static x => !x.IsReferencedSchema() && x.HasAnyProperty()).SelectMany(static x => x.Properties)) { result.Add(supProperty.Key, supProperty.Value); } diff --git a/src/Kiota.Builder/Validation/MissingDiscriminator.cs b/src/Kiota.Builder/Validation/MissingDiscriminator.cs index ad12d74ea2..c889f5cf5a 100644 --- a/src/Kiota.Builder/Validation/MissingDiscriminator.cs +++ b/src/Kiota.Builder/Validation/MissingDiscriminator.cs @@ -35,7 +35,7 @@ private static void ValidateSchema(OpenApiSchema schema, IValidationContext cont { if (!schema.IsInclusiveUnion() && !schema.IsExclusiveUnion()) return; - if (schema.AnyOf.All(static x => !x.IsObject()) && schema.OneOf.All(static x => !x.IsObject())) + if (schema.AnyOf.All(static x => !x.IsObjectType()) && schema.OneOf.All(static x => !x.IsObjectType())) return; if (string.IsNullOrEmpty(schema.GetDiscriminatorPropertyName()) || !schema.GetDiscriminatorMappings(idx).Any()) context.CreateWarning(nameof(MissingDiscriminator), $"The schema {address} is a polymorphic type but does not define a discriminator. This will result in a serialization errors."); diff --git a/src/Kiota.Builder/Validation/UrlFormEncodedComplex.cs b/src/Kiota.Builder/Validation/UrlFormEncodedComplex.cs index 0257c53902..78a4b31572 100644 --- a/src/Kiota.Builder/Validation/UrlFormEncodedComplex.cs +++ b/src/Kiota.Builder/Validation/UrlFormEncodedComplex.cs @@ -23,9 +23,9 @@ public UrlFormEncodedComplex() : base(nameof(UrlFormEncodedComplex), static (con private static void ValidateSchema(OpenApiSchema schema, IValidationContext context, string operationId, string schemaName) { if (schema == null) return; - if (!schema.IsObject()) + if (!schema.IsObjectType()) context.CreateWarning(nameof(UrlFormEncodedComplex), $"The operation {operationId} has a {schemaName} which is not an object type. This is not supported by Kiota and serialization will fail."); - if (schema.Properties.Any(static x => x.Value.IsObject())) + if (schema.Properties.Any(static x => x.Value.IsObjectType())) context.CreateWarning(nameof(UrlFormEncodedComplex), $"The operation {operationId} has a {schemaName} with a complex properties and the url form encoded content type. This is not supported by Kiota and serialization of complex properties will fail."); } } diff --git a/tests/Kiota.Builder.Tests/Extensions/OpenApiSchemaExtensionsTests.cs b/tests/Kiota.Builder.Tests/Extensions/OpenApiSchemaExtensionsTests.cs index 9077ecbe49..08d46a13c7 100644 --- a/tests/Kiota.Builder.Tests/Extensions/OpenApiSchemaExtensionsTests.cs +++ b/tests/Kiota.Builder.Tests/Extensions/OpenApiSchemaExtensionsTests.cs @@ -43,17 +43,19 @@ public void Defensive() Assert.False(OpenApiSchemaExtensions.IsInclusiveUnion(null)); Assert.False(OpenApiSchemaExtensions.IsExclusiveUnion(null)); Assert.False(OpenApiSchemaExtensions.IsArray(null)); - Assert.False(OpenApiSchemaExtensions.IsObject(null)); + Assert.False(OpenApiSchemaExtensions.IsObjectType(null)); + Assert.False(OpenApiSchemaExtensions.HasAnyProperty(null)); Assert.False(OpenApiSchemaExtensions.IsReferencedSchema(null)); Assert.Null(OpenApiSchemaExtensions.MergeIntersectionSchemaEntries(null)); Assert.False(new OpenApiSchema { Reference = null }.IsReferencedSchema()); Assert.False(new OpenApiSchema { Type = null }.IsArray()); - Assert.False(new OpenApiSchema { Type = null }.IsObject()); + Assert.False(new OpenApiSchema { Type = null }.IsObjectType()); Assert.False(new OpenApiSchema { AnyOf = null }.IsInclusiveUnion()); Assert.False(new OpenApiSchema { AllOf = null }.IsInherited()); Assert.False(new OpenApiSchema { AllOf = null }.IsIntersection()); Assert.False(new OpenApiSchema { OneOf = null }.IsExclusiveUnion()); + Assert.False(new OpenApiSchema { Properties = null }.HasAnyProperty()); var original = new OpenApiSchema { AllOf = null }; Assert.Equal(original, original.MergeIntersectionSchemaEntries()); diff --git a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs index a093746039..5119e345eb 100644 --- a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs +++ b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs @@ -7702,6 +7702,55 @@ public async Task InheritanceWithAllOfWith3Parts1Schema2Inline(bool reverseOrder Assert.Single(resultClass.Properties.Where(static x => x.Name.Equals("groupprop2", StringComparison.OrdinalIgnoreCase))); } [Fact] + public async Task InheritanceWithoutObjectTypeHasAllProperties() + { + var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + await using var fs = await GetDocumentStream(@"openapi: 3.0.3 +servers: + - url: 'https://example.com' +info: + title: example + version: 0.0.1 +paths: + /path: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/outerPayload' + responses: + '201': + description: Created + content: + application/json: + schema: + type: string + +components: + schemas: + outerPayload: + allOf: + - $ref: '#/components/schemas/innerPayload' + - properties: + someField: + type: string + innerPayload: + properties: + anotherField: + type: string"); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath }, _httpClient); + var document = await builder.CreateOpenApiDocumentAsync(fs); + var node = builder.CreateUriSpace(document); + var codeModel = builder.CreateSourceModel(node); + var outerPayloadClass = codeModel.FindChildByName("outerPayload"); + Assert.NotNull(outerPayloadClass); + Assert.Equal("innerPayload", outerPayloadClass.StartBlock.Inherits?.Name, StringComparer.OrdinalIgnoreCase); + Assert.Single(outerPayloadClass.Properties); + Assert.Single(outerPayloadClass.Properties.Where(static x => x.Name.Equals("someField", StringComparison.OrdinalIgnoreCase))); + } + [Fact] public async Task EnumsWithNullableDoesNotResultInInlineType() { var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); From e479bb98074567157cb42e05a25450895051ac85 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 10 May 2024 13:27:11 -0400 Subject: [PATCH 08/16] - fixes support for nested intersection types --- .../Extensions/OpenApiSchemaExtensions.cs | 7 +- .../Kiota.Builder.Tests/KiotaBuilderTests.cs | 97 +++++++++++++++++++ 2 files changed, 101 insertions(+), 3 deletions(-) diff --git a/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs b/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs index be677c9e5c..b21b33f5a2 100644 --- a/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs +++ b/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs @@ -91,11 +91,12 @@ public static bool IsInherited(this OpenApiSchema? schema) var result = new OpenApiSchema(schema); result.AllOf.Clear(); var meaningfulSchemas = schema.AllOf - .Where(static x => x.IsSemanticallyMeaningful()) + .Where(static x => x.IsSemanticallyMeaningful() || x.AllOf.Any()) .Select(x => MergeIntersectionSchemaEntries(x, schemasToExclude)) .Where(x => x is not null && (schemasToExclude is null || !schemasToExclude.Contains(x))) - .OfType(); - meaningfulSchemas.SelectMany(static x => x.Properties).ToList().ForEach(x => result.Properties.TryAdd(x.Key, x.Value)); + .OfType() + .ToArray(); + meaningfulSchemas.FlattenEmptyEntries(static x => x.AllOf).Union(meaningfulSchemas).SelectMany(static x => x.Properties).ToList().ForEach(x => result.Properties.TryAdd(x.Key, x.Value)); return result; } diff --git a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs index 5119e345eb..ee4d50080e 100644 --- a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs +++ b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs @@ -7467,6 +7467,103 @@ public async Task InheritanceWithAllOfWith3Parts3SchemaChildClass() Assert.Single(groupClass.Properties.Where(static x => x.Name.Equals("facetprop2", StringComparison.OrdinalIgnoreCase))); } [Fact] + public async Task NestedIntersectionTypeAllOf() + { + var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + await using var fs = await GetDocumentStream(@"openapi: 3.0.3 +info: + title: Model Registry REST API + version: v1alpha2 + description: REST API for Model Registry to create and manage ML model metadata + license: + name: Apache 2.0 + url: 'https://www.apache.org/licenses/LICENSE-2.0' +servers: + - url: 'https://localhost:8080' + - url: 'http://localhost:8080' +paths: + /api/model_registry/v1alpha2/registered_models: + summary: Path used to manage the list of registeredmodels. + description: >- + The REST endpoint/path used to list and create zero or more `RegisteredModel` entities. This path contains a `GET` and `POST` operation to perform the list and create tasks, respectively. + get: + responses: + '200': + $ref: '#/components/responses/RegisteredModelListResponse' + summary: List All RegisteredModels + description: Gets a list of all `RegisteredModel` entities. +components: + schemas: + BaseResource: + type: object + properties: + id: + format: int64 + description: Output only. The unique server generated id of the resource. + type: number + readOnly: true + allOf: + - $ref: '#/components/schemas/BaseResourceCreate' + BaseResourceCreate: + type: object + properties: + name: + description: |- + The client provided name of the artifact. This field is optional. If set, + it must be unique among all the artifacts of the same artifact type within + a database instance and cannot be changed once set. + type: string + BaseResourceList: + required: + - size + type: object + properties: + size: + format: int32 + description: Number of items in result list. + type: integer + RegisteredModel: + description: A registered model in model registry. A registered model has ModelVersion children. + allOf: + - $ref: '#/components/schemas/BaseResource' + - $ref: '#/components/schemas/RegisteredModelCreate' + RegisteredModelCreate: + description: A registered model in model registry. A registered model has ModelVersion children. + allOf: + - $ref: '#/components/schemas/BaseResourceCreate' + RegisteredModelList: + description: List of RegisteredModels. + type: object + allOf: + - $ref: '#/components/schemas/BaseResourceList' + - type: object + properties: + items: + description: '' + type: array + items: + $ref: '#/components/schemas/RegisteredModel' + readOnly: false + responses: + RegisteredModelListResponse: + content: + application/json: + schema: + $ref: '#/components/schemas/RegisteredModelList' + description: A response containing a list of `RegisteredModel` entities.\"); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath }, _httpClient); + var document = await builder.CreateOpenApiDocumentAsync(fs); + var node = builder.CreateUriSpace(document); + var codeModel = builder.CreateSourceModel(node); + var registeredModelClass = codeModel.FindChildByName("RegisteredModel"); + Assert.Null(registeredModelClass.StartBlock.Inherits); + Assert.NotNull(registeredModelClass); + Assert.Single(registeredModelClass.Properties.Where(static x => x.Kind is CodePropertyKind.AdditionalData)); + Assert.Single(registeredModelClass.Properties.Where(static x => x.Name.Equals("name", StringComparison.OrdinalIgnoreCase))); + Assert.Single(registeredModelClass.Properties.Where(static x => x.Name.Equals("id", StringComparison.OrdinalIgnoreCase))); + } + [Fact] public async Task InheritanceWithAllOfWith3Parts3SchemaParentClass() { var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); From 5df039a17e4bf3566c109c29e122475285689dca Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 10 May 2024 13:28:45 -0400 Subject: [PATCH 09/16] - code linting Signed-off-by: Vincent Biret --- src/Kiota.Builder/KiotaBuilder.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index 26d3ec95d9..4a44cc56ba 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -1937,7 +1937,6 @@ private void TrimInheritedModels() if (relatedModels.Contains(x) || classesInUse.Contains(x)) return; if (x is CodeClass currentClass) { - //TODO this is trimming mailboxsettingsbase when it shouldn't. Most likely because one of the indices is now broken var parents = currentClass.GetInheritanceTree(false, false); if (parents.Any(y => classesDirectlyInUse.Contains(y))) return; // to support the inheritance recursive downcast foreach (var baseClass in parents) // discriminator might also be in grand parent types From f24a8cf6ce74e78dc5790554e08dc4ece041b5fe Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 10 May 2024 14:44:35 -0400 Subject: [PATCH 10/16] - fixes wrong inheritance warnings for non-inherited models Signed-off-by: Vincent Biret --- src/Kiota.Builder/KiotaBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index 4a44cc56ba..8e425eaa32 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -2086,7 +2086,7 @@ internal static void AddDiscriminatorMethod(CodeClass newClass, string discrimin } var className = currentNode.GetClassName(config.StructuredMimeTypes, schema: discriminatorSchema).CleanupSymbolName(); var shouldInherit = discriminatorSchema.AllOf.Any(x => currentSchema.Reference?.Id.Equals(x.Reference?.Id, StringComparison.OrdinalIgnoreCase) ?? false); - if (baseClass is not null && !discriminatorSchema.IsInherited()) + if (baseClass is not null && shouldInherit && !discriminatorSchema.IsInherited()) { logger.LogWarning("Discriminator {ComponentKey} is not inherited from {ClassName}.", componentKey, baseClass.Name); return null; From 5a8a1871ca48632bae2dccd9dbf7a151dbf2ceed Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 10 May 2024 15:33:25 -0400 Subject: [PATCH 11/16] - avoids multiple enumerations Signed-off-by: Vincent Biret --- src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs b/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs index b21b33f5a2..f7e371d00f 100644 --- a/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs +++ b/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs @@ -102,8 +102,8 @@ public static bool IsInherited(this OpenApiSchema? schema) public static bool IsIntersection(this OpenApiSchema? schema) { - var meaningfulSchemas = schema?.AllOf?.Where(static x => x.IsSemanticallyMeaningful()); - return meaningfulSchemas?.Count() > 3 || meaningfulSchemas?.Count(static x => !string.IsNullOrEmpty(x.Reference?.Id)) > 1 || meaningfulSchemas?.Count(static x => string.IsNullOrEmpty(x.Reference?.Id)) > 1; + var meaningfulSchemas = schema?.AllOf?.Where(static x => x.IsSemanticallyMeaningful()).ToArray(); + return meaningfulSchemas?.Count(static x => !string.IsNullOrEmpty(x.Reference?.Id)) > 1 || meaningfulSchemas?.Count(static x => string.IsNullOrEmpty(x.Reference?.Id)) > 1; } public static bool IsExclusiveUnion(this OpenApiSchema? schema) From 13b2d7d399a07ef0c0096955c83fc0e26662b5ed Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 10 May 2024 15:36:29 -0400 Subject: [PATCH 12/16] - code linting Signed-off-by: Vincent Biret --- src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs | 4 ++-- src/Kiota.Builder/KiotaBuilder.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs b/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs index f7e371d00f..17a67c842c 100644 --- a/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs +++ b/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs @@ -29,7 +29,7 @@ public static IEnumerable GetSchemaNames(this OpenApiSchema schema, bool internal static IEnumerable FlattenSchemaIfRequired(this IList schemas, Func> subsequentGetter) { if (schemas is null) return []; - return schemas.Count == 1 && !schemas[0].Properties.Any() ? + return schemas.Count == 1 && !schemas[0].HasAnyProperty() ? schemas.FlattenEmptyEntries(subsequentGetter, 1) : schemas; } @@ -144,7 +144,7 @@ public static bool IsComposedEnum(this OpenApiSchema schema) public static bool IsSemanticallyMeaningful(this OpenApiSchema schema, bool ignoreNullableObjects = false) { if (schema is null) return false; - return schema.Properties.Any() || + return schema.HasAnyProperty() || schema.Enum is { Count: > 0 } || schema.Items != null || (!string.IsNullOrEmpty(schema.Type) && diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index 8e425eaa32..b7ef75e848 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -1604,7 +1604,7 @@ private CodeTypeBase CreateComposedModelDeclaration(OpenApiUrlTreeNode currentNo if (typesCount == 1 && schema.Nullable && schema.IsInclusiveUnion() || // nullable on the root schema outside of anyOf typesCount == 2 && (schema.AnyOf?.Any(static x => // nullable on a schema in the anyOf x.Nullable && - !x.Properties.Any() && + !x.HasAnyProperty() && !x.IsExclusiveUnion() && !x.IsInclusiveUnion() && !x.IsInherited() && From c63434dc3026fee72a3ff7a6d4a8aa3f57b03e29 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 13 May 2024 14:57:55 -0400 Subject: [PATCH 13/16] - code linting Signed-off-by: Vincent Biret --- tests/Kiota.Builder.Tests/KiotaBuilderTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs index ee4d50080e..66dd9e4f5f 100644 --- a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs +++ b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs @@ -1890,9 +1890,9 @@ public void Inline_Property_Inheritance_Is_Supported() } } }, - AllOf = new List { + AllOf = [ resourceSchema, - } + ] } } } From 0e39159e8cb223d30d4faf88ea508c13192fb41e Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 13 May 2024 14:58:18 -0400 Subject: [PATCH 14/16] - refactors inheritance routing logic Signed-off-by: Vincent Biret --- src/Kiota.Builder/KiotaBuilder.cs | 80 ++++++++++++++++--------------- 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index b7ef75e848..8913affa31 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -1548,54 +1548,56 @@ private CodeType CreateModelDeclarationAndType(OpenApiUrlTreeNode currentNode, O TypeDefinition = codeDeclaration, }; } - private CodeType CreateInheritedModelDeclaration(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, OpenApiOperation? operation, string classNameSuffix, CodeNamespace codeNamespace, bool isRequestBody, string typeNameForInlineSchema) + private CodeType CreateInheritedModelDeclarationAndType(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, OpenApiOperation? operation, string classNameSuffix, CodeNamespace codeNamespace, bool isRequestBody, string typeNameForInlineSchema) { - var isRootSchemaMeaningful = schema.IsSemanticallyMeaningful(); - var allOfs = schema.AllOf.FlattenSchemaIfRequired(static x => x.AllOf) - .Union(isRootSchemaMeaningful ? - [schema] : - Array.Empty()) - .ToArray(); - CodeClass? codeDeclaration = null; - var codeNamespaceFromParent = GetShortestNamespace(codeNamespace, schema); - OpenApiSchema? previousSchema = null; - foreach (var currentSchema in allOfs) + return new CodeType { - var referenceId = GetReferenceIdFromOriginalSchema(currentSchema, schema); - var shortestNamespaceName = GetModelsNamespaceNameFromReferenceId(referenceId); - var shortestNamespace = string.IsNullOrEmpty(referenceId) ? codeNamespaceFromParent : rootNamespace?.FindOrAddNamespace(shortestNamespaceName); - var className = (currentSchema.GetSchemaName(isRootSchemaMeaningful && currentSchema == schema) is string cName && !string.IsNullOrEmpty(cName) ? + TypeDefinition = CreateInheritedModelDeclaration(currentNode, schema, operation, classNameSuffix, codeNamespace, isRequestBody, typeNameForInlineSchema), + }; + } + private CodeClass CreateInheritedModelDeclaration(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, OpenApiOperation? operation, string classNameSuffix, CodeNamespace codeNamespace, bool isRequestBody, string typeNameForInlineSchema) + { + var flattenedAllOfs = schema.AllOf.FlattenSchemaIfRequired(static x => x.AllOf).ToArray(); + var referenceId = schema.Reference?.Id; + var className = (schema.GetSchemaName(true) is string cName && !string.IsNullOrEmpty(cName) ? cName : (!string.IsNullOrEmpty(typeNameForInlineSchema) ? typeNameForInlineSchema : - currentNode.GetClassName(config.StructuredMimeTypes, operation: operation, suffix: classNameSuffix, schema: currentSchema, requestBody: isRequestBody))) + currentNode.GetClassName(config.StructuredMimeTypes, operation: operation, suffix: classNameSuffix, schema: schema, requestBody: isRequestBody))) .CleanupSymbolName(); - if (shortestNamespace != null && - AddModelDeclarationIfDoesntExist(currentNode, currentSchema, className, shortestNamespace, codeDeclaration, previousSchema) is CodeClass addedDeclaration) - { - codeDeclaration = addedDeclaration; - previousSchema = currentSchema; - } - } - if (codeDeclaration is { } currentClass && - !currentClass.Documentation.DescriptionAvailable && + var shortestNamespaceName = GetModelsNamespaceNameFromReferenceId(referenceId); + var codeNamespaceFromParent = GetShortestNamespace(codeNamespace, schema); + if (rootNamespace is null) + throw new InvalidOperationException("Root namespace is not set"); + var shortestNamespace = string.IsNullOrEmpty(referenceId) ? codeNamespaceFromParent : rootNamespace.FindOrAddNamespace(shortestNamespaceName); + var inlineSchema = flattenedAllOfs.FirstOrDefault(static x => !x.IsReferencedSchema()); + var referencedSchema = flattenedAllOfs.FirstOrDefault(static x => x.IsReferencedSchema()); + var rootSchemaHasProperties = schema.HasAnyProperty(); + var codeDeclaration = (rootSchemaHasProperties, inlineSchema, referencedSchema) switch + { + // greatest parent type + (true, null, null) => AddModelDeclarationIfDoesntExist(currentNode, schema, className, shortestNamespace), + // inline schema + referenced schema + (false, not null, not null) => AddModelDeclarationIfDoesntExist(currentNode, inlineSchema, className, shortestNamespace, CreateInheritedModelDeclaration(currentNode, referencedSchema, operation, classNameSuffix, codeNamespace, isRequestBody, typeNameForInlineSchema)), + // properties + referenced schema + (true, null, not null) => AddModelDeclarationIfDoesntExist(currentNode, schema, className, shortestNamespace, CreateInheritedModelDeclaration(currentNode, referencedSchema, operation, classNameSuffix, codeNamespace, isRequestBody, typeNameForInlineSchema)), + // properties + inline schema + (true, not null, null) => AddModelDeclarationIfDoesntExist(currentNode, schema, className, shortestNamespace, CreateInheritedModelDeclaration(currentNode, inlineSchema, operation, classNameSuffix, codeNamespace, isRequestBody, typeNameForInlineSchema)), + // empty schema + referenced schema + (false, null, not null) => AddModelDeclarationIfDoesntExist(currentNode, referencedSchema, className, shortestNamespace), + // empty schema + inline schema + (false, not null, null) => AddModelDeclarationIfDoesntExist(currentNode, inlineSchema, className, shortestNamespace), + // meaningless scenarios + (false, null, null) or (true, not null, not null) => throw new InvalidOperationException("invalid inheritance case"), + + }; + if (codeDeclaration is not CodeClass currentClass) throw new InvalidOperationException("Inheritance is only supported for classes"); + if (!currentClass.Documentation.DescriptionAvailable && string.IsNullOrEmpty(schema.AllOf.LastOrDefault()?.Description) && !string.IsNullOrEmpty(schema.Description)) currentClass.Documentation.DescriptionTemplate = schema.Description.CleanupDescription(); // the last allof entry often is not a reference and doesn't have a description. - return new CodeType - { - TypeDefinition = codeDeclaration, - }; - } - private static string? GetReferenceIdFromOriginalSchema(OpenApiSchema schema, OpenApiSchema parentSchema) - { - var title = schema.Title; - if (!string.IsNullOrEmpty(schema.Reference?.Id)) return schema.Reference.Id; - if (string.IsNullOrEmpty(title)) return string.Empty; - if (parentSchema.Reference?.Id?.EndsWith(title, StringComparison.OrdinalIgnoreCase) ?? false) return parentSchema.Reference.Id; - if (parentSchema.Items?.Reference?.Id?.EndsWith(title, StringComparison.OrdinalIgnoreCase) ?? false) return parentSchema.Items.Reference.Id; - return parentSchema.GetSchemaReferenceIds().FirstOrDefault(refId => refId.EndsWith(title, StringComparison.OrdinalIgnoreCase)); + return currentClass; } private CodeTypeBase CreateComposedModelDeclaration(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, OpenApiOperation? operation, string suffixForInlineSchema, CodeNamespace codeNamespace, bool isRequestBody, string typeNameForInlineSchema) { @@ -1683,7 +1685,7 @@ private CodeTypeBase CreateModelDeclarations(OpenApiUrlTreeNode currentNode, Ope if (schema.IsInherited()) { - return CreateInheritedModelDeclaration(currentNode, schema, operation, suffix, codeNamespace, isRequestBody, typeNameForInlineSchema); + return CreateInheritedModelDeclarationAndType(currentNode, schema, operation, suffix, codeNamespace, isRequestBody, typeNameForInlineSchema); } if (schema.IsIntersection() && schema.MergeIntersectionSchemaEntries() is OpenApiSchema mergedSchema) From f2b0fa3faa1609270b7c5f29d36c35e300ec6613 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 13 May 2024 15:13:16 -0400 Subject: [PATCH 15/16] - code linting Signed-off-by: Vincent Biret --- src/Kiota.Builder/KiotaBuilder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index 8913affa31..4cdf9324e6 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -1570,8 +1570,8 @@ private CodeClass CreateInheritedModelDeclaration(OpenApiUrlTreeNode currentNode if (rootNamespace is null) throw new InvalidOperationException("Root namespace is not set"); var shortestNamespace = string.IsNullOrEmpty(referenceId) ? codeNamespaceFromParent : rootNamespace.FindOrAddNamespace(shortestNamespaceName); - var inlineSchema = flattenedAllOfs.FirstOrDefault(static x => !x.IsReferencedSchema()); - var referencedSchema = flattenedAllOfs.FirstOrDefault(static x => x.IsReferencedSchema()); + var inlineSchema = Array.Find(flattenedAllOfs, static x => !x.IsReferencedSchema()); + var referencedSchema = Array.Find(flattenedAllOfs, static x => x.IsReferencedSchema()); var rootSchemaHasProperties = schema.HasAnyProperty(); var codeDeclaration = (rootSchemaHasProperties, inlineSchema, referencedSchema) switch { From 75d86bba76ae8560be379283c98ecefccec4a25e Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Tue, 14 May 2024 08:03:08 -0400 Subject: [PATCH 16/16] Apply suggestions from code review Co-authored-by: Andrew Omondi --- CHANGELOG.md | 1 + src/Kiota.Builder/KiotaBuilder.cs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b413a0ef8d..f0afb3cd4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed a bug where multiple allOf entries type would not get merged if they were part of a discriminator. [#4325](https://github.com/microsoft/kiota/issues/4325) - Fixed a bug where allOf structure with one entry and properties would not be considered as inheritance case. [#4346](https://github.com/microsoft/kiota/issues/4346) - Fixed a bug where some allOf scenarios would be missing properties if type object wasn't set on the schema. [#4074](https://github.com/microsoft/kiota/issues/4074) +- Fixed a bug where schema with multiple allOf entries would incorrectly get merged to inherit from the first entry [#4428] (https://github.com/microsoft/kiota/issues/4428) ## [1.14.0] - 2024-05-02 diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index 4cdf9324e6..fee259f137 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -1578,9 +1578,9 @@ private CodeClass CreateInheritedModelDeclaration(OpenApiUrlTreeNode currentNode // greatest parent type (true, null, null) => AddModelDeclarationIfDoesntExist(currentNode, schema, className, shortestNamespace), // inline schema + referenced schema - (false, not null, not null) => AddModelDeclarationIfDoesntExist(currentNode, inlineSchema, className, shortestNamespace, CreateInheritedModelDeclaration(currentNode, referencedSchema, operation, classNameSuffix, codeNamespace, isRequestBody, typeNameForInlineSchema)), + (false, not null, not null) => AddModelDeclarationIfDoesntExist(currentNode, inlineSchema, className, shortestNamespace, CreateInheritedModelDeclaration(currentNode, referencedSchema, operation, classNameSuffix, codeNamespace, isRequestBody, string.Empty)), // properties + referenced schema - (true, null, not null) => AddModelDeclarationIfDoesntExist(currentNode, schema, className, shortestNamespace, CreateInheritedModelDeclaration(currentNode, referencedSchema, operation, classNameSuffix, codeNamespace, isRequestBody, typeNameForInlineSchema)), + (true, null, not null) => AddModelDeclarationIfDoesntExist(currentNode, schema, className, shortestNamespace, CreateInheritedModelDeclaration(currentNode, referencedSchema, operation, classNameSuffix, codeNamespace, isRequestBody, string.Empty)), // properties + inline schema (true, not null, null) => AddModelDeclarationIfDoesntExist(currentNode, schema, className, shortestNamespace, CreateInheritedModelDeclaration(currentNode, inlineSchema, operation, classNameSuffix, codeNamespace, isRequestBody, typeNameForInlineSchema)), // empty schema + referenced schema