Skip to content
Open
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Added support for OpenAPI 3.2.0
- Added support for enum path parameters
- Added support for net10

### Changed
Expand Down
14 changes: 10 additions & 4 deletions src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Kiota.Builder.Extensions;

public static class OpenApiSchemaExtensions
{
private static readonly Func<IOpenApiSchema, IList<IOpenApiSchema>> classNamesFlattener = x =>
private static readonly Func<IOpenApiSchema, IList<IOpenApiSchema>> subsequentSchemaGetter = x =>
(x.AnyOf ?? Enumerable.Empty<IOpenApiSchema>()).Union(x.AllOf ?? []).Union(x.OneOf ?? []).ToList();
public static IEnumerable<string> GetSchemaNames(this IOpenApiSchema schema, bool directOnly = false)
{
Expand All @@ -21,13 +21,19 @@ public static IEnumerable<string> GetSchemaNames(this IOpenApiSchema schema, boo
if (schema.GetReferenceId() is string refId && !string.IsNullOrEmpty(refId))
return [refId.Split('/')[^1].Split('.')[^1]];
if (!directOnly && schema.AnyOf is { Count: > 0 })
return schema.AnyOf.FlattenIfRequired(classNamesFlattener);
return schema.AnyOf.FlattenIfRequired(subsequentSchemaGetter);
if (!directOnly && schema.AllOf is { Count: > 0 })
return schema.AllOf.FlattenIfRequired(classNamesFlattener);
return schema.AllOf.FlattenIfRequired(subsequentSchemaGetter);
if (!directOnly && schema.OneOf is { Count: > 0 })
return schema.OneOf.FlattenIfRequired(classNamesFlattener);
return schema.OneOf.FlattenIfRequired(subsequentSchemaGetter);
return [];
}

public static IList<IOpenApiSchema> GetSubsequentSchemas(this IOpenApiSchema schema)
{
return subsequentSchemaGetter(schema);
}

internal static string? GetReferenceId(this IOpenApiSchema schema)
{
return schema switch
Expand Down
60 changes: 54 additions & 6 deletions src/Kiota.Builder/KiotaBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1110,9 +1110,10 @@ private CodeParameter GetIndexerParameter(OpenApiUrlTreeNode currentNode)
default;
var type = parameter switch
{
null => DefaultIndexerParameterType,
_ => GetPrimitiveType(parameter.Schema),
} ?? DefaultIndexerParameterType;
not null when GetEnumType(currentNode, parameter) is {} enumType => enumType,
not null when GetPrimitiveType(parameter.Schema) is {} primitiveType => primitiveType,
_ => DefaultIndexerParameterType,
};
type.IsNullable = false;
var segment = currentNode.DeduplicatedSegment();
var result = new CodeParameter
Expand All @@ -1127,6 +1128,41 @@ private CodeParameter GetIndexerParameter(OpenApiUrlTreeNode currentNode)
};
return result;
}

private CodeType? GetEnumType(OpenApiUrlTreeNode currentNode, IOpenApiParameter parameter)
{
IOpenApiSchema? enumCandidateSchema = parameter.Schema;
if (enumCandidateSchema is null || modelsNamespace is null)
{
return default;
}

// Many specs wrap enum refs under allOf/anyOf/oneOf or nested empty entries: [ { $ref: ... } ]
var subsequentSchemas = enumCandidateSchema.GetSubsequentSchemas();
var flattened = subsequentSchemas
.FlattenSchemaIfRequired(static x => x.GetSubsequentSchemas())
.ToList();
var candidates = flattened.Count != 0 ? flattened : [enumCandidateSchema];

// Prefer the actual enum-bearing subschema
enumCandidateSchema = candidates.FirstOrDefault(static x => x.IsEnum()) ?? enumCandidateSchema;

var targetNamespace = GetShortestNamespace(modelsNamespace, enumCandidateSchema);
var declarationName = enumCandidateSchema.GetSchemaName()?.CleanupSymbolName();
if (string.IsNullOrEmpty(declarationName))
{
return default;
}

var enumDeclaration = AddEnumDeclarationIfDoesntExist(currentNode, enumCandidateSchema, declarationName!, targetNamespace);
if (enumDeclaration is not null)
{
return new CodeType { Name = enumDeclaration.Name, TypeDefinition = enumDeclaration };
}

return default;
}

private static IDictionary<string, IOpenApiPathItem> GetPathItems(OpenApiUrlTreeNode currentNode, bool validateIsParameterNode = true)
{
if ((!validateIsParameterNode || currentNode.IsParameter) && currentNode.PathItems.Count != 0)
Expand Down Expand Up @@ -2267,11 +2303,21 @@ private IEnumerable<CodeNamespace> GetAllNamespaces(CodeNamespace currentNamespa
}
private IEnumerable<CodeElement> GetTypeDefinitionsInNamespace(CodeNamespace currentNamespace)
{
var requestExecutors = GetAllNamespaces(currentNamespace)
.SelectMany(static x => x.Classes)
.Where(static x => x.IsOfKind(CodeClassKind.RequestBuilder))
var requestBuilders = GetAllNamespaces(currentNamespace)
.SelectMany(static x => x.Classes)
.Where(static x => x.IsOfKind(CodeClassKind.RequestBuilder))
.ToArray();

var requestExecutors = requestBuilders
.SelectMany(static x => x.Methods)
.Where(static x => x.IsOfKind(CodeMethodKind.RequestExecutor));

var indexerParameterTypes = requestBuilders
.Select(static rb => rb.Indexer?.IndexParameter.Type)
.OfType<CodeTypeBase>()
.SelectMany(static t => t.AllTypes)
.ToArray();

return requestExecutors.SelectMany(static x => x.ReturnType.AllTypes)
.Union(requestExecutors
.SelectMany(static x => x.Parameters)
Expand All @@ -2283,6 +2329,8 @@ private IEnumerable<CodeElement> GetTypeDefinitionsInNamespace(CodeNamespace cur
.OfType<CodeClass>()
.Select(static x => x.Properties.FirstOrDefault(static y => y.Kind is CodePropertyKind.QueryParameters)?.Type)
.OfType<CodeType>())
// include the indexer parameter types so enums used there are not pruned as unused
.Union(indexerParameterTypes)
.Union(requestExecutors.SelectMany(static x => x.ErrorMappings.SelectMany(static y => y.Value.AllTypes)))
.Where(static x => x.TypeDefinition != null)
.Select(static x => x.TypeDefinition!)
Expand Down
134 changes: 134 additions & 0 deletions tests/Kiota.Builder.Tests/KiotaBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10110,6 +10110,140 @@ public void CleansUpOperationIdChangesOperationId()
Assert.Equal("PostAdministrativeUnits_With201_response", operations[1].Value.OperationId);
Assert.Equal("directory_adminstativeunits_item_get", operations[2].Value.OperationId);
}

[Fact]
public async Task GeneratesEnumTypeForIndexerParameterAndCreatesEnumModelAsync()
{
var tempFilePath = Path.GetTempFileName();
await File.WriteAllTextAsync(tempFilePath, @$"openapi: 3.0.1
info:
title: Test API
version: 1.0.0
servers:
- url: https://api.contoso.test
paths:
/tenants/{{tenant}}/resources:
get:
parameters:
- name: tenant
in: path
required: true
schema:
$ref: '#/components/schemas/Tenant'
responses:
'200':
description: OK
components:
schemas:
Tenant:
type: string
enum: [A, B]
");

await using var fs = new FileStream(tempFilePath, FileMode.Open);
var mockLogger = new Mock<ILogger<KiotaBuilder>>();
var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration
{
ClientClassName = "ApiSdk",
OpenAPIFilePath = tempFilePath,
Language = GenerationLanguage.CSharp
}, _httpClient);

var document = await builder.CreateOpenApiDocumentAsync(fs);
var node = builder.CreateUriSpace(document!);
builder.SetApiRootUrl();
var codeModel = builder.CreateSourceModel(node);

var collectionRequestBuilderNamespace = codeModel.FindNamespaceByName("ApiSdk.tenants");
Assert.NotNull(collectionRequestBuilderNamespace);
var collectionRequestBuilder = collectionRequestBuilderNamespace.FindChildByName<CodeClass>("tenantsRequestBuilder");
Assert.NotNull(collectionRequestBuilder);

var indexer = collectionRequestBuilder.Indexer;
Assert.NotNull(indexer);
Assert.NotNull(indexer.IndexParameter);
Assert.NotNull(indexer.IndexParameter.Type);
Assert.False(indexer.IndexParameter.Type.IsNullable);

// verify the type is an enum definition named Tenant
var indexParamTypeDef = indexer.IndexParameter.Type.AllTypes.First().TypeDefinition;
Assert.IsType<CodeEnum>(indexParamTypeDef);
var enumType = (CodeEnum)indexParamTypeDef!;
Assert.Equal("Tenant", enumType.Name);

// verify the enum model exists in the Models namespace
var modelsNS = codeModel.FindNamespaceByName("ApiSdk.Models");
Assert.NotNull(modelsNS);
var tenantEnumInModels = modelsNS.FindChildByName<CodeEnum>("Tenant", false);
Assert.NotNull(tenantEnumInModels);
}

[Fact]
public async Task GeneratesEnumTypeForIndexerParameterFromAllOfWrapperAsync()
{
var tempFilePath = Path.GetTempFileName();
await File.WriteAllTextAsync(tempFilePath, @$"openapi: 3.0.1
info:
title: Test API
version: 1.0.0
servers:
- url: https://api.contoso.test
paths:
/tenants/{{tenant}}/resources:
get:
parameters:
- name: tenant
in: path
required: true
schema:
allOf:
- $ref: '#/components/schemas/Tenant'
responses:
'200':
description: OK
components:
schemas:
Tenant:
type: string
enum: [A, B]
");

await using var fs = new FileStream(tempFilePath, FileMode.Open);
var mockLogger = new Mock<ILogger<KiotaBuilder>>();
var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration
{
ClientClassName = "ApiSdk",
OpenAPIFilePath = tempFilePath,
Language = GenerationLanguage.CSharp
}, _httpClient);

var document = await builder.CreateOpenApiDocumentAsync(fs);
var node = builder.CreateUriSpace(document!);
builder.SetApiRootUrl();
var codeModel = builder.CreateSourceModel(node);

var collectionRequestBuilderNamespace = codeModel.FindNamespaceByName("ApiSdk.tenants");
Assert.NotNull(collectionRequestBuilderNamespace);
var collectionRequestBuilder = collectionRequestBuilderNamespace.FindChildByName<CodeClass>("tenantsRequestBuilder");
Assert.NotNull(collectionRequestBuilder);

var indexer = collectionRequestBuilder.Indexer;
Assert.NotNull(indexer);
Assert.NotNull(indexer.IndexParameter);
Assert.NotNull(indexer.IndexParameter.Type);
Assert.False(indexer.IndexParameter.Type.IsNullable);

var indexParamTypeDef = indexer.IndexParameter.Type.AllTypes.First().TypeDefinition;
Assert.IsType<CodeEnum>(indexParamTypeDef);
var enumType = (CodeEnum)indexParamTypeDef!;
Assert.Equal("Tenant", enumType.Name);

var modelsNS = codeModel.FindNamespaceByName("ApiSdk.Models");
Assert.NotNull(modelsNS);
var tenantEnumInModels = modelsNS.FindChildByName<CodeEnum>("Tenant", false);
Assert.NotNull(tenantEnumInModels);
}

[GeneratedRegex(@"^[a-zA-Z0-9_]*$", RegexOptions.IgnoreCase | RegexOptions.Singleline, 2000)]
private static partial Regex OperationIdValidationRegex();
}