diff --git a/.gitignore b/.gitignore index 35ee1b2537..60749a6d7d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ project.lock.json artifacts/ *.nuget.props .DS_Store -Thumbs.db \ No newline at end of file +Thumbs.db +test/WebSites/CliExample/wwwroot/api-docs/v1/*.json \ No newline at end of file diff --git a/README.md b/README.md index fbf492d252..9123813b8d 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,7 @@ Swashbuckle consists of three packages - a Swagger generator, middleware to expo |__Swashbuckle.AspNetCore.Swagger__|Exposes _SwaggerDocument_ objects as a JSON API. It expects an implementation of _ISwaggerProvider_ to be registered which it queries to retrieve Swagger document(s) before returning as serialized JSON| |__Swashbuckle.AspNetCore.SwaggerGen__|Injects an implementation of _ISwaggerProvider_ that can be used by the above component. This particular implementation automatically generates _SwaggerDocument_(s) from your routes, controllers and models| |__Swashbuckle.AspNetCore.SwaggerUI__|Exposes an embedded version of the swagger-ui. You specify the API endpoints where it can obtain Swagger JSON and it uses them to power interactive docs for your API| +|__dotnet-swagger__ (Beta)|Provides a CLI interface for retrieving Swagger directly from a startup assembly, and writing to file| # Configuration & Customization # @@ -137,6 +138,9 @@ The steps described above will get you up and running with minimal setup. Howeve * [Inject Custom CSS](#inject-custom-css) * [Enable OAuth2.0 Flows](#enable-oauth20-flows) +* [dotnet-swagger (CLI tool)](#dotnet-swagger-cli-tool) + * [Retrieve Swagger Directly from a Startup Assembly](#retrieve-swagger-directly-from-a-startup-assembly) + ## Swashbuckle.AspNetCore.Swagger ## ### Change the Path for Swagger JSON Endpoints ### @@ -780,3 +784,38 @@ app.UseSwaggerUI(c => c.ConfigureOAuth2("swagger-ui", "swagger-ui-secret", "swagger-ui-realm", "Swagger UI"); } ``` + +## dotnet-swagger (CLI Tool) ## + +_NOTE:_ This feature is currently beta only. If you use it, please post feedback to the following [issue](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/541) + +### Retrieve Swagger Directly from a Startup Assembly ### + +The dotnet-swagger CLI tool can retrieve Swagger JSON directly from your application startup assembly, and write it to file. This can be useful if you want to incorporate Swagger generation into a CI/CD process, or if you want to serve it from static file at run-time. + +The tool can be installed as a [per-project, framework-dependent CLI extension](https://docs.microsoft.com/en-us/dotnet/core/tools/extensibility#per-project-based-extensibility) by adding the following reference to your .csproj file and running `dotnet restore`: + +```xml + + + +``` + +Once this is done, you can run the following command from your project root: + +``` +dotnet swagger tofile --help +``` + +Before you invoke the `tofile` command, you need to ensure your application is configured to expose Swagger JSON, as described in [Getting Started](#getting-started). Once this is done, you can point to your startup assembly and generate a local Swagger JSON file with the following command: + +``` +dotnet swagger tofile [startupassembly] [swaggerdoc] [output] +``` + +Where ... +* [startupassembly] is the relative path to your application's startup assembly +* [swaggerdoc] is the name of the swagger document you want to retrieve, as configured in your startup class +* [output] is the relative path where the Swagger JSON will be output to + +Checkout the [CliExample app](test/WebSites/CliExample) for more inspiration. It leverages the MSBuild Exec command to generate Swagger JSON at build-time. \ No newline at end of file diff --git a/Swashbuckle.AspNetCore.sln b/Swashbuckle.AspNetCore.sln index d201ce40a4..5747f2f2e2 100644 --- a/Swashbuckle.AspNetCore.sln +++ b/Swashbuckle.AspNetCore.sln @@ -6,6 +6,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{15A55F4A-FC3 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FA1B4021-0A97-4F68-B966-148191F6AAA8}" ProjectSection(SolutionItems) = preProject + .gitignore = .gitignore appveyor.yml = appveyor.yml CONTRIBUTING.md = CONTRIBUTING.md ISSUE_TEMPLATE.md = ISSUE_TEMPLATE.md @@ -57,6 +58,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Swashbuckle.AspNetCore.ReDo EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReDoc", "test\WebSites\ReDoc\ReDoc.csproj", "{311943AF-B796-42E7-B692-8D668B5FED77}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-swagger", "src\dotnet-swagger\dotnet-swagger.csproj", "{8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-swagger.Test", "test\dotnet-swagger.Test\dotnet-swagger.Test.csproj", "{6570BBFF-55B9-4F76-8805-A9C89F284103}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliExample", "test\WebSites\CliExample\CliExample.csproj", "{1BFC563B-7C56-4797-B94E-CCD4096C94A5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -259,6 +266,42 @@ Global {311943AF-B796-42E7-B692-8D668B5FED77}.Release|x64.Build.0 = Release|Any CPU {311943AF-B796-42E7-B692-8D668B5FED77}.Release|x86.ActiveCfg = Release|Any CPU {311943AF-B796-42E7-B692-8D668B5FED77}.Release|x86.Build.0 = Release|Any CPU + {8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Debug|x64.ActiveCfg = Debug|Any CPU + {8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Debug|x64.Build.0 = Debug|Any CPU + {8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Debug|x86.ActiveCfg = Debug|Any CPU + {8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Debug|x86.Build.0 = Debug|Any CPU + {8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Release|Any CPU.Build.0 = Release|Any CPU + {8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Release|x64.ActiveCfg = Release|Any CPU + {8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Release|x64.Build.0 = Release|Any CPU + {8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Release|x86.ActiveCfg = Release|Any CPU + {8CEB1812-D66F-4EF9-90DC-8102FABBB5A4}.Release|x86.Build.0 = Release|Any CPU + {6570BBFF-55B9-4F76-8805-A9C89F284103}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6570BBFF-55B9-4F76-8805-A9C89F284103}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6570BBFF-55B9-4F76-8805-A9C89F284103}.Debug|x64.ActiveCfg = Debug|Any CPU + {6570BBFF-55B9-4F76-8805-A9C89F284103}.Debug|x64.Build.0 = Debug|Any CPU + {6570BBFF-55B9-4F76-8805-A9C89F284103}.Debug|x86.ActiveCfg = Debug|Any CPU + {6570BBFF-55B9-4F76-8805-A9C89F284103}.Debug|x86.Build.0 = Debug|Any CPU + {6570BBFF-55B9-4F76-8805-A9C89F284103}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6570BBFF-55B9-4F76-8805-A9C89F284103}.Release|Any CPU.Build.0 = Release|Any CPU + {6570BBFF-55B9-4F76-8805-A9C89F284103}.Release|x64.ActiveCfg = Release|Any CPU + {6570BBFF-55B9-4F76-8805-A9C89F284103}.Release|x64.Build.0 = Release|Any CPU + {6570BBFF-55B9-4F76-8805-A9C89F284103}.Release|x86.ActiveCfg = Release|Any CPU + {6570BBFF-55B9-4F76-8805-A9C89F284103}.Release|x86.Build.0 = Release|Any CPU + {1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Debug|x64.ActiveCfg = Debug|Any CPU + {1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Debug|x64.Build.0 = Debug|Any CPU + {1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Debug|x86.ActiveCfg = Debug|Any CPU + {1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Debug|x86.Build.0 = Debug|Any CPU + {1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Release|Any CPU.Build.0 = Release|Any CPU + {1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Release|x64.ActiveCfg = Release|Any CPU + {1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Release|x64.Build.0 = Release|Any CPU + {1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Release|x86.ActiveCfg = Release|Any CPU + {1BFC563B-7C56-4797-B94E-CCD4096C94A5}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -281,6 +324,9 @@ Global {A2345EE6-EDF0-4E79-8A83-C8EDF0AADB39} = {245144DE-BC89-4822-B044-020458BFECC0} {B5E94C7D-B76E-4181-87E5-ACD8971A987E} = {15A55F4A-FC33-4D96-BAAD-FBDCDD96D5F5} {311943AF-B796-42E7-B692-8D668B5FED77} = {245144DE-BC89-4822-B044-020458BFECC0} + {8CEB1812-D66F-4EF9-90DC-8102FABBB5A4} = {15A55F4A-FC33-4D96-BAAD-FBDCDD96D5F5} + {6570BBFF-55B9-4F76-8805-A9C89F284103} = {1669F896-133C-4996-B58C-E7CDA299ADFF} + {1BFC563B-7C56-4797-B94E-CCD4096C94A5} = {245144DE-BC89-4822-B044-020458BFECC0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {36FC6A67-247D-4149-8EDD-79FFD1A75F51} diff --git a/src/Swashbuckle.AspNetCore.Swagger/Application/SwaggerSerializerFactory.cs b/src/Swashbuckle.AspNetCore.Swagger/Application/SwaggerSerializerFactory.cs index ffd41b8078..6d9fa2b459 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/Application/SwaggerSerializerFactory.cs +++ b/src/Swashbuckle.AspNetCore.Swagger/Application/SwaggerSerializerFactory.cs @@ -6,7 +6,7 @@ namespace Swashbuckle.AspNetCore.Swagger { public class SwaggerSerializerFactory { - internal static JsonSerializer Create(IOptions applicationJsonOptions) + public static JsonSerializer Create(IOptions applicationJsonOptions) { // TODO: Should this handle case where mvcJsonOptions.Value == null? return new JsonSerializer diff --git a/src/dotnet-swagger/CommandRunner.cs b/src/dotnet-swagger/CommandRunner.cs new file mode 100644 index 0000000000..2710bf08fc --- /dev/null +++ b/src/dotnet-swagger/CommandRunner.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Swashbuckle.AspNetCore.Cli +{ + public class CommandRunner + { + private readonly Dictionary _argumentDescriptors; + private readonly Dictionary _optionDescriptors; + private Func, int> _runFunc; + private readonly List _subRunners; + private readonly TextWriter _output; + + public CommandRunner(string commandName, string commandDescription, TextWriter output) + { + CommandName = commandName; + CommandDescription = commandDescription; + _argumentDescriptors = new Dictionary(); + _optionDescriptors = new Dictionary(); + _runFunc = (namedArgs) => { return 1; }; // noop + _subRunners = new List(); + _output = output; + } + + public string CommandName { get; private set; } + + public string CommandDescription { get; private set; } + + public void Argument(string name, string description) + { + _argumentDescriptors.Add(name, description); + } + + public void Option(string name, string description) + { + if (!name.StartsWith("--")) throw new ArgumentException("name of option must begin with --"); + _optionDescriptors.Add(name, description); + } + + public void OnRun(Func, int> runFunc) + { + _runFunc = runFunc; + } + + public void SubCommand(string name, string description, Action configAction) + { + var runner = new CommandRunner($"{CommandName} {name}", description, _output); + configAction(runner); + _subRunners.Add(runner); + } + + public int Run(IEnumerable args) + { + if (args.Any()) + { + var subRunner = _subRunners.FirstOrDefault(r => r.CommandName.Split(' ').Last() == args.First()); + if (subRunner != null) return subRunner.Run(args.Skip(1)); + } + + if (_subRunners.Any() || !TryParseArgs(args, out IDictionary namedArgs)) + { + PrintUsage(); + return 1; + } + + return _runFunc(namedArgs); + } + + private bool TryParseArgs(IEnumerable args, out IDictionary namedArgs) + { + namedArgs = new Dictionary(); + var argsQueue = new Queue(args); + + // Process options first + while (argsQueue.Any() && argsQueue.Peek().StartsWith("--")) + { + // Ensure it's expected and that the value is also provided + var name = argsQueue.Dequeue(); + if (!_optionDescriptors.ContainsKey(name) || !argsQueue.Any() || argsQueue.Peek().StartsWith("--")) + return false; + namedArgs.Add(name, argsQueue.Dequeue()); + } + + // Process required args - ensure corresponding values are provided + foreach (var name in _argumentDescriptors.Keys) + { + if (!argsQueue.Any() || argsQueue.Peek().StartsWith("--")) return false; + namedArgs.Add(name, argsQueue.Dequeue()); + } + + return argsQueue.Count() == 0; + } + + private void PrintUsage() + { + if (_subRunners.Any()) + { + // List sub commands + _output.WriteLine(CommandDescription); + _output.WriteLine("Commands:"); + foreach (var runner in _subRunners) + { + var shortName = runner.CommandName.Split(' ').Last(); + if (shortName.StartsWith("_")) continue; // convention to hide commands + _output.WriteLine($" {shortName}: {runner.CommandDescription}"); + } + _output.WriteLine(); + } + else + { + // Usage for this command + var optionsPart = _optionDescriptors.Any() ? "[options] " : ""; + var argParts = _argumentDescriptors.Keys.Select(name => $"[{name}]"); + _output.WriteLine($"Usage: {CommandName} {optionsPart}{string.Join(" ", argParts)}"); + _output.WriteLine(); + + // Arguments + foreach (var entry in _argumentDescriptors) + { + _output.WriteLine($"{entry.Key}:"); + _output.WriteLine($" {entry.Value}"); + _output.WriteLine(); + } + + // Options + if (_optionDescriptors.Any()) + { + _output.WriteLine("options:"); + foreach (var entry in _optionDescriptors) + { + _output.WriteLine($" {entry.Key}: {entry.Value}"); + } + _output.WriteLine(); + } + } + } + } +} diff --git a/src/dotnet-swagger/Program.cs b/src/dotnet-swagger/Program.cs new file mode 100644 index 0000000000..ef8750c002 --- /dev/null +++ b/src/dotnet-swagger/Program.cs @@ -0,0 +1,94 @@ +using System; +using System.Reflection; +using System.Diagnostics; +using System.IO; +using System.Runtime.Loader; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Options; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Swagger; + +namespace Swashbuckle.AspNetCore.Cli +{ + class Program + { + static int Main(string[] args) + { + // Helper to simplify command line parsing etc. + var runner = new CommandRunner("dotnet swagger", "Swashbuckle (Swagger) Command Line Tools", Console.Out); + + // NOTE: The "dotnet swagger tofile" command does not serve the request directly. Instead, it invokes a corresponding + // command (called _tofile) via "dotnet exec" so that the runtime configuration (*.runtimeconfig & *.deps.json) of the + // provided startupassembly can be used instead of the tool's. This is neccessary to successfully load the + // startupassembly and it's transitive dependencies. See https://github.com/dotnet/coreclr/issues/13277 for more. + + // > dotnet swagger tofile ... + runner.SubCommand("tofile", "retrieves Swagger from a startup assembly, and writes to file ", c => + { + c.Argument("startupassembly", "relative path to the application's startup assembly"); + c.Argument("swaggerdoc", "name of the swagger doc you want to retrieve, as configured in your startup class"); + c.Argument("output", "relative path where the Swagger will be output"); + c.Option("--host", "a specific host to include in the Swagger output"); + c.Option("--basepath", "a specific basePath to inlcude in the Swagger output"); + c.OnRun((namedArgs) => + { + var depsFile = namedArgs["startupassembly"].Replace(".dll", ".deps.json"); + var runtimeConfig = namedArgs["startupassembly"].Replace(".dll", ".runtimeconfig.json"); + + var subProcess = Process.Start("dotnet", string.Format( + "exec --depsfile {0} --runtimeconfig {1} {2} _{3}", // note the underscore + depsFile, + runtimeConfig, + typeof(Program).GetTypeInfo().Assembly.Location, + string.Join(" ", args) + )); + + subProcess.WaitForExit(); + return subProcess.ExitCode; + }); + }); + + // > dotnet swagger _tofile ... (* should only be invoked via "dotnet exec") + runner.SubCommand("_tofile", "retrieves Swagger from a startup assembly, and writes to file ", c => + { + c.Argument("startupassembly", "relative path to the application's startup assembly"); + c.Argument("swaggerdoc", "name of the swagger doc you want to retrieve, as configured in your startup class"); + c.Argument("output", "relative path where the Swagger will be output"); + c.Option("--host", "a specific host to include in the Swagger output"); + c.Option("--basepath", "a specific basePath to inlcude in the Swagger output"); + c.OnRun((namedArgs) => + { + // 1) Configure host with provided startupassembly + var startupAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath( + $"{Directory.GetCurrentDirectory()}\\{namedArgs["startupassembly"]}"); + var host = WebHost.CreateDefaultBuilder() + .UseStartup(startupAssembly.FullName) + .Build(); + + // 2) Retrieve Swagger via configured provider + var swaggerProvider = (ISwaggerProvider)host.Services.GetService(typeof(ISwaggerProvider)); + var swagger = swaggerProvider.GetSwagger( + namedArgs["swaggerdoc"], + namedArgs.ContainsKey("--host") ? namedArgs["--host"] : null, + namedArgs.ContainsKey("--basepath") ? namedArgs["--basepath"] : null, + null); + + // 3) Write to specified output location + var outputPath = $"{Directory.GetCurrentDirectory()}\\{namedArgs["output"]}"; + var mvcOptionsAccessor = (IOptions)host.Services.GetService(typeof(IOptions)); + var serializer = SwaggerSerializerFactory.Create(mvcOptionsAccessor); + using (var streamWriter = File.CreateText(outputPath)) + { + serializer.Serialize(streamWriter, swagger); + Console.WriteLine($"Swagger JSON succesfully written to {outputPath}"); + } + + return 0; + }); + }); + + return runner.Run(args); + } + } +} diff --git a/src/dotnet-swagger/dotnet-swagger.csproj b/src/dotnet-swagger/dotnet-swagger.csproj new file mode 100644 index 0000000000..fcc1eede1b --- /dev/null +++ b/src/dotnet-swagger/dotnet-swagger.csproj @@ -0,0 +1,18 @@ + + + + TODO + netcoreapp2.0 + 1.2.0-beta1 + Exe + Swashbuckle.AspNetCore.Cli + + + + + + + + + + \ No newline at end of file diff --git a/test/WebSites/CliExample/CliExample.csproj b/test/WebSites/CliExample/CliExample.csproj new file mode 100644 index 0000000000..07934321c4 --- /dev/null +++ b/test/WebSites/CliExample/CliExample.csproj @@ -0,0 +1,29 @@ + + + + netcoreapp2.0 + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/WebSites/CliExample/Controllers/CrudActionsController.cs b/test/WebSites/CliExample/Controllers/CrudActionsController.cs new file mode 100644 index 0000000000..14c5879b08 --- /dev/null +++ b/test/WebSites/CliExample/Controllers/CrudActionsController.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Mvc; + +namespace CliExample.Controllers +{ + [Route("/products")] + [Produces("application/json")] + public class CrudActionsController + { + [HttpPost] + public int Create([FromBody, Required]Product product) + { + return 1; + } + + [HttpGet] + public IEnumerable GetAll() + { + return new[] + { + new Product { Id = 1, Description = "A product" }, + new Product { Id = 2, Description = "Another product" }, + }; + } + + [HttpGet("{id}")] + public Product GetById(int id) + { + return new Product { Id = id, Description = "A product" }; + } + + [HttpPut("{id}")] + public void Update(int id, [FromBody, Required]Product product) + { + } + + [HttpPatch("{id}")] + public void PartialUpdate(int id, [FromBody, Required]IDictionary updates) + { + } + + [HttpDelete("{id}")] + public void Delete(int id) + { + } + } + + public class Product + { + public int Id { get; set; } + + public string Description { get; set; } + } +} \ No newline at end of file diff --git a/test/WebSites/CliExample/Program.cs b/test/WebSites/CliExample/Program.cs new file mode 100644 index 0000000000..77727f4872 --- /dev/null +++ b/test/WebSites/CliExample/Program.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace CliExample +{ + public class Program + { + public static void Main(string[] args) + { + BuildWebHost(args).Run(); + } + + public static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup() + .Build(); + } +} diff --git a/test/WebSites/CliExample/Properties/launchSettings.json b/test/WebSites/CliExample/Properties/launchSettings.json new file mode 100644 index 0000000000..589e4e8aec --- /dev/null +++ b/test/WebSites/CliExample/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:51071/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "api-docs", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "CliExample": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:51072/" + } + } +} \ No newline at end of file diff --git a/test/WebSites/CliExample/Startup.cs b/test/WebSites/CliExample/Startup.cs new file mode 100644 index 0000000000..1074c7832a --- /dev/null +++ b/test/WebSites/CliExample/Startup.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace CliExample +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new Swashbuckle.AspNetCore.Swagger.Info { Title = "CRUD API", Version = "v1" }); + }); + + services.AddMvc(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + // If available, serve Swagger JSON from static file (i.e. pre-generated via "dotnet swagger") + app.UseStaticFiles(); + + // Otherwise, generate on-the-fly via regular Swagger middleware + app.UseSwagger(c => + { + c.RouteTemplate = "api-docs/{documentName}/swagger.json"; + c.PreSerializeFilters.Add((swagger, httpReq) => swagger.Host = httpReq.Host.Value); + }); + + app.UseSwaggerUI(c => + { + c.RoutePrefix = "api-docs"; + c.SwaggerEndpoint("v1/swagger.json", "V1 Docs"); + }); + + app.UseMvc(); + } + } +} diff --git a/test/WebSites/CliExample/wwwroot/api-docs/v1/.gitkeep b/test/WebSites/CliExample/wwwroot/api-docs/v1/.gitkeep new file mode 100644 index 0000000000..5f282702bb --- /dev/null +++ b/test/WebSites/CliExample/wwwroot/api-docs/v1/.gitkeep @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/dotnet-swagger.Test/CommandRunnerTests.cs b/test/dotnet-swagger.Test/CommandRunnerTests.cs new file mode 100644 index 0000000000..8ad056c34c --- /dev/null +++ b/test/dotnet-swagger.Test/CommandRunnerTests.cs @@ -0,0 +1,106 @@ +using System.Collections.Generic; +using System.IO; +using Xunit; + +namespace Swashbuckle.AspNetCore.Cli.Test +{ + public class CommandRunnerTests + { + [Fact] + public void Run_ParsesArgumentsAndExecutesCommands_AccordingToPreConfiguredMetadata() + { + var receivedValues = new List(); + var subject = new CommandRunner("test", "a test", new StringWriter()); + subject.SubCommand("cmd1", "", c => { + c.Option("--opt1", ""); + c.Argument("arg1", ""); + c.OnRun((namedArgs) => + { + receivedValues.Add(namedArgs["--opt1"]); + receivedValues.Add(namedArgs["arg1"]); + return 2; + }); + }); + subject.SubCommand("cmd2", "", c => { + c.Option("--opt1", ""); + c.Argument("arg1", ""); + c.OnRun((namedArgs) => + { + receivedValues.Add(namedArgs["--opt1"]); + receivedValues.Add(namedArgs["arg1"]); + return 3; + }); + }); + + var cmd1ExitCode = subject.Run(new[] { "cmd1", "--opt1", "foo", "bar" }); + var cmd2ExitCode = subject.Run(new[] { "cmd2", "--opt1", "blah", "dblah" }); + + Assert.Equal(2, cmd1ExitCode); + Assert.Equal(3, cmd2ExitCode); + Assert.Equal(new[] { "foo", "bar", "blah", "dblah" }, receivedValues.ToArray()); + } + + [Fact] + public void Run_PrintsAvailableCommands_WhenUnexpectedCommandIsProvided() + { + var output = new StringWriter(); + var subject = new CommandRunner("test", "a test", output); + subject.SubCommand("cmd", "does something", c => { + }); + + var exitCode = subject.Run(new[] { "foo" }); + + Assert.StartsWith("a test", output.ToString()); + Assert.Contains("Commands:", output.ToString()); + Assert.Contains("cmd: does something", output.ToString()); + } + + [Fact] + public void Run_PrintsAvailableCommands_WhenHelpOptionIsProvided() + { + var output = new StringWriter(); + var subject = new CommandRunner("test", "a test", output); + subject.SubCommand("cmd", "does something", c => { + }); + + var exitCode = subject.Run(new[] { "--help" }); + + Assert.StartsWith("a test", output.ToString()); + Assert.Contains("Commands:", output.ToString()); + Assert.Contains("cmd: does something", output.ToString()); + } + + [Theory] + [InlineData(new[] { "--opt1" }, new string[] { }, new[] { "cmd", "--opt2", "foo" }, true)] + [InlineData(new[] { "--opt1" }, new string[] { }, new[] { "cmd", "--opt1" }, true)] + [InlineData(new[] { "--opt1" }, new string[] { }, new[] { "cmd", "--opt1", "--opt2" }, true)] + [InlineData(new[] { "--opt1" }, new string[] { }, new[] { "cmd", "--opt1", "foo" }, false)] + [InlineData(new string[] { }, new[] { "arg1" }, new[] { "cmd" }, true)] + [InlineData(new string[] { }, new[] { "arg1" }, new[] { "cmd", "--opt1" }, true)] + [InlineData(new string[] {}, new[] { "arg1" }, new[] { "cmd", "foo", "bar" }, true)] + [InlineData(new string[] {}, new[] { "arg1" }, new[] { "cmd", "foo" }, false)] + public void Run_PrintsCommandUsage_WhenUnexpectedArgumentsAreProvided( + string[] optionNames, + string[] argNames, + string[] providedArgs, + bool shouldPrintUsage) + { + var output = new StringWriter(); + var subject = new CommandRunner("test", "a test", output); + subject.SubCommand("cmd", "a command", c => + { + foreach (var name in optionNames) + c.Option(name, ""); + foreach (var name in argNames) + c.Argument(name, ""); + }); + + subject.Run(providedArgs); + + if (shouldPrintUsage) + Assert.StartsWith("Usage: test cmd", output.ToString()); + else + Assert.Empty(output.ToString()); + } + } +} \ No newline at end of file diff --git a/test/dotnet-swagger.Test/dotnet-swagger.Test.csproj b/test/dotnet-swagger.Test/dotnet-swagger.Test.csproj new file mode 100644 index 0000000000..9c59ded81b --- /dev/null +++ b/test/dotnet-swagger.Test/dotnet-swagger.Test.csproj @@ -0,0 +1,22 @@ + + + + netcoreapp2.0 + + false + + Swashbuckle.AspNetCore.Cli + + + + + + + + + + + + + +