From b5291023f3d3412be6e6518ab11df5858adc5169 Mon Sep 17 00:00:00 2001 From: danaelhe Date: Wed, 6 Aug 2025 12:03:43 -0400 Subject: [PATCH 1/4] typescript: allow mixed types for `anyOf` --- src/Kiota.Builder/KiotaBuilder.cs | 51 ++++++- .../Kiota.Builder.Tests/KiotaBuilderTests.cs | 139 ++++++++++++++++++ 2 files changed, 189 insertions(+), 1 deletion(-) diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index 2dc50ad5f7..b7a3c59655 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -1107,7 +1107,7 @@ private CodeParameter GetIndexerParameter(OpenApiUrlTreeNode currentNode) var type = parameter switch { null => DefaultIndexerParameterType, - _ => GetPrimitiveType(parameter.Schema), + _ => GetPathParameterType(parameter.Schema), } ?? DefaultIndexerParameterType; type.IsNullable = false; var segment = currentNode.DeduplicatedSegment(); @@ -1253,6 +1253,55 @@ openApiExtension is OpenApiPrimaryErrorMessageExtension primaryErrorMessageExten (_, _) => null, }; } + + private CodeTypeBase? GetPathParameterType(IOpenApiSchema? typeSchema) + { + // Check if it's a union type with mixed primitives (anyOf or oneOf) first + if (typeSchema != null && (typeSchema.AnyOf?.Count > 0 || typeSchema.OneOf?.Count > 0)) + { + var schemas = typeSchema.AnyOf ?? typeSchema.OneOf; + if (schemas != null) + { + var primitiveTypes = new List(); + + foreach (var schema in schemas) + { + if (GetPrimitiveType(schema) is CodeType schemaType) + { + primitiveTypes.Add(schemaType); + } + } + + // If we found multiple primitive types, create a union type + if (primitiveTypes.Count > 1) + { + var unionType = new CodeUnionType + { + Name = "PathParameterUnion" + }; + + foreach (var primitiveType in primitiveTypes) + { + if (!unionType.ContainsType(primitiveType)) + { + unionType.AddType(primitiveType); + } + } + + return unionType; + } + // If we only found one primitive type, return it + else if (primitiveTypes.Count == 1) + { + return primitiveTypes[0]; + } + } + } + + // Fall back to regular primitive type handling + return GetPrimitiveType(typeSchema); + } + private const string RequestBodyPlainTextContentType = "text/plain"; private const string RequestBodyOctetStreamContentType = "application/octet-stream"; private const string DefaultResponseIndicator = "default"; diff --git a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs index f638c7bdfa..67fcaddf45 100644 --- a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs +++ b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs @@ -6687,6 +6687,145 @@ public async Task IndexerTypeIsAccurateAndBackwardCompatibleIndexersAreAddedAsyn var actorsItemRequestBuilder = actorsItemRequestBuilderNamespace.FindChildByName("actorItemRequestBuilder"); Assert.Equal(actorsCollectionIndexer.ReturnType.Name, actorsItemRequestBuilder.Name); } + + [Fact] + public async Task IndexerSupportsUnionOfPrimitiveTypesForPathParametersAsync() + { + var tempFilePath = Path.GetTempFileName(); + await using var fs = await GetDocumentStreamAsync(@"openapi: 3.0.0 +info: + title: Test API with Union Path Parameters + version: 1.0.0 +servers: + - url: https://api.example.com/v1 +paths: + /keys/{ssh_key_identifier}: + get: + parameters: + - name: ssh_key_identifier + in: path + required: true + description: Either the ID or the fingerprint of an existing SSH key. + schema: + anyOf: + - type: string + - type: integer + example: 512189 + responses: + 200: + description: Success! + content: + application/json: + schema: + $ref: '#/components/schemas/SSHKey' +components: + schemas: + SSHKey: + type: object + properties: + id: + type: integer + fingerprint: + type: string + key: + type: string"); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "TestClient", OpenAPIFilePath = tempFilePath }, _httpClient); + var document = await builder.CreateOpenApiDocumentAsync(fs); + var node = builder.CreateUriSpace(document!); + var codeModel = builder.CreateSourceModel(node); + + var keysCollectionRequestBuilderNamespace = codeModel.FindNamespaceByName("ApiSdk.keys"); + Assert.NotNull(keysCollectionRequestBuilderNamespace); + var keysCollectionRequestBuilder = keysCollectionRequestBuilderNamespace.FindChildByName("keysRequestBuilder"); + var keysCollectionIndexer = keysCollectionRequestBuilder.Indexer; + Assert.NotNull(keysCollectionIndexer); + + // Check that the indexer parameter type is a union type containing both string and integer + var parameterType = keysCollectionIndexer.IndexParameter.Type; + Assert.IsType(parameterType); + var unionType = (CodeUnionType)keysCollectionIndexer.IndexParameter.Type; + Assert.Equal(2, unionType.Types.Count()); + + // Verify both types are present in the union + Assert.Contains(unionType.Types, t => t.Name.Equals("string", StringComparison.OrdinalIgnoreCase)); + Assert.Contains(unionType.Types, t => t.Name.Equals("integer", StringComparison.OrdinalIgnoreCase)); + + // Verify description + Assert.Equal("Either the ID or the fingerprint of an existing SSH key.", keysCollectionIndexer.IndexParameter.Documentation.DescriptionTemplate); + Assert.False(keysCollectionIndexer.IndexParameter.Type.IsNullable); + Assert.False(keysCollectionIndexer.Deprecation.IsDeprecated); + } + + [Fact] + public async Task IndexerSupportsUnionOfPrimitiveTypesForPathParametersWithOneOfAsync() + { + var tempFilePath = Path.GetTempFileName(); + await using var fs = await GetDocumentStreamAsync(@"openapi: 3.0.0 +info: + title: Test API with OneOf Path Parameters + version: 1.0.0 +servers: + - url: https://api.example.com/v1 +paths: + /keys/{ssh_key_identifier}: + get: + parameters: + - name: ssh_key_identifier + in: path + required: true + description: Either the ID or the fingerprint of an existing SSH key. + schema: + oneOf: + - type: string + - type: integer + example: 512189 + responses: + 200: + description: Success! + content: + application/json: + schema: + $ref: '#/components/schemas/SSHKey' +components: + schemas: + SSHKey: + type: object + properties: + id: + type: integer + fingerprint: + type: string + key: + type: string"); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "TestClient", OpenAPIFilePath = tempFilePath }, _httpClient); + var document = await builder.CreateOpenApiDocumentAsync(fs); + var node = builder.CreateUriSpace(document!); + var codeModel = builder.CreateSourceModel(node); + + var keysCollectionRequestBuilderNamespace = codeModel.FindNamespaceByName("ApiSdk.keys"); + Assert.NotNull(keysCollectionRequestBuilderNamespace); + var keysCollectionRequestBuilder = keysCollectionRequestBuilderNamespace.FindChildByName("keysRequestBuilder"); + var keysCollectionIndexer = keysCollectionRequestBuilder.Indexer; + Assert.NotNull(keysCollectionIndexer); + + // Check that the indexer parameter type is a union type containing both string and integer + var parameterType = keysCollectionIndexer.IndexParameter.Type; + Assert.IsType(parameterType); + var unionType = (CodeUnionType)keysCollectionIndexer.IndexParameter.Type; + Assert.Equal(2, unionType.Types.Count()); + + // Verify both types are present in the union + Assert.Contains(unionType.Types, t => t.Name.Equals("string", StringComparison.OrdinalIgnoreCase)); + Assert.Contains(unionType.Types, t => t.Name.Equals("integer", StringComparison.OrdinalIgnoreCase)); + + // Verify description + Assert.Equal("Either the ID or the fingerprint of an existing SSH key.", keysCollectionIndexer.IndexParameter.Documentation.DescriptionTemplate); + Assert.False(keysCollectionIndexer.IndexParameter.Type.IsNullable); + Assert.False(keysCollectionIndexer.Deprecation.IsDeprecated); + } + [Fact] public async Task MapsBooleanEnumToBooleanTypeAsync() { From 4bd649f01792f16af458f4243cd7acc8c94b3d77 Mon Sep 17 00:00:00 2001 From: danaelhe Date: Fri, 8 Aug 2025 10:06:26 -0400 Subject: [PATCH 2/4] update changelog, remove redundancy in kiotabuilder --- CHANGELOG.md | 11 +++---- src/Kiota.Builder/KiotaBuilder.cs | 48 ++++++++++++------------------- 2 files changed, 24 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a40b84017b..cf785f4413 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- typescript: allow mixed types for anyOf. [#6801](https://github.com/microsoft/kiota/issues/6801#issuecomment-3160393844) + ## [1.28.0] - 2025-07-11 ### Added @@ -41,7 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed an issue where migration from lock to workspace would fail because of stream management. [#6515](https://github.com/microsoft/kiota/issues/6515) - Fixed a bug where media types from error responses would be missing from the accept header. [#6572](https://github.com/microsoft/kiota/issues/6572) - Fixed a bug where serialization names for Dart were not correct [#6624](https://github.com/microsoft/kiota/issues/6624) -- Fixed a bug where imports from __future__ would appear below other imports in python generated code. [#4600](https://github.com/microsoft/kiota/issues/4600) +- Fixed a bug where imports from **future** would appear below other imports in python generated code. [#4600](https://github.com/microsoft/kiota/issues/4600) ## [1.26.1] - 2025-05-15 @@ -183,7 +185,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 invalid code in Php caused by "_/_/" in property description. [5635](https://github.com/microsoft/kiota/issues/5635) - Fixed a bug where discriminator property name lookup could end up in an infinite loop. [#5771](https://github.com/microsoft/kiota/issues/5771) - Fixed TypeScript generation error when generating usings from shaken serializers. [#5634](https://github.com/microsoft/kiota/issues/5634) - Multiple fixed and improvements in OpenAPI description generation for plugins. [#5806](https://github.com/microsoft/kiota/issues/5806) @@ -324,7 +326,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added uri-form encoded serialization for PHP. [#2074](https://github.com/microsoft/kiota/issues/2074) - Added information message with base URL in the CLI experience. [#4635](https://github.com/microsoft/kiota/issues/4635) - Added optional parameter --disable-ssl-validation for generate, show, and download commands. [#4176](https://github.com/microsoft/kiota/issues/4176) -- For *Debug* builds of kiota, the `--log-level` / `--ll` option is now observed if specified explicitly on the command line. It still defaults to `Debug` for *Debug* builds and `Warning` for *Release* builds. [#4739](https://github.com/microsoft/kiota/pull/4739) +- For _Debug_ builds of kiota, the `--log-level` / `--ll` option is now observed if specified explicitly on the command line. It still defaults to `Debug` for _Debug_ builds and `Warning` for _Release_ builds. [#4739](https://github.com/microsoft/kiota/pull/4739) ### Changed @@ -926,7 +928,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Removed unused generated import for PHP Generation. - Fixed a bug where long namespaces would make Ruby packaging fail. - Fixed a bug where classes with namespace names are generated outside namespace in Python. [#2188](https://github.com/microsoft/kiota/issues/2188) -- Changed signature of escaped reserved names from {x}*escaped to {x}* in line with Python style guides. +- Changed signature of escaped reserved names from {x}_escaped to {x}_ in line with Python style guides. - Add null checks in generated Shell language code. - Fixed a bug where Go indexers would fail to pass the index parameter. - Fixed a bug where path segments with parameters could be missing words. [#2209](https://github.com/microsoft/kiota/issues/2209) @@ -1675,4 +1677,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Initial GitHub release - diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index b7a3c59655..31d9717b99 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -1257,44 +1257,32 @@ openApiExtension is OpenApiPrimaryErrorMessageExtension primaryErrorMessageExten private CodeTypeBase? GetPathParameterType(IOpenApiSchema? typeSchema) { // Check if it's a union type with mixed primitives (anyOf or oneOf) first - if (typeSchema != null && (typeSchema.AnyOf?.Count > 0 || typeSchema.OneOf?.Count > 0)) + if (typeSchema != null && (typeSchema.AnyOf ?? typeSchema.OneOf) is { Count : > 0 } schemas) { - var schemas = typeSchema.AnyOf ?? typeSchema.OneOf; - if (schemas != null) + var primitiveTypes = schemas.Select(static s => GetPrimitiveType(s)).OfType().ToArray(); + + // If we found multiple primitive types, create a union type + if (primitiveTypes.Length > 1) { - var primitiveTypes = new List(); - - foreach (var schema in schemas) + var unionType = new CodeUnionType { - if (GetPrimitiveType(schema) is CodeType schemaType) - { - primitiveTypes.Add(schemaType); - } - } + Name = "PathParameterUnion" + }; - // If we found multiple primitive types, create a union type - if (primitiveTypes.Count > 1) + foreach (var primitiveType in primitiveTypes) { - var unionType = new CodeUnionType - { - Name = "PathParameterUnion" - }; - - foreach (var primitiveType in primitiveTypes) + if (!unionType.ContainsType(primitiveType)) { - if (!unionType.ContainsType(primitiveType)) - { - unionType.AddType(primitiveType); - } + unionType.AddType(primitiveType); } - - return unionType; - } - // If we only found one primitive type, return it - else if (primitiveTypes.Count == 1) - { - return primitiveTypes[0]; } + + return unionType; + } + // If we only found one primitive type, return it + else if (primitiveTypes.Length == 1) + { + return primitiveTypes[0]; } } From 27a8b07194ecb9565ac43adbc703cc3f05f16151 Mon Sep 17 00:00:00 2001 From: danaelhe Date: Sat, 9 Aug 2025 22:26:33 -0400 Subject: [PATCH 3/4] dotnet format tests/Kiota.Builder.Tests/Kiota.Builder.Tests.csproj --- src/Kiota.Builder/KiotaBuilder.cs | 14 +++++++------- tests/Kiota.Builder.Tests/KiotaBuilderTests.cs | 18 +++++++++--------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index 31d9717b99..1688e1db34 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -1253,14 +1253,14 @@ openApiExtension is OpenApiPrimaryErrorMessageExtension primaryErrorMessageExten (_, _) => null, }; } - + private CodeTypeBase? GetPathParameterType(IOpenApiSchema? typeSchema) { // Check if it's a union type with mixed primitives (anyOf or oneOf) first - if (typeSchema != null && (typeSchema.AnyOf ?? typeSchema.OneOf) is { Count : > 0 } schemas) + if (typeSchema != null && (typeSchema.AnyOf ?? typeSchema.OneOf) is { Count: > 0 } schemas) { var primitiveTypes = schemas.Select(static s => GetPrimitiveType(s)).OfType().ToArray(); - + // If we found multiple primitive types, create a union type if (primitiveTypes.Length > 1) { @@ -1268,7 +1268,7 @@ openApiExtension is OpenApiPrimaryErrorMessageExtension primaryErrorMessageExten { Name = "PathParameterUnion" }; - + foreach (var primitiveType in primitiveTypes) { if (!unionType.ContainsType(primitiveType)) @@ -1276,7 +1276,7 @@ openApiExtension is OpenApiPrimaryErrorMessageExtension primaryErrorMessageExten unionType.AddType(primitiveType); } } - + return unionType; } // If we only found one primitive type, return it @@ -1285,11 +1285,11 @@ openApiExtension is OpenApiPrimaryErrorMessageExtension primaryErrorMessageExten return primitiveTypes[0]; } } - + // Fall back to regular primitive type handling return GetPrimitiveType(typeSchema); } - + private const string RequestBodyPlainTextContentType = "text/plain"; private const string RequestBodyOctetStreamContentType = "application/octet-stream"; private const string DefaultResponseIndicator = "default"; diff --git a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs index 67fcaddf45..0a6ef761ab 100644 --- a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs +++ b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs @@ -6687,7 +6687,7 @@ public async Task IndexerTypeIsAccurateAndBackwardCompatibleIndexersAreAddedAsyn var actorsItemRequestBuilder = actorsItemRequestBuilderNamespace.FindChildByName("actorItemRequestBuilder"); Assert.Equal(actorsCollectionIndexer.ReturnType.Name, actorsItemRequestBuilder.Name); } - + [Fact] public async Task IndexerSupportsUnionOfPrimitiveTypesForPathParametersAsync() { @@ -6740,23 +6740,23 @@ public async Task IndexerSupportsUnionOfPrimitiveTypesForPathParametersAsync() var keysCollectionRequestBuilder = keysCollectionRequestBuilderNamespace.FindChildByName("keysRequestBuilder"); var keysCollectionIndexer = keysCollectionRequestBuilder.Indexer; Assert.NotNull(keysCollectionIndexer); - + // Check that the indexer parameter type is a union type containing both string and integer var parameterType = keysCollectionIndexer.IndexParameter.Type; Assert.IsType(parameterType); var unionType = (CodeUnionType)keysCollectionIndexer.IndexParameter.Type; Assert.Equal(2, unionType.Types.Count()); - + // Verify both types are present in the union Assert.Contains(unionType.Types, t => t.Name.Equals("string", StringComparison.OrdinalIgnoreCase)); Assert.Contains(unionType.Types, t => t.Name.Equals("integer", StringComparison.OrdinalIgnoreCase)); - + // Verify description Assert.Equal("Either the ID or the fingerprint of an existing SSH key.", keysCollectionIndexer.IndexParameter.Documentation.DescriptionTemplate); Assert.False(keysCollectionIndexer.IndexParameter.Type.IsNullable); Assert.False(keysCollectionIndexer.Deprecation.IsDeprecated); } - + [Fact] public async Task IndexerSupportsUnionOfPrimitiveTypesForPathParametersWithOneOfAsync() { @@ -6809,23 +6809,23 @@ public async Task IndexerSupportsUnionOfPrimitiveTypesForPathParametersWithOneOf var keysCollectionRequestBuilder = keysCollectionRequestBuilderNamespace.FindChildByName("keysRequestBuilder"); var keysCollectionIndexer = keysCollectionRequestBuilder.Indexer; Assert.NotNull(keysCollectionIndexer); - + // Check that the indexer parameter type is a union type containing both string and integer var parameterType = keysCollectionIndexer.IndexParameter.Type; Assert.IsType(parameterType); var unionType = (CodeUnionType)keysCollectionIndexer.IndexParameter.Type; Assert.Equal(2, unionType.Types.Count()); - + // Verify both types are present in the union Assert.Contains(unionType.Types, t => t.Name.Equals("string", StringComparison.OrdinalIgnoreCase)); Assert.Contains(unionType.Types, t => t.Name.Equals("integer", StringComparison.OrdinalIgnoreCase)); - + // Verify description Assert.Equal("Either the ID or the fingerprint of an existing SSH key.", keysCollectionIndexer.IndexParameter.Documentation.DescriptionTemplate); Assert.False(keysCollectionIndexer.IndexParameter.Type.IsNullable); Assert.False(keysCollectionIndexer.Deprecation.IsDeprecated); } - + [Fact] public async Task MapsBooleanEnumToBooleanTypeAsync() { From 3e0227c248ba8103a4b7db8b0adb976c16cbf4ae Mon Sep 17 00:00:00 2001 From: danaelhe Date: Mon, 11 Aug 2025 14:57:48 -0400 Subject: [PATCH 4/4] fix idempotency ci tests --- src/Kiota.Builder/KiotaBuilder.cs | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index 1688e1db34..19287371b4 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -2548,7 +2548,7 @@ internal static void AddSerializationMembers(CodeClass model, bool includeAdditi } private void AddPropertyForQueryParameter(OpenApiUrlTreeNode node, NetHttpMethod operationType, IOpenApiParameter parameter, CodeClass parameterClass) { - CodeType? resultType = default; + CodeTypeBase? resultType = default; var addBackwardCompatibleParameter = false; if (parameter.Schema is not null && (parameter.Schema.IsEnum() || (parameter.Schema.IsArray() && parameter.Schema.Items.IsEnum()))) @@ -2573,6 +2573,19 @@ private void AddPropertyForQueryParameter(OpenApiUrlTreeNode node, NetHttpMethod addBackwardCompatibleParameter = true; } } + + // Handle union types (oneOf/anyOf) for query parameters + if (resultType is null && parameter.Schema is not null && + (parameter.Schema.IsInclusiveUnion() || parameter.Schema.IsExclusiveUnion()) && + string.IsNullOrEmpty(parameter.Schema.Format) && + !parameter.Schema.IsODataPrimitiveType()) + { + var codeNamespace = parameterClass.GetImmediateParentOfType(); + var typeNameForInlineSchema = $"{operationType.Method.ToLowerInvariant().ToFirstCharacterUpperCase()}{parameter.Name.CleanupSymbolName().ToFirstCharacterUpperCase()}QueryParameterType"; + var unionTypeResult = CreateModelDeclarations(node, parameter.Schema, null, codeNamespace, string.Empty, typeNameForInlineSchema: typeNameForInlineSchema); + resultType = unionTypeResult; + } + resultType ??= GetPrimitiveType(parameter.Schema) ?? new CodeType() { // since its a query parameter default to string if there is no schema @@ -2630,17 +2643,6 @@ private static CodeType GetDefaultQueryParameterType() Name = "string", }; } - private static CodeType GetQueryParameterType(IOpenApiSchema schema) - { - var paramType = GetPrimitiveType(schema) ?? new() - { - IsExternal = true, - Name = schema.Items is not null && (schema.Items.Type & ~JsonSchemaType.Null)?.ToIdentifiers().FirstOrDefault() is string name ? name : "null", - }; - - paramType.CollectionKind = schema.IsArray() ? CodeTypeBase.CodeTypeCollectionKind.Array : default; - return paramType; - } private void CleanUpInternalState() {