diff --git a/CHANGELOG.md b/CHANGELOG.md index d35ddea7ad..6e1c1329df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs b/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs index 8d8f93bf25..0ab534e0ea 100644 --- a/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs +++ b/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs @@ -10,7 +10,7 @@ namespace Kiota.Builder.Extensions; public static class OpenApiSchemaExtensions { - private static readonly Func> classNamesFlattener = x => + private static readonly Func> subsequentSchemaGetter = x => (x.AnyOf ?? Enumerable.Empty()).Union(x.AllOf ?? []).Union(x.OneOf ?? []).ToList(); public static IEnumerable GetSchemaNames(this IOpenApiSchema schema, bool directOnly = false) { @@ -21,13 +21,19 @@ public static IEnumerable 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 GetSubsequentSchemas(this IOpenApiSchema schema) + { + return subsequentSchemaGetter(schema); + } + internal static string? GetReferenceId(this IOpenApiSchema schema) { return schema switch diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index a703a44962..88d7a2bc0a 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -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 @@ -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 GetPathItems(OpenApiUrlTreeNode currentNode, bool validateIsParameterNode = true) { if ((!validateIsParameterNode || currentNode.IsParameter) && currentNode.PathItems.Count != 0) @@ -2267,11 +2303,21 @@ private IEnumerable GetAllNamespaces(CodeNamespace currentNamespa } private IEnumerable 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() + .SelectMany(static t => t.AllTypes) + .ToArray(); + return requestExecutors.SelectMany(static x => x.ReturnType.AllTypes) .Union(requestExecutors .SelectMany(static x => x.Parameters) @@ -2283,6 +2329,8 @@ private IEnumerable GetTypeDefinitionsInNamespace(CodeNamespace cur .OfType() .Select(static x => x.Properties.FirstOrDefault(static y => y.Kind is CodePropertyKind.QueryParameters)?.Type) .OfType()) + // 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!) diff --git a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs index 50740a6275..555110d2b1 100644 --- a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs +++ b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs @@ -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>(); + 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("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(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("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>(); + 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("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(indexParamTypeDef); + var enumType = (CodeEnum)indexParamTypeDef!; + Assert.Equal("Tenant", enumType.Name); + + var modelsNS = codeModel.FindNamespaceByName("ApiSdk.Models"); + Assert.NotNull(modelsNS); + var tenantEnumInModels = modelsNS.FindChildByName("Tenant", false); + Assert.NotNull(tenantEnumInModels); + } + [GeneratedRegex(@"^[a-zA-Z0-9_]*$", RegexOptions.IgnoreCase | RegexOptions.Singleline, 2000)] private static partial Regex OperationIdValidationRegex(); }