Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ 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)
- Fixes a bug where invalid C# code is generated when API path contains an underscore [#6698](https://github.com/microsoft/kiota/issues/6698)
- Fixed a bug where union of integer and boolean types collection would not compile in dotnet. [#6834](https://github.com/microsoft/kiota/issues/6834)

Expand Down Expand Up @@ -44,7 +45,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

Expand Down Expand Up @@ -186,7 +187,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)
Expand Down Expand Up @@ -327,7 +328,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

Expand Down Expand Up @@ -929,7 +930,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)
Expand Down Expand Up @@ -1678,4 +1679,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Initial GitHub release

65 changes: 52 additions & 13 deletions src/Kiota.Builder/KiotaBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -1253,6 +1253,43 @@ 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)
{
var primitiveTypes = schemas.Select(static s => GetPrimitiveType(s)).OfType<CodeType>().ToArray();

// If we found multiple primitive types, create a union type
if (primitiveTypes.Length > 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.Length == 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";
Expand Down Expand Up @@ -2511,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())))
Expand All @@ -2536,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<CodeNamespace>();
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
Expand Down Expand Up @@ -2593,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()
{
Expand Down
139 changes: 139 additions & 0 deletions tests/Kiota.Builder.Tests/KiotaBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6687,6 +6687,145 @@ public async Task IndexerTypeIsAccurateAndBackwardCompatibleIndexersAreAddedAsyn
var actorsItemRequestBuilder = actorsItemRequestBuilderNamespace.FindChildByName<CodeClass>("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<ILogger<KiotaBuilder>>();
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<CodeClass>("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<CodeUnionType>(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<ILogger<KiotaBuilder>>();
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<CodeClass>("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<CodeUnionType>(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()
{
Expand Down
Loading