Skip to content
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
34 changes: 29 additions & 5 deletions src/OpenApi/src/Extensions/ApiDescriptionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,22 @@ internal static class ApiDescriptionExtensions
/// Maps the HTTP method of the ApiDescription to the HttpMethod.
/// </summary>
/// <param name="apiDescription">The ApiDescription to resolve an HttpMethod from.</param>
/// <returns>The <see cref="HttpMethod"/> associated with the given <paramref name="apiDescription"/>, if known.</returns>
public static HttpMethod? GetHttpMethod(this ApiDescription apiDescription) =>
apiDescription.HttpMethod?.ToUpperInvariant() switch
/// <returns>
/// The <see cref="HttpMethod"/> associated with the given <paramref name="apiDescription"/>, including custom methods
/// when the provided HTTP method token is valid. Returns <see langword="null"/> when the HTTP method is missing or invalid.
/// </returns>
public static HttpMethod? GetHttpMethod(this ApiDescription apiDescription)
{
var httpMethod = apiDescription.HttpMethod;
if (string.IsNullOrWhiteSpace(httpMethod))
Comment thread
baywet marked this conversation as resolved.
{
return null;
}

var normalizedHttpMethod = httpMethod.Trim();
Comment thread
baywet marked this conversation as resolved.

return normalizedHttpMethod.ToUpperInvariant() switch
{
// Only add methods documented in the OpenAPI spec: https://spec.openapis.org/oas/v3.2.0.html#path-item-object
"GET" => HttpMethod.Get,
"POST" => HttpMethod.Post,
"PUT" => HttpMethod.Put,
Expand All @@ -30,9 +41,22 @@ internal static class ApiDescriptionExtensions
"OPTIONS" => HttpMethod.Options,
"TRACE" => HttpMethod.Trace,
"QUERY" => HttpMethod.Query,
_ => null,
_ => TryCreateHttpMethod(normalizedHttpMethod),
Comment thread
Youssef1313 marked this conversation as resolved.
};
Comment thread
baywet marked this conversation as resolved.
Comment thread
baywet marked this conversation as resolved.

static HttpMethod? TryCreateHttpMethod(string httpMethod)
{
try
{
return new HttpMethod(httpMethod);
}
catch (FormatException)
{
return null;
}
}
}
Comment thread
baywet marked this conversation as resolved.

/// <summary>
/// Maps the relative path included in the ApiDescription to the path
/// that should be included in the OpenApiDocument. This typically
Expand Down
17 changes: 0 additions & 17 deletions src/OpenApi/src/Services/OpenApiConstants.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Net.Http;

namespace Microsoft.AspNetCore.OpenApi;

internal static class OpenApiConstants
Expand All @@ -19,21 +17,6 @@ internal static class OpenApiConstants
internal const string RefPrefix = "#";
internal const string NullableProperty = "x-is-nullable-property";
internal const string DefaultOpenApiResponseKey = "default";
// Since there's a finite set of HTTP methods that can be included in a given
// OpenApiPaths, we can pre-allocate an array of these methods and use a direct
// lookup on the OpenApiPaths dictionary to avoid allocating an enumerator
// over the KeyValuePairs in OpenApiPaths.
internal static readonly HttpMethod[] HttpMethods = [
HttpMethod.Get,
HttpMethod.Post,
HttpMethod.Put,
HttpMethod.Delete,
HttpMethod.Options,
HttpMethod.Head,
HttpMethod.Patch,
HttpMethod.Trace,
HttpMethod.Query
];
// Represents primitive types that should never be represented as
// a schema reference and always inlined.
internal static readonly List<Type> PrimitiveTypes =
Expand Down
13 changes: 6 additions & 7 deletions src/OpenApi/src/Services/OpenApiDocumentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,14 +163,13 @@ internal async Task ForEachOperationAsync(
{
foreach (var pathItem in document.Paths.Values)
{
for (var i = 0; i < OpenApiConstants.HttpMethods.Length; i++)
if (pathItem.Operations is null)
{
var httpMethod = OpenApiConstants.HttpMethods[i];
if (pathItem.Operations is null || !pathItem.Operations.TryGetValue(httpMethod, out var operation))
{
continue;
}
continue;
}

foreach (var operation in pathItem.Operations.Values)
Comment thread
baywet marked this conversation as resolved.
{
Comment thread
baywet marked this conversation as resolved.
if (operation.Metadata is { } annotations &&
annotations.TryGetValue(OpenApiConstants.DescriptionId, out var descriptionId) &&
descriptionId is string descriptionIdString &&
Expand Down Expand Up @@ -294,7 +293,7 @@ private async Task<Dictionary<HttpMethod, OpenApiOperation>> GetOperationsAsync(

if (description.GetHttpMethod() is not { } method)
{
// Skip unsupported HTTP methods
// Skip descriptions with a missing or invalid HTTP method.
continue;
}
Comment thread
baywet marked this conversation as resolved.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public void MapRelativePathToItemPath_WithRoutePattern_HandlesRoutesThatStartWit

public static class HttpMethodTestData
{
public static IEnumerable<object[]> TestCases => new List<object[]>
public static IEnumerable<object[]> KnownMethods => new List<object[]>
{
new object[] { "GET", HttpMethod.Get },
new object[] { "POST", HttpMethod.Post },
Expand All @@ -69,40 +69,84 @@ public static class HttpMethodTestData
new object[] { "OPTIONS", HttpMethod.Options },
new object[] { "TRACE", HttpMethod.Trace },
new object[] { "QUERY", HttpMethod.Query },
new object[] { "gEt", HttpMethod.Get }, // Test case-insensitivity
new object[] { "gEt", HttpMethod.Get },
new object[] { "pOsT", HttpMethod.Post },
new object[] { "QuErY", HttpMethod.Query },
new object[] { " GET ", HttpMethod.Get },
};

public static IEnumerable<object[]> UnsupportedMethods => new List<object[]>
{
new object[] { "foo", "foo" },
new object[] { "Foo", "Foo" },
new object[] { "FOO", "FOO" },
new object[] { "customMethod", "customMethod" },
new object[] { " FOO ", "FOO" },
new object[] { " FooBar ", "FooBar" },
};

public static IEnumerable<object[]> InvalidMethods => new List<object[]>
{
new object[] { "FOO BAR" },
};
}

[Theory]
[MemberData(nameof(HttpMethodTestData.TestCases), MemberType = typeof(HttpMethodTestData))]
public void GetHttpMethod_ReturnsHttpMethodForApiDescription(string httpMethod, HttpMethod expectedHttpMethod)
[MemberData(nameof(HttpMethodTestData.KnownMethods), MemberType = typeof(HttpMethodTestData))]
public void GetHttpMethod_ReturnsKnownHttpMethodForApiDescription(string httpMethod, HttpMethod expectedHttpMethod)
{
// Arrange
var apiDescription = new ApiDescription
{
HttpMethod = httpMethod
};

// Act
var result = apiDescription.GetHttpMethod();

// Assert
Assert.Equal(expectedHttpMethod, result);
Assert.Equal(expectedHttpMethod.Method, result?.Method);
}

[Fact]
public void GetHttpMethod_ReturnsNullForUnsupportedMethod()
[Theory]
[MemberData(nameof(HttpMethodTestData.UnsupportedMethods), MemberType = typeof(HttpMethodTestData))]
public void GetHttpMethod_PreservesUnsupportedMethodCasingAfterTrimming(string httpMethod, string expectedHttpMethod)
{
// Arrange - Test that unsupported HTTP methods return null
var apiDescription = new ApiDescription
{
HttpMethod = "UNSUPPORTED"
HttpMethod = httpMethod
};

var result = apiDescription.GetHttpMethod();

Assert.Equal(expectedHttpMethod, result?.Method);
}

[Theory]
[MemberData(nameof(HttpMethodTestData.InvalidMethods), MemberType = typeof(HttpMethodTestData))]
public void GetHttpMethod_ReturnsNullForInvalidHttpMethodToken(string httpMethod)
{
var apiDescription = new ApiDescription
{
HttpMethod = httpMethod
};

var result = apiDescription.GetHttpMethod();

Assert.Null(result);
}

[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void GetHttpMethod_ReturnsNullWhenApiDescriptionHasNoHttpMethod(string httpMethod)
{
var apiDescription = new ApiDescription
{
HttpMethod = httpMethod
};

// Act
var result = apiDescription.GetHttpMethod();

// Assert
Assert.Null(result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,37 @@
}
}
},
"/unsupported": {
"x-oai-additionalOperations": {
"FOO": {
"tags": [
"Test"
],
"responses": {
"200": {
"description": "OK",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/CurrentWeather"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/CurrentWeather"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/CurrentWeather"
}
}
}
}
}
}
}
},
"/query-with-body": {
"x-oai-additionalOperations": {
"QUERY": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,37 @@
}
}
},
"/unsupported": {
"x-oai-additionalOperations": {
"FOO": {
"tags": [
"Test"
],
"responses": {
"200": {
"description": "OK",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/CurrentWeather"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/CurrentWeather"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/CurrentWeather"
}
}
}
}
}
}
}
},
"/query-with-body": {
"x-oai-additionalOperations": {
"QUERY": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,37 @@
}
}
},
"/unsupported": {
"additionalOperations": {
"FOO": {
"tags": [
"Test"
],
"responses": {
"200": {
"description": "OK",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/CurrentWeather"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/CurrentWeather"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/CurrentWeather"
}
}
}
}
}
}
}
},
"/query-with-body": {
"query": {
"tags": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1654,6 +1654,37 @@
}
}
},
"/unsupported": {
"x-oai-additionalOperations": {
"FOO": {
"tags": [
"Test"
],
"responses": {
"200": {
"description": "OK",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/CurrentWeather"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/CurrentWeather"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/CurrentWeather"
}
}
}
}
}
}
}
},
"/query-with-body": {
"x-oai-additionalOperations": {
"QUERY": {
Expand Down
Loading
Loading