From d2f0c895fb411938957c20fe870f9bf61a95c728 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Thu, 24 Oct 2024 14:32:15 -0700 Subject: [PATCH 1/2] Support returning OpenAPI document in YAML from MapOpenApi --- src/OpenApi/sample/Program.cs | 1 + .../OpenApiEndpointRouteBuilderExtensions.cs | 16 +++++++++-- ...nApiEndpointRouteBuilderExtensionsTests.cs | 28 +++++++++++++------ 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/OpenApi/sample/Program.cs b/src/OpenApi/sample/Program.cs index a622780ff482..8a6182c75b0c 100644 --- a/src/OpenApi/sample/Program.cs +++ b/src/OpenApi/sample/Program.cs @@ -37,6 +37,7 @@ var app = builder.Build(); app.MapOpenApi(); +app.MapOpenApi("/openapi/{documentName}.yaml"); if (app.Environment.IsDevelopment()) { app.MapSwaggerUi(); diff --git a/src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs b/src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs index c5bed38669e4..5a35f406e274 100644 --- a/src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs +++ b/src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs @@ -49,8 +49,16 @@ public static IEndpointConventionBuilder MapOpenApi(this IEndpointRouteBuilder e using var writer = Utf8BufferTextWriter.Get(output); try { - document.Serialize(new OpenApiJsonWriter(writer), documentOptions.OpenApiVersion); - context.Response.ContentType = "application/json;charset=utf-8"; + if (UseYaml(pattern)) + { + document.Serialize(new OpenApiYamlWriter(writer), documentOptions.OpenApiVersion); + context.Response.ContentType = "application/yaml;charset=utf-8"; + } + else + { + document.Serialize(new OpenApiJsonWriter(writer), documentOptions.OpenApiVersion); + context.Response.ContentType = "application/json;charset=utf-8"; + } await context.Response.BodyWriter.WriteAsync(output.ToArray(), context.RequestAborted); await context.Response.BodyWriter.FlushAsync(context.RequestAborted); } @@ -63,4 +71,8 @@ public static IEndpointConventionBuilder MapOpenApi(this IEndpointRouteBuilder e } }).ExcludeFromDescription(); } + + private static bool UseYaml(string pattern) => + pattern.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase) || + pattern.EndsWith(".yml", StringComparison.OrdinalIgnoreCase); } diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/OpenApiEndpointRouteBuilderExtensionsTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/OpenApiEndpointRouteBuilderExtensionsTests.cs index be44b44344ce..ad5673be1fea 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/OpenApiEndpointRouteBuilderExtensionsTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/OpenApiEndpointRouteBuilderExtensionsTests.cs @@ -29,11 +29,13 @@ public void MapOpenApi_ReturnsEndpointConventionBuilder() Assert.IsAssignableFrom(returnedBuilder); } - [Fact] - public void MapOpenApi_SupportsCustomizingPath() + [Theory] + [InlineData("/custom/{documentName}/openapi.json")] + [InlineData("/custom/{documentName}/openapi.yaml")] + [InlineData("/custom/{documentName}/openapi.yml")] + public void MapOpenApi_SupportsCustomizingPath(string expectedPath) { // Arrange - var expectedPath = "/custom/{documentName}/openapi.json"; var serviceProvider = CreateServiceProvider(); var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(serviceProvider)); @@ -72,13 +74,16 @@ public async Task MapOpenApi_ReturnsRenderedDocument() }); } - [Fact] - public async Task MapOpenApi_ReturnsDefaultDocumentIfNoNameProvided() + [Theory] + [InlineData("/openapi.json", "application/json;charset=utf-8")] + [InlineData("/openapi.yaml", "application/yaml;charset=utf-8")] + [InlineData("/openapi.yml", "application/yaml;charset=utf-8")] + public async Task MapOpenApi_ReturnsDefaultDocumentIfNoNameProvided(string expectedPath, string expectedContentType) { // Arrange var serviceProvider = CreateServiceProvider(); var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(serviceProvider)); - builder.MapOpenApi("/openapi.json"); + builder.MapOpenApi(expectedPath); var context = new DefaultHttpContext(); var responseBodyStream = new MemoryStream(); context.Response.Body = responseBodyStream; @@ -91,6 +96,7 @@ public async Task MapOpenApi_ReturnsDefaultDocumentIfNoNameProvided() // Assert Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); + Assert.Equal(expectedContentType, context.Response.ContentType); ValidateOpenApiDocument(responseBodyStream, document => { Assert.Equal("OpenApiEndpointRouteBuilderExtensionsTests | v1", document.Info.Title); @@ -121,8 +127,11 @@ public async Task MapOpenApi_Returns404ForUnresolvedDocument() Assert.Equal("No OpenAPI document with the name 'v2' was found.", Encoding.UTF8.GetString(responseBodyStream.ToArray())); } - [Fact] - public async Task MapOpenApi_ReturnsDocumentIfNameProvidedInQuery() + [Theory] + [InlineData("/openapi.json", "application/json;charset=utf-8")] + [InlineData("/openapi.yaml", "application/yaml;charset=utf-8")] + [InlineData("/openapi.yml", "application/yaml;charset=utf-8")] + public async Task MapOpenApi_ReturnsDocumentIfNameProvidedInQuery(string expectedPath, string expectedContentType) { // Arrange var documentName = "v2"; @@ -130,7 +139,7 @@ public async Task MapOpenApi_ReturnsDocumentIfNameProvidedInQuery() var serviceProviderIsService = new ServiceProviderIsService(); var serviceProvider = CreateServiceProvider(documentName); var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(serviceProvider)); - builder.MapOpenApi("/openapi.json"); + builder.MapOpenApi(expectedPath); var context = new DefaultHttpContext(); var responseBodyStream = new MemoryStream(); context.Response.Body = responseBodyStream; @@ -144,6 +153,7 @@ public async Task MapOpenApi_ReturnsDocumentIfNameProvidedInQuery() // Assert Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); + Assert.Equal(expectedContentType, context.Response.ContentType); ValidateOpenApiDocument(responseBodyStream, document => { Assert.Equal($"OpenApiEndpointRouteBuilderExtensionsTests | {documentName}", document.Info.Title); From a9b0993ea061f867b5bf599d4055d6a3a8528ca0 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Fri, 8 Nov 2024 13:09:34 -0800 Subject: [PATCH 2/2] Update tests and YAML content type --- .../OpenApiEndpointRouteBuilderExtensions.cs | 2 +- ...nApiEndpointRouteBuilderExtensionsTests.cs | 25 +++++++++++++------ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs b/src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs index 5a35f406e274..def54f8c8d95 100644 --- a/src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs +++ b/src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs @@ -52,7 +52,7 @@ public static IEndpointConventionBuilder MapOpenApi(this IEndpointRouteBuilder e if (UseYaml(pattern)) { document.Serialize(new OpenApiYamlWriter(writer), documentOptions.OpenApiVersion); - context.Response.ContentType = "application/yaml;charset=utf-8"; + context.Response.ContentType = "text/plain+yaml;charset=utf-8"; } else { diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/OpenApiEndpointRouteBuilderExtensionsTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/OpenApiEndpointRouteBuilderExtensionsTests.cs index ad5673be1fea..5ef1079759e9 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/OpenApiEndpointRouteBuilderExtensionsTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/OpenApiEndpointRouteBuilderExtensionsTests.cs @@ -75,10 +75,11 @@ public async Task MapOpenApi_ReturnsRenderedDocument() } [Theory] - [InlineData("/openapi.json", "application/json;charset=utf-8")] - [InlineData("/openapi.yaml", "application/yaml;charset=utf-8")] - [InlineData("/openapi.yml", "application/yaml;charset=utf-8")] - public async Task MapOpenApi_ReturnsDefaultDocumentIfNoNameProvided(string expectedPath, string expectedContentType) + [InlineData("/openapi.json", "application/json;charset=utf-8", false)] + [InlineData("/openapi.toml", "application/json;charset=utf-8", false)] + [InlineData("/openapi.yaml", "text/plain+yaml;charset=utf-8", true)] + [InlineData("/openapi.yml", "text/plain+yaml;charset=utf-8", true)] + public async Task MapOpenApi_ReturnsDefaultDocumentIfNoNameProvided(string expectedPath, string expectedContentType, bool isYaml) { // Arrange var serviceProvider = CreateServiceProvider(); @@ -97,6 +98,10 @@ public async Task MapOpenApi_ReturnsDefaultDocumentIfNoNameProvided(string expec // Assert Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); Assert.Equal(expectedContentType, context.Response.ContentType); + var responseString = Encoding.UTF8.GetString(responseBodyStream.ToArray()); + // String check to validate that generated document starts with YAML syntax + Assert.Equal(isYaml, responseString.StartsWith("openapi: 3.0.1", StringComparison.OrdinalIgnoreCase)); + responseBodyStream.Position = 0; ValidateOpenApiDocument(responseBodyStream, document => { Assert.Equal("OpenApiEndpointRouteBuilderExtensionsTests | v1", document.Info.Title); @@ -128,10 +133,10 @@ public async Task MapOpenApi_Returns404ForUnresolvedDocument() } [Theory] - [InlineData("/openapi.json", "application/json;charset=utf-8")] - [InlineData("/openapi.yaml", "application/yaml;charset=utf-8")] - [InlineData("/openapi.yml", "application/yaml;charset=utf-8")] - public async Task MapOpenApi_ReturnsDocumentIfNameProvidedInQuery(string expectedPath, string expectedContentType) + [InlineData("/openapi.json", "application/json;charset=utf-8", false)] + [InlineData("/openapi.yaml", "text/plain+yaml;charset=utf-8", true)] + [InlineData("/openapi.yml", "text/plain+yaml;charset=utf-8", true)] + public async Task MapOpenApi_ReturnsDocumentIfNameProvidedInQuery(string expectedPath, string expectedContentType, bool isYaml) { // Arrange var documentName = "v2"; @@ -154,6 +159,10 @@ public async Task MapOpenApi_ReturnsDocumentIfNameProvidedInQuery(string expecte // Assert Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); Assert.Equal(expectedContentType, context.Response.ContentType); + var responseString = Encoding.UTF8.GetString(responseBodyStream.ToArray()); + // String check to validate that generated document starts with YAML syntax + Assert.Equal(isYaml, responseString.StartsWith("openapi: 3.0.1", StringComparison.OrdinalIgnoreCase)); + responseBodyStream.Position = 0; ValidateOpenApiDocument(responseBodyStream, document => { Assert.Equal($"OpenApiEndpointRouteBuilderExtensionsTests | {documentName}", document.Info.Title);