From ddf5474f332211784418feeb07e6f885fe5faeb8 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 25 Nov 2024 13:44:17 -0500 Subject: [PATCH 1/5] fix: single entry one/any of merging Signed-off-by: Vincent Biret --- CHANGELOG.md | 5 +- .../Extensions/OpenApiSchemaExtensions.cs | 38 +++- src/Kiota.Builder/KiotaBuilder.cs | 10 + .../Kiota.Builder.Tests/KiotaBuilderTests.cs | 176 ++++++++++++++++++ 4 files changed, 223 insertions(+), 6 deletions(-) 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() { From 3174e6c6a4b94d5496220fcc4487c49fbd16fe33 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 25 Nov 2024 14:35:10 -0500 Subject: [PATCH 2/5] fix: nullable information for enum collections Signed-off-by: Vincent Biret --- src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs index 2bee2503fb..b2a97e360b 100644 --- a/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs @@ -140,7 +140,7 @@ private void WriteFactoryMethodBodyForUnionModel(CodeMethod codeElement, CodeCla } else if (propertyType.TypeDefinition is CodeClass && propertyType.IsCollection || propertyType.TypeDefinition is null || propertyType.TypeDefinition is CodeEnum) { - var typeName = conventions.GetTypeString(propertyType, codeElement, true, false); + var typeName = conventions.GetTypeString(propertyType, codeElement, true, propertyType.TypeDefinition is CodeEnum && propertyType.CollectionKind is not CodeTypeBase.CodeTypeCollectionKind.None); var valueVarName = $"{property.Name.ToFirstCharacterLowerCase()}Value"; writer.WriteLine($"{(includeElse ? "else " : string.Empty)}if({parseNodeParameter.Name.ToFirstCharacterLowerCase()}.{GetDeserializationMethodName(propertyType, codeElement)} is {typeName} {valueVarName})"); writer.WriteBlock(lines: $"{ResultVarName}.{property.Name.ToFirstCharacterUpperCase()} = {valueVarName};"); @@ -161,7 +161,7 @@ private void WriteFactoryMethodBodyForIntersectionModel(CodeMethod codeElement, { if (property.Type is CodeType propertyType) { - var typeName = conventions.GetTypeString(propertyType, codeElement, true, false); + var typeName = conventions.GetTypeString(propertyType, codeElement, true, propertyType.TypeDefinition is CodeEnum && propertyType.CollectionKind is not CodeTypeBase.CodeTypeCollectionKind.None); var valueVarName = $"{property.Name.ToFirstCharacterLowerCase()}Value"; writer.WriteLine($"{(includeElse ? "else " : string.Empty)}if({parseNodeParameter.Name.ToFirstCharacterLowerCase()}.{GetDeserializationMethodName(propertyType, codeElement)} is {typeName} {valueVarName})"); writer.WriteBlock(lines: $"{ResultVarName}.{property.Name.ToFirstCharacterUpperCase()} = {valueVarName};"); From 6d69bcd60a087dd7d50ac11940e6f797b017155f Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 25 Nov 2024 14:48:18 -0500 Subject: [PATCH 3/5] fix: cast for go enum types Signed-off-by: Vincent Biret --- src/Kiota.Builder/Writers/Go/CodeMethodWriter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Kiota.Builder/Writers/Go/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/Go/CodeMethodWriter.cs index 542be4f681..df2e4abd69 100644 --- a/src/Kiota.Builder/Writers/Go/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/Go/CodeMethodWriter.cs @@ -195,7 +195,7 @@ private void WriteFactoryMethodBodyForIntersectionModel(CodeMethod codeElement, WriteCollectionCast(propertyTypeImportName, valueVarName, "cast", writer, isInterfaceType ? string.Empty : "*", !isInterfaceType); valueVarName = "cast"; } - else if (propertyType.TypeDefinition is CodeClass || propertyType.TypeDefinition is CodeInterface) + else if (propertyType.TypeDefinition is CodeClass || propertyType.TypeDefinition is CodeInterface || propertyType.TypeDefinition is CodeEnum) { writer.StartBlock($"if {GetTypeAssertion(valueVarName, propertyTypeImportName, "cast", "ok")}; ok {{"); valueVarName = "cast"; From 7477648e11ca1dd82e340b9d2771110eefe193da Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 25 Nov 2024 15:11:22 -0500 Subject: [PATCH 4/5] fix: missing block close Signed-off-by: Vincent Biret --- src/Kiota.Builder/Writers/Go/CodeMethodWriter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Kiota.Builder/Writers/Go/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/Go/CodeMethodWriter.cs index df2e4abd69..c17a8b0044 100644 --- a/src/Kiota.Builder/Writers/Go/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/Go/CodeMethodWriter.cs @@ -201,7 +201,7 @@ private void WriteFactoryMethodBodyForIntersectionModel(CodeMethod codeElement, valueVarName = "cast"; } writer.WriteLine($"{ResultVarName}.{property.Setter!.Name.ToFirstCharacterUpperCase()}({valueVarName})"); - if (!propertyType.IsCollection && (propertyType.TypeDefinition is CodeClass || propertyType.TypeDefinition is CodeInterface)) + if (!propertyType.IsCollection && (propertyType.TypeDefinition is CodeClass || propertyType.TypeDefinition is CodeInterface || propertyType.TypeDefinition is CodeEnum)) writer.CloseBlock(); writer.DecreaseIndent(); } From 5d2944ed29c2bccb620db5ddd04717943e02947b Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Tue, 26 Nov 2024 13:00:22 -0500 Subject: [PATCH 5/5] fix: adds missing pointer symbol Signed-off-by: Vincent Biret --- src/Kiota.Builder/Writers/Go/CodeMethodWriter.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Kiota.Builder/Writers/Go/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/Go/CodeMethodWriter.cs index c17a8b0044..ea026bd4c8 100644 --- a/src/Kiota.Builder/Writers/Go/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/Go/CodeMethodWriter.cs @@ -197,6 +197,10 @@ private void WriteFactoryMethodBodyForIntersectionModel(CodeMethod codeElement, } else if (propertyType.TypeDefinition is CodeClass || propertyType.TypeDefinition is CodeInterface || propertyType.TypeDefinition is CodeEnum) { + if (propertyType.TypeDefinition is CodeEnum) + { + propertyTypeImportName = conventions.GetTypeString(property.Type, parentClass, false, true); + } writer.StartBlock($"if {GetTypeAssertion(valueVarName, propertyTypeImportName, "cast", "ok")}; ok {{"); valueVarName = "cast"; }