Skip to content

Commit

Permalink
Add customized document serialization support (#2677)
Browse files Browse the repository at this point in the history
Added new `ISwaggerDocumentSerializer` interface for custom serialization support.
  • Loading branch information
remcolam authored Apr 25, 2024
1 parent bb51243 commit 12c4a96
Show file tree
Hide file tree
Showing 24 changed files with 552 additions and 336 deletions.
277 changes: 8 additions & 269 deletions Swashbuckle.AspNetCore.sln

Large diffs are not rendered by default.

35 changes: 28 additions & 7 deletions src/Swashbuckle.AspNetCore.Cli/Program.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
using System;
using System.Reflection;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Runtime.Loader;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using System.Threading;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Writers;
using Swashbuckle.AspNetCore.Swagger;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore;
using Microsoft.Extensions.Hosting;

namespace Swashbuckle.AspNetCore.Cli
{
Expand Down Expand Up @@ -87,6 +90,8 @@ public static int Main(string[] args)

// 3) Retrieve Swagger via configured provider
var swaggerProvider = serviceProvider.GetRequiredService<ISwaggerProvider>();
var swaggerOptions = serviceProvider.GetService<IOptions<SwaggerOptions>>();
var swaggerDocumentSerializer = swaggerOptions?.Value?.CustomDocumentSerializer;
var swagger = swaggerProvider.GetSwagger(
namedArgs["swaggerdoc"],
namedArgs.TryGetValue("--host", out var arg) ? arg : null,
Expand All @@ -97,7 +102,8 @@ public static int Main(string[] args)
? Path.Combine(Directory.GetCurrentDirectory(), arg1)
: null;

using (var streamWriter = (outputPath != null ? File.CreateText(outputPath) : Console.Out))
using (Stream stream = (outputPath != null ? File.OpenWrite(outputPath) : Console.OpenStandardOutput()))
using (var streamWriter = new FormattingStreamWriter(stream, CultureInfo.InvariantCulture))
{
IOpenApiWriter writer;
if (namedArgs.ContainsKey("--yaml"))
Expand All @@ -106,9 +112,24 @@ public static int Main(string[] args)
writer = new OpenApiJsonWriter(streamWriter);

if (namedArgs.ContainsKey("--serializeasv2"))
swagger.SerializeAsV2(writer);
{
if (swaggerDocumentSerializer != null)
{
swaggerDocumentSerializer.SerializeDocument(swagger, writer, Microsoft.OpenApi.OpenApiSpecVersion.OpenApi2_0);
}
else
{
swagger.SerializeAsV2(writer);
}
}
else if (swaggerDocumentSerializer != null)
{
swaggerDocumentSerializer.SerializeDocument(swagger, writer, Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_0);
}
else
{
swagger.SerializeAsV3(writer);
}

if (outputPath != null)
Console.WriteLine($"Swagger JSON/YAML successfully written to {outputPath}");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using Swashbuckle.AspNetCore.Swagger;

namespace Microsoft.Extensions.DependencyInjection;

/// <summary>
/// Extensions for helping with configuring instances of <see cref="SwaggerOptions"/>.
/// </summary>
public static class SwaggerOptionsExtensions
{
/// <summary>
/// Sets a custom Swagger document serializer to use.
/// </summary>
/// <remarks>For the CLI tool to be able to use this, this needs to be configured for use in the service collection of your application.</remarks>
/// <typeparam name="TDocumentSerializer">The type of the custom Swagger document serializer implementation.</typeparam>
/// <param name="swaggerOptions">The options to configure the serializer for.</param>
/// <param name="constructorParameters">The parameters to pass into the constructor of the custom Swagger document serializer implementation.</param>
public static void SetCustomDocumentSerializer<TDocumentSerializer>(
this SwaggerOptions swaggerOptions,
params object[] constructorParameters)
where TDocumentSerializer : ISwaggerDocumentSerializer
{
if (swaggerOptions == null)
{
throw new ArgumentNullException(nameof(swaggerOptions));
}
swaggerOptions.CustomDocumentSerializer = (TDocumentSerializer)Activator.CreateInstance(typeof(TDocumentSerializer), constructorParameters);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;
using Swashbuckle.AspNetCore.Swagger;

namespace Microsoft.Extensions.DependencyInjection;

/// <summary>
/// Extensions to configure dependencies for Swagger.
/// </summary>
public static class SwaggerServiceCollectionExtensions
{
/// <summary>
/// Configures Swagger options in the specified service collection.
/// </summary>
/// <param name="services">The service collection to configure the Swagger options for.</param>
/// <param name="setupAction">A delegate to a method to use to configure the Swagger options.</param>
public static void ConfigureSwagger(
this IServiceCollection services,
Action<SwaggerOptions> setupAction)
{
services.Configure(setupAction);
}
}
20 changes: 20 additions & 0 deletions src/Swashbuckle.AspNetCore.Swagger/ISwaggerDocumentSerializer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Microsoft.OpenApi;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Writers;

namespace Swashbuckle.AspNetCore.Swagger
{
/// <summary>
/// Provide an implementation for this interface if you wish to customize how the OpenAPI document is written.
/// </summary>
public interface ISwaggerDocumentSerializer
{
/// <summary>
/// Serializes an OpenAPI document.
/// </summary>
/// <param name="document">The OpenAPI document that should be serialized.</param>
/// <param name="writer">The writer to which the document needs to be written.</param>
/// <param name="specVersion">The OpenAPI specification version to serialize as.</param>
void SerializeDocument(OpenApiDocument document, IOpenApiWriter writer, OpenApiSpecVersion specVersion);
}
}
34 changes: 31 additions & 3 deletions src/Swashbuckle.AspNetCore.Swagger/SwaggerMiddleware.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Globalization;
using System;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading.Tasks;
Expand All @@ -8,6 +9,7 @@
using Microsoft.AspNetCore.Routing.Patterns;
#endif
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Writers;

Expand Down Expand Up @@ -134,7 +136,20 @@ private async Task RespondWithSwaggerJson(HttpResponse response, OpenApiDocument
using (var textWriter = new StringWriter(CultureInfo.InvariantCulture))
{
var jsonWriter = new OpenApiJsonWriter(textWriter);
if (_options.SerializeAsV2) swagger.SerializeAsV2(jsonWriter); else swagger.SerializeAsV3(jsonWriter);
if (_options.SerializeAsV2)
{
if (_options.CustomDocumentSerializer != null)
_options.CustomDocumentSerializer.SerializeDocument(swagger, jsonWriter, Microsoft.OpenApi.OpenApiSpecVersion.OpenApi2_0);
else
swagger.SerializeAsV2(jsonWriter);
}
else
{
if (_options.CustomDocumentSerializer != null)
_options.CustomDocumentSerializer.SerializeDocument(swagger, jsonWriter, Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_0);
else
swagger.SerializeAsV3(jsonWriter);
}

await response.WriteAsync(textWriter.ToString(), new UTF8Encoding(false));
}
Expand All @@ -148,7 +163,20 @@ private async Task RespondWithSwaggerYaml(HttpResponse response, OpenApiDocument
using (var textWriter = new StringWriter(CultureInfo.InvariantCulture))
{
var yamlWriter = new OpenApiYamlWriter(textWriter);
if (_options.SerializeAsV2) swagger.SerializeAsV2(yamlWriter); else swagger.SerializeAsV3(yamlWriter);
if (_options.SerializeAsV2)
{
if (_options.CustomDocumentSerializer != null)
_options.CustomDocumentSerializer.SerializeDocument(swagger, yamlWriter, Microsoft.OpenApi.OpenApiSpecVersion.OpenApi2_0);
else
swagger.SerializeAsV2(yamlWriter);
}
else
{
if (_options.CustomDocumentSerializer != null)
_options.CustomDocumentSerializer.SerializeDocument(swagger, yamlWriter, Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_0);
else
swagger.SerializeAsV3(yamlWriter);
}

await response.WriteAsync(textWriter.ToString(), new UTF8Encoding(false));
}
Expand Down
8 changes: 7 additions & 1 deletion src/Swashbuckle.AspNetCore.Swagger/SwaggerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,15 @@ public SwaggerOptions()
/// </summary>
public bool SerializeAsV2 { get; set; }

/// <summary>
/// Gets or sets an optional custom <see cref="ISwaggerDocumentSerializer"/> implementation to use to serialize Swagger documents.
/// </summary>
/// <remarks>For the CLI tool to be able to use this, this needs to be configured for use in the service collection of your application.</remarks>
public ISwaggerDocumentSerializer CustomDocumentSerializer { get; set; }

/// <summary>
/// Actions that can be applied to an OpenApiDocument before it's serialized.
/// Useful for setting metadata that's derived from the current request
/// Useful for setting metadata that's derived from the current request.
/// </summary>
public List<Action<OpenApiDocument, HttpRequest>> PreSerializeFilters { get; private set; }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Writers;
using Swashbuckle.AspNetCore.Swagger;
Expand Down Expand Up @@ -47,11 +49,17 @@ public async Task GenerateAsync(string documentName, TextWriter writer)
var jsonWriter = new OpenApiJsonWriter(writer);
if (_options.SerializeAsV2)
{
swagger.SerializeAsV2(jsonWriter);
if (_options.CustomDocumentSerializer != null)
_options.CustomDocumentSerializer.SerializeDocument(swagger, jsonWriter, OpenApi.OpenApiSpecVersion.OpenApi2_0);
else
swagger.SerializeAsV2(jsonWriter);
}
else
{
swagger.SerializeAsV3(jsonWriter);
if (_options.CustomDocumentSerializer != null)
_options.CustomDocumentSerializer.SerializeDocument(swagger, jsonWriter, OpenApi.OpenApiSpecVersion.OpenApi3_0);
else
swagger.SerializeAsV3(jsonWriter);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ public class FilterDescriptor

public object[] Arguments { get; set; }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Swashbuckle.AspNetCore.TestSupport\Swashbuckle.AspNetCore.TestSupport.csproj" />
<ProjectReference Include="..\WebSites\Basic\Basic.csproj" />
<ProjectReference Include="..\WebSites\CustomDocumentSerializer\CustomDocumentSerializer.csproj" />
<ProjectReference Include="..\WebSites\MinimalApp\MinimalApp.csproj" />
<ProjectReference Include="..\..\src\Swashbuckle.AspNetCore.Cli\Swashbuckle.AspNetCore.Cli.csproj" />
</ItemGroup>
Expand Down
82 changes: 48 additions & 34 deletions test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.IO;
using System.Text.Json;
using Swashbuckle.AspNetCore.TestSupport.Utilities;
using Xunit;

namespace Swashbuckle.AspNetCore.Cli.Test
Expand All @@ -16,46 +17,59 @@ public void Throws_When_Startup_Assembly_Does_Not_Exist()
[Fact]
public void Can_Generate_Swagger_Json()
{
var dir = Directory.CreateDirectory(Path.Join(Path.GetTempPath(), Path.GetRandomFileName()));
try
{
var args = new string[] { "tofile", "--output", $"{dir}/swagger.json", "--serializeasv2", Path.Combine(Directory.GetCurrentDirectory(), "Basic.dll"), "v1" };
Assert.Equal(0, Program.Main(args));

using var document = JsonDocument.Parse(File.ReadAllText(Path.Combine(dir.FullName, "swagger.json")));

// verify one of the endpoints
var paths = document.RootElement.GetProperty("paths");
var productsPath = paths.GetProperty("/products");
Assert.True(productsPath.TryGetProperty("post", out _));
}
finally
{
dir.Delete(true);
}
using var temporaryDirectory = new TemporaryDirectory();
var args = new string[] { "tofile", "--output", $"{temporaryDirectory.Path}/swagger.json", "--serializeasv2", Path.Combine(Directory.GetCurrentDirectory(), "Basic.dll"), "v1" };
Assert.Equal(0, Program.Main(args));
using var document = JsonDocument.Parse(File.ReadAllText(Path.Combine(temporaryDirectory.Path, "swagger.json")));

// verify one of the endpoints
var paths = document.RootElement.GetProperty("paths");
var productsPath = paths.GetProperty("/products");
Assert.True(productsPath.TryGetProperty("post", out _));
}

[Fact]
public void CustomDocumentSerializer_Writes_Custom_V2_Document()
{
using var temporaryDirectory = new TemporaryDirectory();
var args = new string[] { "tofile", "--output", $"{temporaryDirectory.Path}/swagger.json", "--serializeasv2", Path.Combine(Directory.GetCurrentDirectory(), "CustomDocumentSerializer.dll"), "v1" };
Assert.Equal(0, Program.Main(args));

using var document = JsonDocument.Parse(File.ReadAllText(Path.Combine(temporaryDirectory.Path, "swagger.json")));

// verify that the custom serializer wrote the swagger info
var swaggerInfo = document.RootElement.GetProperty("swagger").GetString();
Assert.Equal("DocumentSerializerTest2.0", swaggerInfo);
}

[Fact]
public void CustomDocumentSerializer_Writes_Custom_V3_Document()
{
using var temporaryDirectory = new TemporaryDirectory();
var args = new string[] { "tofile", "--output", $"{temporaryDirectory.Path}/swagger.json", Path.Combine(Directory.GetCurrentDirectory(), "CustomDocumentSerializer.dll"), "v1" };
Assert.Equal(0, Program.Main(args));

using var document = JsonDocument.Parse(File.ReadAllText(Path.Combine(temporaryDirectory.Path, "swagger.json")));

// verify that the custom serializer wrote the swagger info
var swaggerInfo = document.RootElement.GetProperty("swagger").GetString();
Assert.Equal("DocumentSerializerTest3.0", swaggerInfo);
}

#if NET6_0_OR_GREATER
[Fact]
public void Can_Generate_Swagger_Json_ForTopLevelApp()
{
var dir = Directory.CreateDirectory(Path.Join(Path.GetTempPath(), Path.GetRandomFileName()));
try
{
var args = new string[] { "tofile", "--output", $"{dir}/swagger.json", "--serializeasv2", Path.Combine(Directory.GetCurrentDirectory(), "MinimalApp.dll"), "v1" };
Assert.Equal(0, Program.Main(args));

using var document = JsonDocument.Parse(File.ReadAllText(Path.Combine(dir.FullName, "swagger.json")));

// verify one of the endpoints
var paths = document.RootElement.GetProperty("paths");
var path = paths.GetProperty("/WeatherForecast");
Assert.True(path.TryGetProperty("get", out _));
}
finally
{
dir.Delete(true);
}
using var temporaryDirectory = new TemporaryDirectory();
var args = new string[] { "tofile", "--output", $"{temporaryDirectory.Path}/swagger.json", "--serializeasv2", Path.Combine(Directory.GetCurrentDirectory(), "MinimalApp.dll"), "v1" };
Assert.Equal(0, Program.Main(args));

using var document = JsonDocument.Parse(File.ReadAllText(Path.Combine(temporaryDirectory.Path, "swagger.json")));

// verify one of the endpoints
var paths = document.RootElement.GetProperty("paths");
var path = paths.GetProperty("/WeatherForecast");
Assert.True(path.TryGetProperty("get", out _));
}
#endif
}
Expand Down
Loading

0 comments on commit 12c4a96

Please sign in to comment.