diff --git a/CHANGELOG.md b/CHANGELOG.md index f43f06497d..0f62425ee4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added support for external documentation links within descriptions in Python. [#2041](https://github.com/microsoft/kiota/issues/2041) - Added support for API manifests. [#3104](https://github.com/microsoft/kiota/issues/3104) +- Added support for reserved path parameters. [#2320](https://github.com/microsoft/kiota/issues/2320) ### Changed diff --git a/src/Kiota.Builder/Extensions/OpenApiUrlTreeNodeExtensions.cs b/src/Kiota.Builder/Extensions/OpenApiUrlTreeNodeExtensions.cs index 41c4ed71c6..6e7416b9e9 100644 --- a/src/Kiota.Builder/Extensions/OpenApiUrlTreeNodeExtensions.cs +++ b/src/Kiota.Builder/Extensions/OpenApiUrlTreeNodeExtensions.cs @@ -1,8 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection.Metadata; using System.Text.RegularExpressions; - +using Kiota.Builder.OpenApiExtensions; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Services; @@ -196,17 +197,24 @@ public static string GetUrlTemplate(this OpenApiUrlTreeNode currentNode) .Aggregate(static (x, y) => $"{x},{y}") + '}'; } + var pathReservedPathParametersIds = currentNode.PathItems.TryGetValue(Constants.DefaultOpenApiLabel, out var pItem) ? + pItem.Parameters + .Union(pItem.Operations.SelectMany(static x => x.Value.Parameters)) + .Where(static x => x.In == ParameterLocation.Path && x.Extensions.TryGetValue(OpenApiReservedParameterExtension.Name, out var ext) && ext is OpenApiReservedParameterExtension reserved && reserved.IsReserved.HasValue && reserved.IsReserved.Value) + .Select(static x => x.Name) + .ToHashSet(StringComparer.OrdinalIgnoreCase) : + new HashSet(); return "{+baseurl}" + - SanitizePathParameterNamesForUrlTemplate(currentNode.Path.Replace('\\', '/')) + + SanitizePathParameterNamesForUrlTemplate(currentNode.Path.Replace('\\', '/'), pathReservedPathParametersIds) + queryStringParameters; } private static readonly Regex pathParamMatcher = new(@"{(?[^}]+)}", RegexOptions.Compiled, Constants.DefaultRegexTimeout); - private static string SanitizePathParameterNamesForUrlTemplate(string original) + private static string SanitizePathParameterNamesForUrlTemplate(string original, HashSet reservedParameterNames) { if (string.IsNullOrEmpty(original) || !original.Contains('{', StringComparison.OrdinalIgnoreCase)) return original; var parameters = pathParamMatcher.Matches(original); foreach (var value in parameters.Select(x => x.Groups["paramname"].Value)) - original = original.Replace(value, value.SanitizeParameterNameForUrlTemplate(), StringComparison.Ordinal); + original = original.Replace(value, (reservedParameterNames.Contains(value) ? "+" : string.Empty) + value.SanitizeParameterNameForUrlTemplate(), StringComparison.Ordinal); return original; } public static string SanitizeParameterNameForUrlTemplate(this string original) diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index 90f7f25a65..27b475fe45 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -467,6 +467,10 @@ ex is SecurityException || { OpenApiDeprecationExtension.Name, static (i, _ ) => OpenApiDeprecationExtension.Parse(i) + }, + { + OpenApiReservedParameterExtension.Name, + static (i, _ ) => OpenApiReservedParameterExtension.Parse(i) } }, RuleSet = ruleSet, diff --git a/src/Kiota.Builder/OpenApiExtensions/OpenApiReservedParameterExtension.cs b/src/Kiota.Builder/OpenApiExtensions/OpenApiReservedParameterExtension.cs new file mode 100644 index 0000000000..4291902ce2 --- /dev/null +++ b/src/Kiota.Builder/OpenApiExtensions/OpenApiReservedParameterExtension.cs @@ -0,0 +1,52 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------ + +using System; +using Microsoft.OpenApi; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Writers; + +namespace Kiota.Builder.OpenApiExtensions; + +/// +/// Extension element for OpenAPI to add reserved parameters. x-ms-reserved-parameters +/// +public class OpenApiReservedParameterExtension : IOpenApiExtension +{ + /// + /// Name of the extension as used in the description. + /// + public static string Name => "x-ms-reserved-parameter"; + /// + public void Write(IOpenApiWriter writer, OpenApiSpecVersion specVersion) + { + ArgumentNullException.ThrowIfNull(writer); + if (IsReserved.HasValue) + writer.WriteValue(IsReserved.Value); + } + /// + /// Whether the associated parameter is reserved or not. + /// + public bool? IsReserved + { + get; set; + } + /// + /// Parses the to . + /// + /// The source object. + /// The . + /// + public static OpenApiReservedParameterExtension Parse(IOpenApiAny source) + { + ArgumentNullException.ThrowIfNull(source); + if (source is not OpenApiBoolean rawBoolean) throw new ArgumentOutOfRangeException(nameof(source)); + return new OpenApiReservedParameterExtension + { + IsReserved = rawBoolean.Value + }; + } +} diff --git a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs index 942056f97f..09dfb9a5c3 100644 --- a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs +++ b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs @@ -5983,6 +5983,170 @@ public void SinglePathParametersAreDeduplicated() Assert.Equal("{+baseurl}/users/{id}/careerAdvisor", careerAdvisorUrlTemplate.DefaultValue.Trim('"')); } [Fact] + public void AddReservedPathParameterSymbol() + { + var userSchema = new OpenApiSchema + { + Type = "object", + Properties = new Dictionary { + { + "id", new OpenApiSchema { + Type = "string" + } + }, + { + "displayName", new OpenApiSchema { + Type = "string" + } + } + }, + Reference = new OpenApiReference + { + Id = "#/components/schemas/microsoft.graph.user" + }, + UnresolvedReference = false + }; + var document = new OpenApiDocument + { + Paths = new OpenApiPaths + { + ["users/{id}/manager"] = new OpenApiPathItem + { + Parameters = new List { + new OpenApiParameter { + Name = "id", + In = ParameterLocation.Path, + Required = true, + Schema = new OpenApiSchema { + Type = "string" + }, + Extensions = { + ["x-ms-reserved-parameter"] = new OpenApiReservedParameterExtension { + IsReserved = true + } + } + } + }, + Operations = { + [OperationType.Get] = new OpenApiOperation + { + Responses = new OpenApiResponses { + ["200"] = new OpenApiResponse + { + Content = { + ["application/json"] = new OpenApiMediaType + { + Schema = userSchema + } + } + } + } + } + } + }, + }, + Components = new OpenApiComponents + { + Schemas = new Dictionary { + { + "microsoft.graph.user", userSchema + } + } + } + }; + var mockLogger = new CountLogger(); + var builder = new KiotaBuilder(mockLogger, new GenerationConfiguration { ClientClassName = "Graph", ApiRootUrl = "https://localhost" }, _httpClient); + var node = builder.CreateUriSpace(document); + var codeModel = builder.CreateSourceModel(node); + var managerRB = codeModel.FindNamespaceByName("ApiSdk.users.item.manager").FindChildByName("ManagerRequestBuilder", false); + Assert.NotNull(managerRB); + var managerUrlTemplate = managerRB.FindChildByName("UrlTemplate", false); + Assert.NotNull(managerUrlTemplate); + Assert.Equal("{+baseurl}/users/{+id}/manager", managerUrlTemplate.DefaultValue.Trim('"')); + } + [Fact] + public void DoesNotAddReservedPathParameterSymbol() + { + var userSchema = new OpenApiSchema + { + Type = "object", + Properties = new Dictionary { + { + "id", new OpenApiSchema { + Type = "string" + } + }, + { + "displayName", new OpenApiSchema { + Type = "string" + } + } + }, + Reference = new OpenApiReference + { + Id = "#/components/schemas/microsoft.graph.user" + }, + UnresolvedReference = false + }; + var document = new OpenApiDocument + { + Paths = new OpenApiPaths + { + ["users/{id}/manager"] = new OpenApiPathItem + { + Parameters = new List { + new OpenApiParameter { + Name = "id", + In = ParameterLocation.Path, + Required = true, + Schema = new OpenApiSchema { + Type = "string" + }, + Extensions = { + ["x-ms-reserved-parameter"] = new OpenApiReservedParameterExtension { + IsReserved = false + } + } + } + }, + Operations = { + [OperationType.Get] = new OpenApiOperation + { + Responses = new OpenApiResponses { + ["200"] = new OpenApiResponse + { + Content = { + ["application/json"] = new OpenApiMediaType + { + Schema = userSchema + } + } + } + } + } + } + }, + }, + Components = new OpenApiComponents + { + Schemas = new Dictionary { + { + "microsoft.graph.user", userSchema + } + } + } + }; + var mockLogger = new CountLogger(); + var builder = new KiotaBuilder(mockLogger, new GenerationConfiguration { ClientClassName = "Graph", ApiRootUrl = "https://localhost" }, _httpClient); + var node = builder.CreateUriSpace(document); + var codeModel = builder.CreateSourceModel(node); + var managerRB = codeModel.FindNamespaceByName("ApiSdk.users.item.manager").FindChildByName("ManagerRequestBuilder", false); + Assert.NotNull(managerRB); + var managerUrlTemplate = managerRB.FindChildByName("UrlTemplate", false); + Assert.NotNull(managerUrlTemplate); + Assert.Equal("{+baseurl}/users/{id}/manager", managerUrlTemplate.DefaultValue.Trim('"')); + } + [Fact] public async Task MergesIntersectionTypes() { var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); diff --git a/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiReservedParameterExtensionTests.cs b/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiReservedParameterExtensionTests.cs new file mode 100644 index 0000000000..dcac5011eb --- /dev/null +++ b/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiReservedParameterExtensionTests.cs @@ -0,0 +1,35 @@ +using System; +using System.IO; +using Kiota.Builder.OpenApiExtensions; +using Microsoft.OpenApi; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Writers; +using Xunit; + +namespace Kiota.Builder.Tests.OpenApiExtensions; + +public class OpenApiReservedParameterExtensionTests +{ + [Fact] + public void Parses() + { + var oaiValue = new OpenApiBoolean(true); + var value = OpenApiReservedParameterExtension.Parse(oaiValue); + Assert.NotNull(value); + Assert.True(value.IsReserved); + } + [Fact] + public void Serializes() + { + var value = new OpenApiReservedParameterExtension + { + IsReserved = true + }; + using TextWriter sWriter = new StringWriter(); + OpenApiJsonWriter writer = new(sWriter); + + value.Write(writer, OpenApiSpecVersion.OpenApi3_0); + var result = sWriter.ToString(); + Assert.Equal("true", result, StringComparer.OrdinalIgnoreCase); + } +}