From f1a773a1b6378f213cc919db96b4ae5fdad7ed80 Mon Sep 17 00:00:00 2001 From: tuttb Date: Sat, 15 Feb 2025 21:15:41 -0500 Subject: [PATCH 1/7] Add 2 new bits of functionality: * ISwaggerProvider/IAsyncSwaggerProvider: Add GetSwaggerDocumentNames method and implementation (and test coverage). This fixes #472. * SwaggerUi: Add options to control the behavior of a new /swagger/documentUrls route that just returns the JSON version of ConfigOption.Urls. You can see this working in the MultipleVersions website. --- .../IAsyncSwaggerProvider.cs | 2 + .../ISwaggerProvider.cs | 4 +- .../PublicAPI/PublicAPI.Unshipped.txt | 2 + .../PublicAPI/PublicAPI.Unshipped.txt | 1 + .../SwaggerGenerator/SwaggerGenerator.cs | 5 +++ .../PublicAPI/PublicAPI.Unshipped.txt | 5 +++ .../SwaggerUIJsonSerializerContext.cs | 2 + .../SwaggerUIMiddleware.cs | 41 ++++++++++++++++++- .../SwaggerUIOptions.cs | 10 +++++ .../SwaggerUIOptionsExtensions.cs | 9 +++- .../SwaggerGenerator/SwaggerGeneratorTests.cs | 8 ++++ test/WebSites/MultipleVersions/Startup.cs | 1 + 12 files changed, 86 insertions(+), 4 deletions(-) diff --git a/src/Swashbuckle.AspNetCore.Swagger/IAsyncSwaggerProvider.cs b/src/Swashbuckle.AspNetCore.Swagger/IAsyncSwaggerProvider.cs index 9d6036798d..b4e6603f95 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/IAsyncSwaggerProvider.cs +++ b/src/Swashbuckle.AspNetCore.Swagger/IAsyncSwaggerProvider.cs @@ -9,5 +9,7 @@ Task GetSwaggerAsync( string documentName, string host = null, string basePath = null); + + string[] GetSwaggerDocumentNames(); } } diff --git a/src/Swashbuckle.AspNetCore.Swagger/ISwaggerProvider.cs b/src/Swashbuckle.AspNetCore.Swagger/ISwaggerProvider.cs index eaba415b72..da4b63779c 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/ISwaggerProvider.cs +++ b/src/Swashbuckle.AspNetCore.Swagger/ISwaggerProvider.cs @@ -11,6 +11,8 @@ OpenApiDocument GetSwagger( string documentName, string host = null, string basePath = null); + + string[] GetSwaggerDocumentNames(); } public class UnknownSwaggerDocument : InvalidOperationException @@ -21,4 +23,4 @@ public UnknownSwaggerDocument(string documentName, IEnumerable knownDocu string.Join(",", knownDocuments?.Select(x => $"\"{x}\"")))) {} } -} \ No newline at end of file +} diff --git a/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/PublicAPI.Unshipped.txt b/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/PublicAPI.Unshipped.txt index e69de29bb2..8fbfbfb242 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +Swashbuckle.AspNetCore.Swagger.IAsyncSwaggerProvider.GetSwaggerDocumentNames() -> string[] +Swashbuckle.AspNetCore.Swagger.ISwaggerProvider.GetSwaggerDocumentNames() -> string[] \ No newline at end of file diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/PublicAPI/PublicAPI.Unshipped.txt b/src/Swashbuckle.AspNetCore.SwaggerGen/PublicAPI/PublicAPI.Unshipped.txt index ecfd63780b..4b28e8ee4d 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/PublicAPI/PublicAPI.Unshipped.txt @@ -1,4 +1,5 @@ static Swashbuckle.AspNetCore.SwaggerGen.OpenApiAnyFactory.CreateFromJson(string json, System.Text.Json.JsonSerializerOptions options) -> Microsoft.OpenApi.Any.IOpenApiAny static Swashbuckle.AspNetCore.SwaggerGen.XmlCommentsTextHelper.Humanize(string text, string xmlCommentEndOfLine) -> string +Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GetSwaggerDocumentNames() -> string[] Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorOptions.XmlCommentEndOfLine.get -> string Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorOptions.XmlCommentEndOfLine.set -> void diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs index 83a410b16b..a76308bea6 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs @@ -112,6 +112,11 @@ public OpenApiDocument GetSwagger(string documentName, string host = null, strin } } + public string[] GetSwaggerDocumentNames() + { + return _options.SwaggerDocs.Keys.ToArray(); + } + private void SortSchemas(OpenApiDocument document) { document.Components.Schemas = new SortedDictionary(document.Components.Schemas, _options.SchemaComparer); diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/PublicAPI/PublicAPI.Unshipped.txt b/src/Swashbuckle.AspNetCore.SwaggerUI/PublicAPI/PublicAPI.Unshipped.txt index e69de29bb2..39bf1c2b5a 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/PublicAPI/PublicAPI.Unshipped.txt @@ -0,0 +1,5 @@ +static Microsoft.AspNetCore.Builder.SwaggerUIOptionsExtensions.EnableSwaggerDocumentUrlsRoute(this Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIOptions options) -> void +Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIOptions.ExposeSwaggerDocumentUrlsRoute.get -> bool +Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIOptions.ExposeSwaggerDocumentUrlsRoute.set -> void +Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIOptions.SwaggerDocumentUrlsPath.get -> string +Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIOptions.SwaggerDocumentUrlsPath.set -> void diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIJsonSerializerContext.cs b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIJsonSerializerContext.cs index 72e175fb6e..413761e753 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIJsonSerializerContext.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIJsonSerializerContext.cs @@ -1,5 +1,6 @@ #if NET6_0_OR_GREATER using System; +using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; @@ -7,6 +8,7 @@ namespace Swashbuckle.AspNetCore.SwaggerUI; [JsonSerializable(typeof(ConfigObject))] +[JsonSerializable(typeof(List))] [JsonSerializable(typeof(InterceptorFunctions))] [JsonSerializable(typeof(OAuthConfigObject))] // These primitive types are declared for common types that may be used with ConfigObject.AdditionalItems. See https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2884. diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs index 4a65cb7cf9..7def9d1d5b 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; @@ -88,6 +90,13 @@ public async Task Invoke(HttpContext httpContext) await RespondWithFile(httpContext.Response, match.Groups[1].Value); return; } + + var pattern = $"^/?{Regex.Escape(_options.RoutePrefix)}/{_options.SwaggerDocumentUrlsPath}/?$"; + if (Regex.IsMatch(path, pattern, RegexOptions.IgnoreCase)) + { + await RespondWithDocumentUrls(httpContext.Response); + return; + } } await _staticFileMiddleware.Invoke(httpContext); @@ -150,6 +159,36 @@ private async Task RespondWithFile(HttpResponse response, string fileName) } } +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage( + "AOT", + "IL2026:RequiresUnreferencedCode", + Justification = "Method is only called if the user provides their own custom JsonSerializerOptions.")] + [UnconditionalSuppressMessage( + "AOT", + "IL3050:RequiresDynamicCode", + Justification = "Method is only called if the user provides their own custom JsonSerializerOptions.")] +#endif + private async Task RespondWithDocumentUrls(HttpResponse response) + { + response.StatusCode = 200; + + response.ContentType = "application/javascript;charset=utf-8"; + string json = "[]"; + +#if NET6_0_OR_GREATER + if (_jsonSerializerOptions is null) + { + var l = new List(_options.ConfigObject.Urls); + json = JsonSerializer.Serialize(l, SwaggerUIOptionsJsonContext.Default.ListUrlDescriptor); + } +#endif + + json ??= JsonSerializer.Serialize(_options.ConfigObject, _jsonSerializerOptions); + + await response.WriteAsync(json, Encoding.UTF8); + } + #if NET5_0_OR_GREATER [UnconditionalSuppressMessage( "AOT", diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptions.cs b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptions.cs index 09fc304abe..7672553a3b 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptions.cs @@ -63,6 +63,16 @@ public class SwaggerUIOptions /// Gets or sets the path or URL to the Swagger UI CSS file. /// public string StylesPath { get; set; } = "./swagger-ui.css"; + + /// + /// Expose the ConfigObject.Urls object via a new <root>/documentUrls + /// + public bool ExposeSwaggerDocumentUrlsRoute { get; set; } = false; + + /// + /// Gets or sets the relative URL path to the documentUrls route. + /// + public string SwaggerDocumentUrlsPath { get; set; } = "documentUrls"; } public class ConfigObject diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptionsExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptionsExtensions.cs index a5b05fa946..fe73533181 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptionsExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptionsExtensions.cs @@ -203,7 +203,7 @@ public static void ValidatorUrl(this SwaggerUIOptions options, string url) /// /// You can use this parameter to enable the swagger-ui's built-in validator (badge) functionality - /// Setting it to null will disable validation + /// Setting it to null will disable validation /// /// /// @@ -239,7 +239,7 @@ public static void OAuthUsername(this SwaggerUIOptions options, string value) /// /// Setting this exposes the client secrets in inline javascript in the swagger-ui generated html. public static void OAuthClientSecret(this SwaggerUIOptions options, string value) - { + { options.OAuthConfigObject.ClientSecret = value; } @@ -333,5 +333,10 @@ public static void UseResponseInterceptor(this SwaggerUIOptions options, string { options.Interceptors.ResponseInterceptorFunction = value; } + + public static void EnableSwaggerDocumentUrlsRoute(this SwaggerUIOptions options) + { + options.ExposeSwaggerDocumentUrlsRoute = true; + } } } diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs index 9631d56e31..e3e196cdf0 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs @@ -50,6 +50,14 @@ public void GetSwagger_GeneratesSwaggerDocument_ForApiDescriptionsWithMatchingGr } ); + var provider = subject as ISwaggerProvider; + var documentNames = provider.GetSwaggerDocumentNames(); + Assert.Equal(new[] { "v1", "v2" }, documentNames); + + var asyncProvider = subject as IAsyncSwaggerProvider; + documentNames = provider.GetSwaggerDocumentNames(); + Assert.Equal(new[] { "v1", "v2" }, documentNames); + var document = subject.GetSwagger("v1"); Assert.Equal("V1", document.Info.Version); diff --git a/test/WebSites/MultipleVersions/Startup.cs b/test/WebSites/MultipleVersions/Startup.cs index d7e47b398a..739a75147f 100644 --- a/test/WebSites/MultipleVersions/Startup.cs +++ b/test/WebSites/MultipleVersions/Startup.cs @@ -59,6 +59,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVers { c.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", $"Version {description.GroupName}"); } + c.EnableSwaggerDocumentUrlsRoute(); }); // Separate endpoints that contain only one version From 489f82483a772f801480a3f7faed578450662981 Mon Sep 17 00:00:00 2001 From: tuttb Date: Sun, 16 Feb 2025 10:29:46 -0500 Subject: [PATCH 2/7] Pull the document names method into a new interface. --- .../IAsyncSwaggerProvider.cs | 2 -- .../ISwaggerDocumentInformation.cs | 14 ++++++++++++++ .../ISwaggerProvider.cs | 2 -- .../PublicAPI/PublicAPI.Unshipped.txt | 4 ++-- .../PublicAPI/PublicAPI.Unshipped.txt | 2 +- .../SwaggerGenerator/SwaggerGenerator.cs | 4 ++-- .../SwaggerGenerator/SwaggerGeneratorTests.cs | 8 ++------ 7 files changed, 21 insertions(+), 15 deletions(-) create mode 100644 src/Swashbuckle.AspNetCore.Swagger/ISwaggerDocumentInformation.cs diff --git a/src/Swashbuckle.AspNetCore.Swagger/IAsyncSwaggerProvider.cs b/src/Swashbuckle.AspNetCore.Swagger/IAsyncSwaggerProvider.cs index b4e6603f95..9d6036798d 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/IAsyncSwaggerProvider.cs +++ b/src/Swashbuckle.AspNetCore.Swagger/IAsyncSwaggerProvider.cs @@ -9,7 +9,5 @@ Task GetSwaggerAsync( string documentName, string host = null, string basePath = null); - - string[] GetSwaggerDocumentNames(); } } diff --git a/src/Swashbuckle.AspNetCore.Swagger/ISwaggerDocumentInformation.cs b/src/Swashbuckle.AspNetCore.Swagger/ISwaggerDocumentInformation.cs new file mode 100644 index 0000000000..5198f5e3ec --- /dev/null +++ b/src/Swashbuckle.AspNetCore.Swagger/ISwaggerDocumentInformation.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.OpenApi.Models; + +namespace Swashbuckle.AspNetCore.Swagger +{ + public interface ISwaggerDocumentInformation + { + string[] GetDocumentNames(); + } +} diff --git a/src/Swashbuckle.AspNetCore.Swagger/ISwaggerProvider.cs b/src/Swashbuckle.AspNetCore.Swagger/ISwaggerProvider.cs index da4b63779c..15fb8f9306 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/ISwaggerProvider.cs +++ b/src/Swashbuckle.AspNetCore.Swagger/ISwaggerProvider.cs @@ -11,8 +11,6 @@ OpenApiDocument GetSwagger( string documentName, string host = null, string basePath = null); - - string[] GetSwaggerDocumentNames(); } public class UnknownSwaggerDocument : InvalidOperationException diff --git a/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/PublicAPI.Unshipped.txt b/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/PublicAPI.Unshipped.txt index 8fbfbfb242..2c53459121 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/PublicAPI.Unshipped.txt @@ -1,2 +1,2 @@ -Swashbuckle.AspNetCore.Swagger.IAsyncSwaggerProvider.GetSwaggerDocumentNames() -> string[] -Swashbuckle.AspNetCore.Swagger.ISwaggerProvider.GetSwaggerDocumentNames() -> string[] \ No newline at end of file +Swashbuckle.AspNetCore.Swagger.ISwaggerDocumentInformation +Swashbuckle.AspNetCore.Swagger.ISwaggerDocumentInformation.GetDocumentNames() -> string[] diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/PublicAPI/PublicAPI.Unshipped.txt b/src/Swashbuckle.AspNetCore.SwaggerGen/PublicAPI/PublicAPI.Unshipped.txt index 4b28e8ee4d..4f39c51732 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/PublicAPI/PublicAPI.Unshipped.txt @@ -1,5 +1,5 @@ static Swashbuckle.AspNetCore.SwaggerGen.OpenApiAnyFactory.CreateFromJson(string json, System.Text.Json.JsonSerializerOptions options) -> Microsoft.OpenApi.Any.IOpenApiAny static Swashbuckle.AspNetCore.SwaggerGen.XmlCommentsTextHelper.Humanize(string text, string xmlCommentEndOfLine) -> string -Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GetSwaggerDocumentNames() -> string[] +Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GetDocumentNames() -> string[] Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorOptions.XmlCommentEndOfLine.get -> string Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorOptions.XmlCommentEndOfLine.set -> void diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs index a76308bea6..d05e382865 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs @@ -20,7 +20,7 @@ namespace Swashbuckle.AspNetCore.SwaggerGen { - public class SwaggerGenerator : ISwaggerProvider, IAsyncSwaggerProvider + public class SwaggerGenerator : ISwaggerProvider, IAsyncSwaggerProvider, ISwaggerDocumentInformation { private readonly IApiDescriptionGroupCollectionProvider _apiDescriptionsProvider; private readonly ISchemaGenerator _schemaGenerator; @@ -112,7 +112,7 @@ public OpenApiDocument GetSwagger(string documentName, string host = null, strin } } - public string[] GetSwaggerDocumentNames() + public string[] GetDocumentNames() { return _options.SwaggerDocs.Keys.ToArray(); } diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs index e3e196cdf0..5ac7777018 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs @@ -50,12 +50,8 @@ public void GetSwagger_GeneratesSwaggerDocument_ForApiDescriptionsWithMatchingGr } ); - var provider = subject as ISwaggerProvider; - var documentNames = provider.GetSwaggerDocumentNames(); - Assert.Equal(new[] { "v1", "v2" }, documentNames); - - var asyncProvider = subject as IAsyncSwaggerProvider; - documentNames = provider.GetSwaggerDocumentNames(); + var provider = subject as ISwaggerDocumentInformation; + var documentNames = provider.GetDocumentNames(); Assert.Equal(new[] { "v1", "v2" }, documentNames); var document = subject.GetSwagger("v1"); From 7ac0fa9571411261d7446a3692a1cb0eb9d10827 Mon Sep 17 00:00:00 2001 From: tuttb Date: Wed, 19 Feb 2025 14:25:29 -0500 Subject: [PATCH 3/7] Changes based on review comments: * Renamed new interface to ISwaggerDocumentMetadataProvider.cs * SwaggerGeneerator: return IList from new method, and simplify to expression. * SwaggerUIJsonSerliazerContext.cs: Put List after InterceptorFunctions. * SwaggerUIOptions(Extensions): Tried to clarify documentation and update naming based on review. * MultipleVersions\Startup.cs: Applied extension method rename. --- src/Swashbuckle.AspNetCore.Cli/Program.cs | 130 +++++++++++++----- .../ISwaggerDocumentInformation.cs | 14 -- .../ISwaggerDocumentMetadataProvider.cs | 9 ++ .../PublicAPI/PublicAPI.Unshipped.txt | 4 +- .../PublicAPI/PublicAPI.Unshipped.txt | 2 +- .../SwaggerGenerator/SwaggerGenerator.cs | 7 +- .../PublicAPI/PublicAPI.Unshipped.txt | 2 +- .../SwaggerUIJsonSerializerContext.cs | 2 +- .../SwaggerUIOptions.cs | 6 +- .../SwaggerUIOptionsExtensions.cs | 7 +- .../SwaggerGenerator/SwaggerGeneratorTests.cs | 4 +- test/WebSites/MultipleVersions/Startup.cs | 2 +- 12 files changed, 126 insertions(+), 63 deletions(-) delete mode 100644 src/Swashbuckle.AspNetCore.Swagger/ISwaggerDocumentInformation.cs create mode 100644 src/Swashbuckle.AspNetCore.Swagger/ISwaggerDocumentMetadataProvider.cs diff --git a/src/Swashbuckle.AspNetCore.Cli/Program.cs b/src/Swashbuckle.AspNetCore.Cli/Program.cs index 62dfadffb4..2dee95cc75 100644 --- a/src/Swashbuckle.AspNetCore.Cli/Program.cs +++ b/src/Swashbuckle.AspNetCore.Cli/Program.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using System.Runtime.Loader; +using System.Text; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; @@ -41,29 +42,7 @@ public static int Main(string[] args) c.OnRun((namedArgs) => { - if (!File.Exists(namedArgs["startupassembly"])) - { - throw new FileNotFoundException(namedArgs["startupassembly"]); - } - - var depsFile = namedArgs["startupassembly"].Replace(".dll", ".deps.json"); - var runtimeConfig = namedArgs["startupassembly"].Replace(".dll", ".runtimeconfig.json"); - var commandName = args[0]; - - var subProcessArguments = new string[args.Length - 1]; - if (subProcessArguments.Length > 0) - { - Array.Copy(args, 1, subProcessArguments, 0, subProcessArguments.Length); - } - - var subProcessCommandLine = string.Format( - "exec --depsfile {0} --runtimeconfig {1} {2} _{3} {4}", // note the underscore prepended to the command name - EscapePath(depsFile), - EscapePath(runtimeConfig), - EscapePath(typeof(Program).GetTypeInfo().Assembly.Location), - commandName, - string.Join(" ", subProcessArguments.Select(x => EscapePath(x))) - ); + string subProcessCommandLine = PrepareCommandLine(args, namedArgs); var subProcess = Process.Start("dotnet", subProcessCommandLine); @@ -84,16 +63,7 @@ public static int Main(string[] args) c.Option("--yaml", "", true); c.OnRun((namedArgs) => { - // 1) Configure host with provided startupassembly - var startupAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath( - Path.Combine(Directory.GetCurrentDirectory(), namedArgs["startupassembly"])); - - // 2) Build a service container that's based on the startup assembly - var serviceProvider = GetServiceProvider(startupAssembly); - - // 3) Retrieve Swagger via configured provider - var swaggerProvider = serviceProvider.GetRequiredService(); - var swaggerOptions = serviceProvider.GetService>(); + SetupAndRetrieveSwaggerProviderAndOptions(namedArgs, out var swaggerProvider, out var swaggerOptions); var swaggerDocumentSerializer = swaggerOptions?.Value?.CustomDocumentSerializer; var swagger = swaggerProvider.GetSwagger( namedArgs["swaggerdoc"], @@ -156,9 +126,103 @@ public static int Main(string[] args) }); }); + // > dotnet swagger list + runner.SubCommand("list", "retrieves the list of Swagger document names from a startup assembly", c => + { + c.Argument("startupassembly", "relative path to the application's startup assembly"); + c.Option("--output", "relative path where the document names will be output, defaults to stdout"); + c.OnRun((namedArgs) => + { + string subProcessCommandLine = PrepareCommandLine(args, namedArgs); + + var subProcess = Process.Start("dotnet", subProcessCommandLine); + + subProcess.WaitForExit(); + return subProcess.ExitCode; + }); + }); + + // > dotnet swagger _list ... (* should only be invoked via "dotnet exec") + runner.SubCommand("_list", "", c => + { + c.Argument("startupassembly", ""); + c.Option("--output", ""); + c.OnRun((namedArgs) => + { + SetupAndRetrieveSwaggerProviderAndOptions(namedArgs, out var swaggerProvider, out var swaggerOptions); + var docMetaProvider = swaggerProvider as ISwaggerDocumentMetadataProvider; + var docNames = docMetaProvider.GetDocumentNames(); + var outputPath = namedArgs.TryGetValue("--output", out var arg1) + ? Path.Combine(Directory.GetCurrentDirectory(), arg1) + : null; + bool outputViaConsole = outputPath == null; + if (!string.IsNullOrEmpty(outputPath)) + { + string directoryPath = Path.GetDirectoryName(outputPath); + if (!string.IsNullOrEmpty(directoryPath) && !Directory.Exists(directoryPath)) + { + Directory.CreateDirectory(directoryPath); + } + } + + + using Stream stream = outputViaConsole ? Console.OpenStandardOutput() : File.Create(outputPath); + using StreamWriter writer = new StreamWriter(stream, outputViaConsole ? Console.OutputEncoding : Encoding.UTF8); + + writer.WriteLine($"Swagger Document Names (Surrounded by single quotes):"); + foreach (var name in docNames) + { + writer.WriteLine($"'{name}'"); + } + return 0; + }); + }); + return runner.Run(args); } + private static void SetupAndRetrieveSwaggerProviderAndOptions(System.Collections.Generic.IDictionary namedArgs, out ISwaggerProvider swaggerProvider, out IOptions swaggerOptions) + { + // 1) Configure host with provided startupassembly + var startupAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath( + Path.Combine(Directory.GetCurrentDirectory(), namedArgs["startupassembly"])); + + // 2) Build a service container that's based on the startup assembly + var serviceProvider = GetServiceProvider(startupAssembly); + + // 3) Retrieve Swagger via configured provider + swaggerProvider = serviceProvider.GetRequiredService(); + swaggerOptions = serviceProvider.GetService>(); + } + + private static string PrepareCommandLine(string[] args, System.Collections.Generic.IDictionary namedArgs) + { + if (!File.Exists(namedArgs["startupassembly"])) + { + throw new FileNotFoundException(namedArgs["startupassembly"]); + } + + var depsFile = namedArgs["startupassembly"].Replace(".dll", ".deps.json"); + var runtimeConfig = namedArgs["startupassembly"].Replace(".dll", ".runtimeconfig.json"); + var commandName = args[0]; + + var subProcessArguments = new string[args.Length - 1]; + if (subProcessArguments.Length > 0) + { + Array.Copy(args, 1, subProcessArguments, 0, subProcessArguments.Length); + } + + var subProcessCommandLine = string.Format( + "exec --depsfile {0} --runtimeconfig {1} {2} _{3} {4}", // note the underscore prepended to the command name + EscapePath(depsFile), + EscapePath(runtimeConfig), + EscapePath(typeof(Program).GetTypeInfo().Assembly.Location), + commandName, + string.Join(" ", subProcessArguments.Select(x => EscapePath(x))) + ); + return subProcessCommandLine; + } + private static string EscapePath(string path) { return path.Contains(' ') diff --git a/src/Swashbuckle.AspNetCore.Swagger/ISwaggerDocumentInformation.cs b/src/Swashbuckle.AspNetCore.Swagger/ISwaggerDocumentInformation.cs deleted file mode 100644 index 5198f5e3ec..0000000000 --- a/src/Swashbuckle.AspNetCore.Swagger/ISwaggerDocumentInformation.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.OpenApi.Models; - -namespace Swashbuckle.AspNetCore.Swagger -{ - public interface ISwaggerDocumentInformation - { - string[] GetDocumentNames(); - } -} diff --git a/src/Swashbuckle.AspNetCore.Swagger/ISwaggerDocumentMetadataProvider.cs b/src/Swashbuckle.AspNetCore.Swagger/ISwaggerDocumentMetadataProvider.cs new file mode 100644 index 0000000000..60692cac42 --- /dev/null +++ b/src/Swashbuckle.AspNetCore.Swagger/ISwaggerDocumentMetadataProvider.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Swashbuckle.AspNetCore.Swagger +{ + public interface ISwaggerDocumentMetadataProvider + { + IList GetDocumentNames(); + } +} diff --git a/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/PublicAPI.Unshipped.txt b/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/PublicAPI.Unshipped.txt index 2c53459121..97812dd87b 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/PublicAPI.Unshipped.txt @@ -1,2 +1,2 @@ -Swashbuckle.AspNetCore.Swagger.ISwaggerDocumentInformation -Swashbuckle.AspNetCore.Swagger.ISwaggerDocumentInformation.GetDocumentNames() -> string[] +Swashbuckle.AspNetCore.Swagger.ISwaggerDocumentMetadataProvider +Swashbuckle.AspNetCore.Swagger.ISwaggerDocumentMetadataProvider.GetDocumentNames() -> System.Collections.Generic.IList diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/PublicAPI/PublicAPI.Unshipped.txt b/src/Swashbuckle.AspNetCore.SwaggerGen/PublicAPI/PublicAPI.Unshipped.txt index 4f39c51732..9103c715aa 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/PublicAPI/PublicAPI.Unshipped.txt @@ -1,5 +1,5 @@ static Swashbuckle.AspNetCore.SwaggerGen.OpenApiAnyFactory.CreateFromJson(string json, System.Text.Json.JsonSerializerOptions options) -> Microsoft.OpenApi.Any.IOpenApiAny static Swashbuckle.AspNetCore.SwaggerGen.XmlCommentsTextHelper.Humanize(string text, string xmlCommentEndOfLine) -> string -Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GetDocumentNames() -> string[] +Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GetDocumentNames() -> System.Collections.Generic.IList Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorOptions.XmlCommentEndOfLine.get -> string Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorOptions.XmlCommentEndOfLine.set -> void diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs index d05e382865..84534e6de7 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs @@ -20,7 +20,7 @@ namespace Swashbuckle.AspNetCore.SwaggerGen { - public class SwaggerGenerator : ISwaggerProvider, IAsyncSwaggerProvider, ISwaggerDocumentInformation + public class SwaggerGenerator : ISwaggerProvider, IAsyncSwaggerProvider, ISwaggerDocumentMetadataProvider { private readonly IApiDescriptionGroupCollectionProvider _apiDescriptionsProvider; private readonly ISchemaGenerator _schemaGenerator; @@ -112,10 +112,7 @@ public OpenApiDocument GetSwagger(string documentName, string host = null, strin } } - public string[] GetDocumentNames() - { - return _options.SwaggerDocs.Keys.ToArray(); - } + public IList GetDocumentNames() => _options.SwaggerDocs.Keys.ToList(); private void SortSchemas(OpenApiDocument document) { diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/PublicAPI/PublicAPI.Unshipped.txt b/src/Swashbuckle.AspNetCore.SwaggerUI/PublicAPI/PublicAPI.Unshipped.txt index 39bf1c2b5a..974e583fc9 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/PublicAPI/PublicAPI.Unshipped.txt @@ -1,4 +1,4 @@ -static Microsoft.AspNetCore.Builder.SwaggerUIOptionsExtensions.EnableSwaggerDocumentUrlsRoute(this Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIOptions options) -> void +static Microsoft.AspNetCore.Builder.SwaggerUIOptionsExtensions.EnableSwaggerDocumentUrlsEndpoint(this Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIOptions options) -> void Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIOptions.ExposeSwaggerDocumentUrlsRoute.get -> bool Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIOptions.ExposeSwaggerDocumentUrlsRoute.set -> void Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIOptions.SwaggerDocumentUrlsPath.get -> string diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIJsonSerializerContext.cs b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIJsonSerializerContext.cs index 413761e753..51e9fbe31d 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIJsonSerializerContext.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIJsonSerializerContext.cs @@ -8,8 +8,8 @@ namespace Swashbuckle.AspNetCore.SwaggerUI; [JsonSerializable(typeof(ConfigObject))] -[JsonSerializable(typeof(List))] [JsonSerializable(typeof(InterceptorFunctions))] +[JsonSerializable(typeof(List))] [JsonSerializable(typeof(OAuthConfigObject))] // These primitive types are declared for common types that may be used with ConfigObject.AdditionalItems. See https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2884. [JsonSerializable(typeof(bool))] diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptions.cs b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptions.cs index 7672553a3b..178a31c507 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptions.cs @@ -65,12 +65,14 @@ public class SwaggerUIOptions public string StylesPath { get; set; } = "./swagger-ui.css"; /// - /// Expose the ConfigObject.Urls object via a new <root>/documentUrls + /// Expose the ConfigObject.Urls object via a new + /// "<>/<> + /// route so that external code can auto-discover all swagger documents. /// public bool ExposeSwaggerDocumentUrlsRoute { get; set; } = false; /// - /// Gets or sets the relative URL path to the documentUrls route. + /// Gets or sets the relative URL path to the route that exposes the SwaggerUIOptions.ConfigObjet.Urls>. /// public string SwaggerDocumentUrlsPath { get; set; } = "documentUrls"; } diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptionsExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptionsExtensions.cs index fe73533181..26a932b931 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptionsExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptionsExtensions.cs @@ -334,7 +334,12 @@ public static void UseResponseInterceptor(this SwaggerUIOptions options, string options.Interceptors.ResponseInterceptorFunction = value; } - public static void EnableSwaggerDocumentUrlsRoute(this SwaggerUIOptions options) + /// + /// Function to enable the option to expose the available + /// Swagger document urls to external parties. + /// + /// + public static void EnableSwaggerDocumentUrlsEndpoint(this SwaggerUIOptions options) { options.ExposeSwaggerDocumentUrlsRoute = true; } diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs index 5ac7777018..66daa70a9a 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs @@ -50,9 +50,9 @@ public void GetSwagger_GeneratesSwaggerDocument_ForApiDescriptionsWithMatchingGr } ); - var provider = subject as ISwaggerDocumentInformation; + var provider = Assert.IsAssignableFrom(subject); var documentNames = provider.GetDocumentNames(); - Assert.Equal(new[] { "v1", "v2" }, documentNames); + Assert.Equal(["v1", "v2"], documentNames); var document = subject.GetSwagger("v1"); diff --git a/test/WebSites/MultipleVersions/Startup.cs b/test/WebSites/MultipleVersions/Startup.cs index 739a75147f..1f2552119c 100644 --- a/test/WebSites/MultipleVersions/Startup.cs +++ b/test/WebSites/MultipleVersions/Startup.cs @@ -59,7 +59,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVers { c.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", $"Version {description.GroupName}"); } - c.EnableSwaggerDocumentUrlsRoute(); + c.EnableSwaggerDocumentUrlsEndpoint(); }); // Separate endpoints that contain only one version From bfd9e374f55824ab9e86361319e838ac0ebc0aba Mon Sep 17 00:00:00 2001 From: tuttb Date: Mon, 24 Feb 2025 17:51:59 -0500 Subject: [PATCH 4/7] More review feedback --- src/Swashbuckle.AspNetCore.Cli/Program.cs | 29 +++++++++--- .../SwaggerUIOptions.cs | 8 ++-- .../Swashbuckle.AspNetCore.Cli.Test.csproj | 1 + .../ToolTests.cs | 45 +++++++++++++++---- 4 files changed, 64 insertions(+), 19 deletions(-) diff --git a/src/Swashbuckle.AspNetCore.Cli/Program.cs b/src/Swashbuckle.AspNetCore.Cli/Program.cs index 2dee95cc75..0824dadf1f 100644 --- a/src/Swashbuckle.AspNetCore.Cli/Program.cs +++ b/src/Swashbuckle.AspNetCore.Cli/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; @@ -151,8 +152,14 @@ public static int Main(string[] args) { SetupAndRetrieveSwaggerProviderAndOptions(namedArgs, out var swaggerProvider, out var swaggerOptions); var docMetaProvider = swaggerProvider as ISwaggerDocumentMetadataProvider; - var docNames = docMetaProvider.GetDocumentNames(); - var outputPath = namedArgs.TryGetValue("--output", out var arg1) + IList docNames = new List(); + string outputPath = null; + if (docMetaProvider != null) + { + docNames = docMetaProvider.GetDocumentNames(); + } + + outputPath = namedArgs.TryGetValue("--output", out var arg1) ? Path.Combine(Directory.GetCurrentDirectory(), arg1) : null; bool outputViaConsole = outputPath == null; @@ -165,14 +172,22 @@ public static int Main(string[] args) } } - using Stream stream = outputViaConsole ? Console.OpenStandardOutput() : File.Create(outputPath); - using StreamWriter writer = new StreamWriter(stream, outputViaConsole ? Console.OutputEncoding : Encoding.UTF8); + using StreamWriter writer = new(stream, outputViaConsole ? Console.OutputEncoding : Encoding.UTF8); - writer.WriteLine($"Swagger Document Names (Surrounded by single quotes):"); - foreach (var name in docNames) + if (docMetaProvider == null) { - writer.WriteLine($"'{name}'"); + writer.WriteLine($"ERROR: ISwaggerProvider instance DOES NOT support ISwaggerDocumentMetadaatProvider, so this coommand " + + "DOES NOT support listing out the document names. Try updating the version of Swashbuckle used by the project you are examining to a newer version."); + return -1; + } + else + { + writer.WriteLine($"Swagger Document Names:"); + foreach (var name in docNames) + { + writer.WriteLine($"\"{name}\""); + } } return 0; }); diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptions.cs b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptions.cs index 178a31c507..28d337da37 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptions.cs @@ -65,14 +65,14 @@ public class SwaggerUIOptions public string StylesPath { get; set; } = "./swagger-ui.css"; /// - /// Expose the ConfigObject.Urls object via a new - /// "<>/<> - /// route so that external code can auto-discover all swagger documents. + /// Gets or sets whether to expose the ConfigObject.Urls object via an + /// HTTP endpoint with the URL specified by + /// so that external code can auto-discover all Swagger documents. /// public bool ExposeSwaggerDocumentUrlsRoute { get; set; } = false; /// - /// Gets or sets the relative URL path to the route that exposes the SwaggerUIOptions.ConfigObjet.Urls>. + /// Gets or sets the relative URL path to the route that exposes the values of the configured values. /// public string SwaggerDocumentUrlsPath { get; set; } = "documentUrls"; } diff --git a/test/Swashbuckle.AspNetCore.Cli.Test/Swashbuckle.AspNetCore.Cli.Test.csproj b/test/Swashbuckle.AspNetCore.Cli.Test/Swashbuckle.AspNetCore.Cli.Test.csproj index d092c5648d..bd789f4a78 100644 --- a/test/Swashbuckle.AspNetCore.Cli.Test/Swashbuckle.AspNetCore.Cli.Test.csproj +++ b/test/Swashbuckle.AspNetCore.Cli.Test/Swashbuckle.AspNetCore.Cli.Test.csproj @@ -11,6 +11,7 @@ + diff --git a/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs b/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs index 0373863b69..282e07c767 100644 --- a/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs +++ b/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs @@ -9,6 +9,20 @@ namespace Swashbuckle.AspNetCore.Cli.Test { public static class ToolTests { + [Fact] + public static void Can_Output_Swagger_Document_Names() + { + var result = RunListCommand((outputPath) => + [ + "list", + "--output", + outputPath, + Path.Combine(Directory.GetCurrentDirectory(), "MultipleVersions.dll") + ], nameof(Can_Output_Swagger_Document_Names)); + var expected = $"Swagger Document Names:{Environment.NewLine}\"1.0\"{Environment.NewLine}\"2.0\"{Environment.NewLine}"; + Assert.Equal(expected, result); + } + [Fact] public static void Throws_When_Startup_Assembly_Does_Not_Exist() { @@ -19,7 +33,7 @@ public static void Throws_When_Startup_Assembly_Does_Not_Exist() [Fact] public static void Can_Generate_Swagger_Json() { - using var document = RunApplication((outputPath) => + using var document = RunToFileCommand((outputPath) => [ "tofile", "--output", @@ -38,7 +52,7 @@ public static void Can_Generate_Swagger_Json() [Fact] public static void Overwrites_Existing_File() { - using var document = RunApplication((outputPath) => + using var document = RunToFileCommand((outputPath) => { File.WriteAllText(outputPath, new string('x', 100_000)); @@ -62,7 +76,7 @@ public static void Overwrites_Existing_File() [Fact] public static void CustomDocumentSerializer_Writes_Custom_V2_Document() { - using var document = RunApplication((outputPath) => + using var document = RunToFileCommand((outputPath) => [ "tofile", "--output", @@ -80,7 +94,7 @@ public static void CustomDocumentSerializer_Writes_Custom_V2_Document() [Fact] public static void CustomDocumentSerializer_Writes_Custom_V3_Document() { - using var document = RunApplication((outputPath) => + using var document = RunToFileCommand((outputPath) => [ "tofile", "--output", @@ -98,7 +112,7 @@ public static void CustomDocumentSerializer_Writes_Custom_V3_Document() [Fact] public static void Can_Generate_Swagger_Json_ForTopLevelApp() { - using var document = RunApplication((outputPath) => + using var document = RunToFileCommand((outputPath) => [ "tofile", "--output", @@ -117,7 +131,7 @@ public static void Can_Generate_Swagger_Json_ForTopLevelApp() [Fact] public static void Does_Not_Run_Crashing_HostedService() { - using var document = RunApplication((outputPath) => + using var document = RunToFileCommand((outputPath) => [ "tofile", "--output", @@ -135,7 +149,7 @@ public static void Does_Not_Run_Crashing_HostedService() [Fact] public static void Creates_New_Folder_Path() { - using var document = RunApplication(outputPath => + using var document = RunToFileCommand(outputPath => [ "tofile", "--output", @@ -151,7 +165,22 @@ public static void Creates_New_Folder_Path() Assert.True(productsPath.TryGetProperty("post", out _)); } - private static JsonDocument RunApplication(Func setup, string subOutputPath = default) + private static string RunListCommand(Func setup, string subOutputPath = default) + { + using var temporaryDirectory = new TemporaryDirectory(); + + var outputPath = !string.IsNullOrEmpty(subOutputPath) + ? Path.Combine(temporaryDirectory.Path, subOutputPath, "swagger.json") + : Path.Combine(temporaryDirectory.Path, "swagger.json"); + + string[] args = setup(outputPath); + + Assert.Equal(0, Program.Main(args)); + + return File.ReadAllText(outputPath); + } + + private static JsonDocument RunToFileCommand(Func setup, string subOutputPath = default) { using var temporaryDirectory = new TemporaryDirectory(); From e727cc7de0eea7993b3fbc6be4078f8ca39d36ce Mon Sep 17 00:00:00 2001 From: tuttb Date: Tue, 25 Feb 2025 14:42:44 -0500 Subject: [PATCH 5/7] hopefully last review tweaks --- src/Swashbuckle.AspNetCore.Cli/Program.cs | 21 +++++++------------ .../ToolTests.cs | 14 ++----------- 2 files changed, 9 insertions(+), 26 deletions(-) diff --git a/src/Swashbuckle.AspNetCore.Cli/Program.cs b/src/Swashbuckle.AspNetCore.Cli/Program.cs index 0824dadf1f..ddb838f415 100644 --- a/src/Swashbuckle.AspNetCore.Cli/Program.cs +++ b/src/Swashbuckle.AspNetCore.Cli/Program.cs @@ -151,13 +151,8 @@ public static int Main(string[] args) c.OnRun((namedArgs) => { SetupAndRetrieveSwaggerProviderAndOptions(namedArgs, out var swaggerProvider, out var swaggerOptions); - var docMetaProvider = swaggerProvider as ISwaggerDocumentMetadataProvider; IList docNames = new List(); string outputPath = null; - if (docMetaProvider != null) - { - docNames = docMetaProvider.GetDocumentNames(); - } outputPath = namedArgs.TryGetValue("--output", out var arg1) ? Path.Combine(Directory.GetCurrentDirectory(), arg1) @@ -175,19 +170,17 @@ public static int Main(string[] args) using Stream stream = outputViaConsole ? Console.OpenStandardOutput() : File.Create(outputPath); using StreamWriter writer = new(stream, outputViaConsole ? Console.OutputEncoding : Encoding.UTF8); - if (docMetaProvider == null) + if (swaggerProvider is not ISwaggerDocumentMetadataProvider docMetaProvider) { - writer.WriteLine($"ERROR: ISwaggerProvider instance DOES NOT support ISwaggerDocumentMetadaatProvider, so this coommand " - + "DOES NOT support listing out the document names. Try updating the version of Swashbuckle used by the project you are examining to a newer version."); + writer.WriteLine($"ERROR: The {nameof(ISwaggerProvider)} instance does not support {nameof(ISwaggerDocumentMetadataProvider)}, " + + "so this coommand cannot list out the document names."); return -1; } - else + docNames = docMetaProvider.GetDocumentNames(); + + foreach (var name in docNames) { - writer.WriteLine($"Swagger Document Names:"); - foreach (var name in docNames) - { - writer.WriteLine($"\"{name}\""); - } + writer.WriteLine($"\"{name}\""); } return 0; }); diff --git a/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs b/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs index 282e07c767..a426df68c0 100644 --- a/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs +++ b/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs @@ -19,7 +19,7 @@ public static void Can_Output_Swagger_Document_Names() outputPath, Path.Combine(Directory.GetCurrentDirectory(), "MultipleVersions.dll") ], nameof(Can_Output_Swagger_Document_Names)); - var expected = $"Swagger Document Names:{Environment.NewLine}\"1.0\"{Environment.NewLine}\"2.0\"{Environment.NewLine}"; + var expected = $"\"1.0\"{Environment.NewLine}\"2.0\"{Environment.NewLine}"; Assert.Equal(expected, result); } @@ -182,17 +182,7 @@ private static string RunListCommand(Func setup, string subOut private static JsonDocument RunToFileCommand(Func setup, string subOutputPath = default) { - using var temporaryDirectory = new TemporaryDirectory(); - - var outputPath = !string.IsNullOrEmpty(subOutputPath) - ? Path.Combine(temporaryDirectory.Path, subOutputPath, "swagger.json") - : Path.Combine(temporaryDirectory.Path, "swagger.json"); - - string[] args = setup(outputPath); - - Assert.Equal(0, Program.Main(args)); - - string json = File.ReadAllText(outputPath); + string json = RunListCommand(setup, subOutputPath); return JsonDocument.Parse(json); } From d62bcc4bdf072dfde141c4176cfa5d591d4adaed Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Wed, 26 Feb 2025 10:51:56 +0000 Subject: [PATCH 6/7] Apply suggestions from code review --- src/Swashbuckle.AspNetCore.Cli/Program.cs | 8 ++++---- .../ToolTests.cs | 20 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Swashbuckle.AspNetCore.Cli/Program.cs b/src/Swashbuckle.AspNetCore.Cli/Program.cs index ddb838f415..5b22a4ff87 100644 --- a/src/Swashbuckle.AspNetCore.Cli/Program.cs +++ b/src/Swashbuckle.AspNetCore.Cli/Program.cs @@ -152,9 +152,8 @@ public static int Main(string[] args) { SetupAndRetrieveSwaggerProviderAndOptions(namedArgs, out var swaggerProvider, out var swaggerOptions); IList docNames = new List(); - string outputPath = null; - outputPath = namedArgs.TryGetValue("--output", out var arg1) + string outputPath = namedArgs.TryGetValue("--output", out var arg1) ? Path.Combine(Directory.GetCurrentDirectory(), arg1) : null; bool outputViaConsole = outputPath == null; @@ -172,16 +171,17 @@ public static int Main(string[] args) if (swaggerProvider is not ISwaggerDocumentMetadataProvider docMetaProvider) { - writer.WriteLine($"ERROR: The {nameof(ISwaggerProvider)} instance does not support {nameof(ISwaggerDocumentMetadataProvider)}, " - + "so this coommand cannot list out the document names."); + writer.WriteLine($"The registered {nameof(ISwaggerProvider)} instance does not implement {nameof(ISwaggerDocumentMetadataProvider)}; unable to list the Swagger document names."); return -1; } + docNames = docMetaProvider.GetDocumentNames(); foreach (var name in docNames) { writer.WriteLine($"\"{name}\""); } + return 0; }); }); diff --git a/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs b/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs index a426df68c0..6bd28bd8ad 100644 --- a/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs +++ b/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs @@ -12,7 +12,7 @@ public static class ToolTests [Fact] public static void Can_Output_Swagger_Document_Names() { - var result = RunListCommand((outputPath) => + var result = RunToStringCommand((outputPath) => [ "list", "--output", @@ -33,7 +33,7 @@ public static void Throws_When_Startup_Assembly_Does_Not_Exist() [Fact] public static void Can_Generate_Swagger_Json() { - using var document = RunToFileCommand((outputPath) => + using var document = RunToJsonCommand((outputPath) => [ "tofile", "--output", @@ -52,7 +52,7 @@ public static void Can_Generate_Swagger_Json() [Fact] public static void Overwrites_Existing_File() { - using var document = RunToFileCommand((outputPath) => + using var document = RunToJsonCommand((outputPath) => { File.WriteAllText(outputPath, new string('x', 100_000)); @@ -76,7 +76,7 @@ public static void Overwrites_Existing_File() [Fact] public static void CustomDocumentSerializer_Writes_Custom_V2_Document() { - using var document = RunToFileCommand((outputPath) => + using var document = RunToJsonCommand((outputPath) => [ "tofile", "--output", @@ -94,7 +94,7 @@ public static void CustomDocumentSerializer_Writes_Custom_V2_Document() [Fact] public static void CustomDocumentSerializer_Writes_Custom_V3_Document() { - using var document = RunToFileCommand((outputPath) => + using var document = RunToJsonCommand((outputPath) => [ "tofile", "--output", @@ -112,7 +112,7 @@ public static void CustomDocumentSerializer_Writes_Custom_V3_Document() [Fact] public static void Can_Generate_Swagger_Json_ForTopLevelApp() { - using var document = RunToFileCommand((outputPath) => + using var document = RunToJsonCommand((outputPath) => [ "tofile", "--output", @@ -131,7 +131,7 @@ public static void Can_Generate_Swagger_Json_ForTopLevelApp() [Fact] public static void Does_Not_Run_Crashing_HostedService() { - using var document = RunToFileCommand((outputPath) => + using var document = RunToJsonCommand((outputPath) => [ "tofile", "--output", @@ -149,7 +149,7 @@ public static void Does_Not_Run_Crashing_HostedService() [Fact] public static void Creates_New_Folder_Path() { - using var document = RunToFileCommand(outputPath => + using var document = RunToJsonCommand(outputPath => [ "tofile", "--output", @@ -165,7 +165,7 @@ public static void Creates_New_Folder_Path() Assert.True(productsPath.TryGetProperty("post", out _)); } - private static string RunListCommand(Func setup, string subOutputPath = default) + private static string RunToStringCommand(Func setup, string subOutputPath = default) { using var temporaryDirectory = new TemporaryDirectory(); @@ -180,7 +180,7 @@ private static string RunListCommand(Func setup, string subOut return File.ReadAllText(outputPath); } - private static JsonDocument RunToFileCommand(Func setup, string subOutputPath = default) + private static JsonDocument RunToJsonCommand(Func setup, string subOutputPath = default) { string json = RunListCommand(setup, subOutputPath); return JsonDocument.Parse(json); From efbc1351410dd7e608de850891531b75d984379c Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Wed, 26 Feb 2025 10:55:36 +0000 Subject: [PATCH 7/7] Fix compilation error Rename method missed from previous commit. --- test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs b/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs index 6bd28bd8ad..343c0c5900 100644 --- a/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs +++ b/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs @@ -182,7 +182,7 @@ private static string RunToStringCommand(Func setup, string su private static JsonDocument RunToJsonCommand(Func setup, string subOutputPath = default) { - string json = RunListCommand(setup, subOutputPath); + string json = RunToStringCommand(setup, subOutputPath); return JsonDocument.Parse(json); }