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

### Changed

Expand Down
51 changes: 46 additions & 5 deletions src/Kiota.Builder/KiotaBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1107,8 +1107,8 @@ private CodeParameter GetIndexerParameter(OpenApiUrlTreeNode currentNode)
var type = parameter switch
{
null => DefaultIndexerParameterType,
_ => GetPrimitiveType(parameter.Schema),
} ?? DefaultIndexerParameterType;
_ => GetEnumType(currentNode, parameter) ?? GetPrimitiveType(parameter.Schema) ?? DefaultIndexerParameterType,
};
type.IsNullable = false;
var segment = currentNode.DeduplicatedSegment();
var result = new CodeParameter
Expand All @@ -1123,6 +1123,35 @@ private CodeParameter GetIndexerParameter(OpenApiUrlTreeNode currentNode)
};
return result;
}

private CodeType? GetEnumType(OpenApiUrlTreeNode currentNode, IOpenApiParameter parameter)
{
IOpenApiSchema? enumCandidateSchema = parameter.Schema;
// many specs wrap refs under allOf: [ { $ref: ... } ]
if (enumCandidateSchema?.AllOf is { Count: 1 } && enumCandidateSchema.AllOf.FirstOrDefault() is IOpenApiSchema singleAllOf)
enumCandidateSchema = singleAllOf;

if (enumCandidateSchema is null || modelsNamespace is null)
{
return default;
}

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 @@ -2262,11 +2291,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 @@ -2278,6 +2317,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();
}