Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

- adds support for reserved path parameters #3201

Merged
merged 2 commits into from
Aug 28, 2023
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
16 changes: 12 additions & 4 deletions src/Kiota.Builder/Extensions/OpenApiUrlTreeNodeExtensions.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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<string>();
return "{+baseurl}" +
SanitizePathParameterNamesForUrlTemplate(currentNode.Path.Replace('\\', '/')) +
SanitizePathParameterNamesForUrlTemplate(currentNode.Path.Replace('\\', '/'), pathReservedPathParametersIds) +
queryStringParameters;
}
private static readonly Regex pathParamMatcher = new(@"{(?<paramname>[^}]+)}", RegexOptions.Compiled, Constants.DefaultRegexTimeout);
private static string SanitizePathParameterNamesForUrlTemplate(string original)
private static string SanitizePathParameterNamesForUrlTemplate(string original, HashSet<string> 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)
Expand Down
4 changes: 4 additions & 0 deletions src/Kiota.Builder/KiotaBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,10 @@ ex is SecurityException ||
{
OpenApiDeprecationExtension.Name,
static (i, _ ) => OpenApiDeprecationExtension.Parse(i)
},
{
OpenApiReservedParameterExtension.Name,
static (i, _ ) => OpenApiReservedParameterExtension.Parse(i)
}
},
RuleSet = ruleSet,
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Extension element for OpenAPI to add reserved parameters. x-ms-reserved-parameters
/// </summary>
public class OpenApiReservedParameterExtension : IOpenApiExtension
{
/// <summary>
/// Name of the extension as used in the description.
/// </summary>
public static string Name => "x-ms-reserved-parameter";
/// <inheritdoc />
public void Write(IOpenApiWriter writer, OpenApiSpecVersion specVersion)
{
ArgumentNullException.ThrowIfNull(writer);
if (IsReserved.HasValue)
writer.WriteValue(IsReserved.Value);
}
/// <summary>
/// Whether the associated parameter is reserved or not.
/// </summary>
public bool? IsReserved
{
get; set;
}
/// <summary>
/// Parses the <see cref="IOpenApiAny"/> to <see cref="OpenApiReservedParameterExtension"/>.
/// </summary>
/// <param name="source">The source object.</param>
/// <returns>The <see cref="OpenApiReservedParameterExtension"/>.</returns>
/// <returns></returns>
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
};
}
}
164 changes: 164 additions & 0 deletions tests/Kiota.Builder.Tests/KiotaBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, OpenApiSchema> {
{
"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<OpenApiParameter> {
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<string, OpenApiSchema> {
{
"microsoft.graph.user", userSchema
}
}
}
};
var mockLogger = new CountLogger<KiotaBuilder>();
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<CodeClass>("ManagerRequestBuilder", false);
Assert.NotNull(managerRB);
var managerUrlTemplate = managerRB.FindChildByName<CodeProperty>("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<string, OpenApiSchema> {
{
"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<OpenApiParameter> {
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<string, OpenApiSchema> {
{
"microsoft.graph.user", userSchema
}
}
}
};
var mockLogger = new CountLogger<KiotaBuilder>();
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<CodeClass>("ManagerRequestBuilder", false);
Assert.NotNull(managerRB);
var managerUrlTemplate = managerRB.FindChildByName<CodeProperty>("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());
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}