diff --git a/CHANGELOG.md b/CHANGELOG.md index 17076aa315..224f085f85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,8 +16,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Fixed python generation in scenarios with opening/closing tags for code comments. [#5636](https://github.com/microsoft/kiota/issues/5636) -- Fixed Python error when a class inherits from a base class and implements an interface. [5637](https://github.com/microsoft/kiota/issues/5637) -- Fix anyOf/oneOf generation in TypeScript. [5353](https://github.com/microsoft/kiota/issues/5353) +- Fixed Python error when a class inherits from a base class and implements an interface. [#5637](https://github.com/microsoft/kiota/issues/5637) +- Fixed a bug where one/any schemas with single schema entries would be missing properties. [#5808](https://github.com/microsoft/kiota/issues/5808) +- Fixed anyOf/oneOf generation in TypeScript. [5353](https://github.com/microsoft/kiota/issues/5353) - Fixed invalid code in Php caused by "*/*/" in property description. [5635](https://github.com/microsoft/kiota/issues/5635) - Fixed TypeScript generation error when generating usings from shaken serializers. [#5634](https://github.com/microsoft/kiota/issues/5634) diff --git a/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs b/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs index c5ed4b24e9..890231960c 100644 --- a/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs +++ b/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs @@ -68,9 +68,9 @@ public static bool HasAnyProperty(this OpenApiSchema? schema) { return schema?.Properties is { Count: > 0 }; } - public static bool IsInclusiveUnion(this OpenApiSchema? schema) + public static bool IsInclusiveUnion(this OpenApiSchema? schema, uint exclusiveMinimumNumberOfEntries = 1) { - return schema?.AnyOf?.Count(static x => IsSemanticallyMeaningful(x, true)) > 1; + return schema?.AnyOf?.Count(static x => IsSemanticallyMeaningful(x, true)) > exclusiveMinimumNumberOfEntries; // so we don't consider any of object/nullable as a union type } @@ -89,6 +89,36 @@ public static bool IsInherited(this OpenApiSchema? schema) return schema.MergeIntersectionSchemaEntries(schemasToExclude, true, filter); } + internal static OpenApiSchema? MergeInclusiveUnionSchemaEntries(this OpenApiSchema? schema) + { + if (schema is null || !schema.IsInclusiveUnion(0)) return null; + var result = new OpenApiSchema(schema); + result.AnyOf.Clear(); + foreach (var subSchema in schema.AnyOf) + { + foreach (var property in subSchema.Properties) + { + result.Properties.TryAdd(property.Key, property.Value); + } + } + return result; + } + + internal static OpenApiSchema? MergeExclusiveUnionSchemaEntries(this OpenApiSchema? schema) + { + if (schema is null || !schema.IsExclusiveUnion(0)) return null; + var result = new OpenApiSchema(schema); + result.OneOf.Clear(); + foreach (var subSchema in schema.OneOf) + { + foreach (var property in subSchema.Properties) + { + result.Properties.TryAdd(property.Key, property.Value); + } + } + return result; + } + internal static OpenApiSchema? MergeIntersectionSchemaEntries(this OpenApiSchema? schema, HashSet? schemasToExclude = default, bool overrideIntersection = false, Func? filter = default) { if (schema is null) return null; @@ -123,9 +153,9 @@ public static bool IsIntersection(this OpenApiSchema? schema) 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) + public static bool IsExclusiveUnion(this OpenApiSchema? schema, uint exclusiveMinimumNumberOfEntries = 1) { - return schema?.OneOf?.Count(static x => IsSemanticallyMeaningful(x, true)) > 1; + return schema?.OneOf?.Count(static x => IsSemanticallyMeaningful(x, true)) > exclusiveMinimumNumberOfEntries; // so we don't consider one of object/nullable as a union type } private static readonly HashSet oDataTypes = new(StringComparer.OrdinalIgnoreCase) { diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index fd4e0887c2..40c3207ff9 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -1895,6 +1895,16 @@ private CodeElement AddModelDeclarationIfDoesntExist(OpenApiUrlTreeNode currentN // multiple allOf entries that do not translate to inheritance return createdClass; } + else if (schema.MergeInclusiveUnionSchemaEntries() is { } iUMergedSchema && + AddModelClass(currentNode, iUMergedSchema, declarationName, currentNamespace, currentOperation, inheritsFrom) is CodeClass uICreatedClass) + { + return uICreatedClass; + } + else if (schema.MergeExclusiveUnionSchemaEntries() is { } eUMergedSchema && + AddModelClass(currentNode, eUMergedSchema, declarationName, currentNamespace, currentOperation, inheritsFrom) is CodeClass uECreatedClass) + { + return uECreatedClass; + } return AddModelClass(currentNode, schema, declarationName, currentNamespace, currentOperation, inheritsFrom); } return existingDeclaration; diff --git a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs index 606bbe5c62..a62089926f 100644 --- a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs +++ b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs @@ -8536,6 +8536,182 @@ public async Task InheritanceWithAllOfBaseClassNoAdditionalPropertiesAsync() Assert.Equal("baseDirectoryObject", link.StartBlock.Inherits.Name); } + [Fact] + public async Task ExclusiveUnionSingleEntriesMergingAsync() + { + var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + await using var fs = await GetDocumentStreamAsync( +""" +openapi: 3.0.0 +info: + title: "Generator not generating oneOf if the containing schema has type: object" + version: "1.0.0" +servers: + - url: https://mytodos.doesnotexist/ +paths: + /uses-components: + post: + description: Return something + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/UsesComponents" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/UsesComponents" +components: + schemas: + ExampleWithSingleOneOfWithTypeObject: + type: object + oneOf: + - $ref: "#/components/schemas/Component1" + discriminator: + propertyName: objectType + ExampleWithSingleOneOfWithoutTypeObject: + oneOf: + - $ref: "#/components/schemas/Component2" + discriminator: + propertyName: objectType + + UsesComponents: + type: object + properties: + component_with_single_oneof_with_type_object: + $ref: "#/components/schemas/ExampleWithSingleOneOfWithTypeObject" + component_with_single_oneof_without_type_object: + $ref: "#/components/schemas/ExampleWithSingleOneOfWithoutTypeObject" + + Component1: + type: object + required: + - objectType + properties: + objectType: + type: string + one: + type: string + + Component2: + type: object + required: + - objectType + properties: + objectType: + type: string + two: + 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); + + // Verify that all three classes referenced by the discriminator inherit from baseDirectoryObject + var withObjectClass = codeModel.FindChildByName("ExampleWithSingleOneOfWithTypeObject"); + Assert.NotNull(withObjectClass); + var oneProperty = withObjectClass.FindChildByName("one", false); + Assert.NotNull(oneProperty); + + var withoutObjectClass = codeModel.FindChildByName("Component2"); + Assert.NotNull(withObjectClass); + var twoProperty = withoutObjectClass.FindChildByName("two", false); + Assert.NotNull(twoProperty); + } + + [Fact] + public async Task InclusiveUnionSingleEntriesMergingAsync() + { + var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + await using var fs = await GetDocumentStreamAsync( +""" +openapi: 3.0.0 +info: + title: "Generator not generating anyOf if the containing schema has type: object" + version: "1.0.0" +servers: + - url: https://mytodos.doesnotexist/ +paths: + /uses-components: + post: + description: Return something + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/UsesComponents" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/UsesComponents" +components: + schemas: + ExampleWithSingleOneOfWithTypeObject: + type: object + anyOf: + - $ref: "#/components/schemas/Component1" + discriminator: + propertyName: objectType + ExampleWithSingleOneOfWithoutTypeObject: + anyOf: + - $ref: "#/components/schemas/Component2" + discriminator: + propertyName: objectType + + UsesComponents: + type: object + properties: + component_with_single_oneof_with_type_object: + $ref: "#/components/schemas/ExampleWithSingleOneOfWithTypeObject" + component_with_single_oneof_without_type_object: + $ref: "#/components/schemas/ExampleWithSingleOneOfWithoutTypeObject" + + Component1: + type: object + required: + - objectType + properties: + objectType: + type: string + one: + type: string + + Component2: + type: object + required: + - objectType + properties: + objectType: + type: string + two: + 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); + + // Verify that all three classes referenced by the discriminator inherit from baseDirectoryObject + var withObjectClass = codeModel.FindChildByName("ExampleWithSingleOneOfWithTypeObject"); + Assert.NotNull(withObjectClass); + var oneProperty = withObjectClass.FindChildByName("one", false); + Assert.NotNull(oneProperty); + + var withoutObjectClass = codeModel.FindChildByName("Component2"); + Assert.NotNull(withObjectClass); + var twoProperty = withoutObjectClass.FindChildByName("two", false); + Assert.NotNull(twoProperty); + } + [Fact] public async Task NestedIntersectionTypeAllOfAsync() {