Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dotnet-swagger CLI tool beta #590

Merged
merged 1 commit into from
Feb 12, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ project.lock.json
artifacts/
*.nuget.props
.DS_Store
Thumbs.db
Thumbs.db
test/WebSites/CliExample/wwwroot/api-docs/v1/*.json
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 #

Expand Down Expand Up @@ -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 ###
Expand Down Expand Up @@ -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
<ItemGroup>
<DotNetCliToolReference Include="dotnet-swagger" Version="1.2.0-beta1" />
</ItemGroup>
```

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.
46 changes: 46 additions & 0 deletions Swashbuckle.AspNetCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Swashbuckle.AspNetCore.Swagger
{
public class SwaggerSerializerFactory
{
internal static JsonSerializer Create(IOptions<MvcJsonOptions> applicationJsonOptions)
public static JsonSerializer Create(IOptions<MvcJsonOptions> applicationJsonOptions)
{
// TODO: Should this handle case where mvcJsonOptions.Value == null?
return new JsonSerializer
Expand Down
140 changes: 140 additions & 0 deletions src/dotnet-swagger/CommandRunner.cs
Original file line number Diff line number Diff line change
@@ -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<string, string> _argumentDescriptors;
private readonly Dictionary<string, string> _optionDescriptors;
private Func<IDictionary<string, string>, int> _runFunc;
private readonly List<CommandRunner> _subRunners;
private readonly TextWriter _output;

public CommandRunner(string commandName, string commandDescription, TextWriter output)
{
CommandName = commandName;
CommandDescription = commandDescription;
_argumentDescriptors = new Dictionary<string, string>();
_optionDescriptors = new Dictionary<string, string>();
_runFunc = (namedArgs) => { return 1; }; // noop
_subRunners = new List<CommandRunner>();
_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<IDictionary<string, string>, int> runFunc)
{
_runFunc = runFunc;
}

public void SubCommand(string name, string description, Action<CommandRunner> configAction)
{
var runner = new CommandRunner($"{CommandName} {name}", description, _output);
configAction(runner);
_subRunners.Add(runner);
}

public int Run(IEnumerable<string> 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<string, string> namedArgs))
{
PrintUsage();
return 1;
}

return _runFunc(namedArgs);
}

private bool TryParseArgs(IEnumerable<string> args, out IDictionary<string, string> namedArgs)
{
namedArgs = new Dictionary<string, string>();
var argsQueue = new Queue<string>(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();
}
}
}
}
}
94 changes: 94 additions & 0 deletions src/dotnet-swagger/Program.cs
Original file line number Diff line number Diff line change
@@ -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<MvcJsonOptions>)host.Services.GetService(typeof(IOptions<MvcJsonOptions>));
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);
}
}
}
Loading