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