Skip to content

Commit

Permalink
Merge pull request #18 from Ellerbach/add-openapi-tool
Browse files Browse the repository at this point in the history
Added OpenAPI tool
  • Loading branch information
mtirionMSFT authored Jan 19, 2023
2 parents 41638b1 + 0d2b8fa commit 28cbbde
Show file tree
Hide file tree
Showing 8 changed files with 386 additions and 0 deletions.
191 changes: 191 additions & 0 deletions src/DocFxOpenApi/DocFxOpenApi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// Licensed to DocFX Companion Tools and contributors under one or more agreements.
// DocFX Companion Tools and contributors licenses this file to you under the MIT license.

namespace DocFxOpenApi
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using CommandLine;
using global::DocFxOpenApi.Domain;
using global::DocFxOpenApi.Helpers;
using Microsoft.OpenApi;
using Microsoft.OpenApi.Extensions;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Readers;

/// <summary>
/// Open API file converter to V2 JSON files.
/// </summary>
internal class DocFxOpenApi
{
private const OpenApiSpecVersion OutputVersion = OpenApiSpecVersion.OpenApi2_0;
private static readonly string[] _openApiFileExtensions = { "json", "yaml", "yml" };

private static int _returnvalue;
private CommandlineOptions _options;
private MessageHelper _message;

private DocFxOpenApi(CommandlineOptions thisOptions)
{
_options = thisOptions;
_message = new MessageHelper(thisOptions);
}

/// <summary>
/// Main entry point.
/// </summary>
/// <param name="args">Commandline options described in <see cref="CommandlineOptions"/> class.</param>
/// <returns>0 if successful, 1 on error.</returns>
private static int Main(string[] args)
{
Parser.Default.ParseArguments<CommandlineOptions>(args)
.WithParsed(RunLogic)
.WithNotParsed(HandleErrors);

Console.WriteLine($"Exit with return code {_returnvalue}");

return _returnvalue;
}

/// <summary>
/// Run the logic of the app with the given parameters.
/// Given folders are checked if they exist.
/// </summary>
/// <param name="o">Parsed commandline options.</param>
private static void RunLogic(CommandlineOptions o)
{
new DocFxOpenApi(o).RunLogic();
}

/// <summary>
/// On parameter errors, we set the returnvalue to 1 to indicated an error.
/// </summary>
/// <param name="errors">List or errors (ignored).</param>
private static void HandleErrors(IEnumerable<Error> errors)
{
_returnvalue = 1;
}

private void RunLogic()
{
if (string.IsNullOrEmpty(_options.OutputFolder))
{
_options.OutputFolder = _options.SpecFolder;
}

_message.Verbose($"Specification folder: {_options.SpecFolder}");
_message.Verbose($"Output folder : {_options.OutputFolder}");
_message.Verbose($"Verbose : {_options.Verbose}");

if (!Directory.Exists(_options.SpecFolder))
{
_message.Error($"ERROR: Specification folder '{_options.SpecFolder}' doesn't exist.");
_returnvalue = 1;
return;
}

Directory.CreateDirectory(_options.OutputFolder!);

this.ConvertOpenApiFiles();
}

private void ConvertOpenApiFiles()
{
foreach (var extension in _openApiFileExtensions)
{
this.ConvertOpenApiFiles(extension);
}
}

private void ConvertOpenApiFiles(string extension)
{
foreach (var file in Directory.GetFiles(
_options.SpecFolder!,
$"*.{extension}",
new EnumerationOptions
{
MatchCasing = MatchCasing.CaseInsensitive,
RecurseSubdirectories = true,
}))
{
this.ConvertOpenApiFile(file);
}
}

private void ConvertOpenApiFile(string inputSpecFile)
{
_message.Verbose($"Reading OpenAPI file '{inputSpecFile}'");
using var stream = File.OpenRead(inputSpecFile);
var document = new OpenApiStreamReader().Read(stream, out var diagnostic);

if (diagnostic.Errors.Any())
{
_message.Error($"ERROR: Not a valid OpenAPI v2 or v3 specification");
foreach (var error in diagnostic.Errors)
{
_message.Error(error.ToString());
}

_returnvalue = 1;
return;
}

_message.Verbose($"Input OpenAPI version '{diagnostic.SpecificationVersion}'");

foreach (var (pathName, path) in document.Paths)
{
foreach (var (operationType, operation) in path.Operations)
{
var description = $"{pathName} {operationType}";

foreach (var (responseType, response) in operation.Responses)
{
foreach (var (mediaType, content) in response.Content)
{
this.CreateSingleExampleFromMultipleExamples(content, $"{description} response {responseType} {mediaType}");
}
}

foreach (var parameter in operation.Parameters)
{
foreach (var (mediaType, content) in parameter.Content)
{
this.CreateSingleExampleFromMultipleExamples(content, $"{description} parameter {parameter.Name} {mediaType}");
}
}

if (operation.RequestBody is not null)
{
foreach (var (mediaType, content) in operation.RequestBody.Content)
{
this.CreateSingleExampleFromMultipleExamples(content, $"{description} requestBody {mediaType}");

if (content.Example is not null && content.Schema is not null && content.Schema.Example is null)
{
_message.Verbose($"[OpenAPIv2 compatibility] Setting type example from sample requestBody example for {content.Schema.Reference?.ReferenceV2 ?? "item"} from {operation.OperationId}");
content.Schema.Example = content.Example;
}
}
}
}
}

var outputFileName = Path.ChangeExtension(Path.GetFileName(inputSpecFile), ".swagger.json");
var outputFile = Path.Combine(_options.OutputFolder!, outputFileName);
_message.Verbose($"Writing output file '{outputFile}' as version '{OutputVersion}'");
using FileStream fs = File.Create(outputFile);
document.Serialize(fs, OutputVersion, OpenApiFormat.Json);
}

private void CreateSingleExampleFromMultipleExamples(OpenApiMediaType content, string description)
{
if (content.Example is null && content.Examples.Any())
{
_message.Verbose($"[OpenAPIv2 compatibility] Setting example from first of multiple OpenAPIv3 examples for {description}");
content.Example = content.Examples.Values.First().Value;
}
}
}
}
19 changes: 19 additions & 0 deletions src/DocFxOpenApi/DocFxOpenApi.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.8.0" />
<PackageReference Include="GitVersionTask" Version="5.5.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.OpenApi.Readers" Version="1.5.0" />
</ItemGroup>
</Project>
34 changes: 34 additions & 0 deletions src/DocFxOpenApi/DocFxOpenApi.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26124.0
MinimumVisualStudioVersion = 15.0.26124.0
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocFxOpenApi", "DocFxOpenApi.csproj", "{AFE399B5-4E43-4D11-A854-5E49EEFC9F2B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{AFE399B5-4E43-4D11-A854-5E49EEFC9F2B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AFE399B5-4E43-4D11-A854-5E49EEFC9F2B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AFE399B5-4E43-4D11-A854-5E49EEFC9F2B}.Debug|x64.ActiveCfg = Debug|Any CPU
{AFE399B5-4E43-4D11-A854-5E49EEFC9F2B}.Debug|x64.Build.0 = Debug|Any CPU
{AFE399B5-4E43-4D11-A854-5E49EEFC9F2B}.Debug|x86.ActiveCfg = Debug|Any CPU
{AFE399B5-4E43-4D11-A854-5E49EEFC9F2B}.Debug|x86.Build.0 = Debug|Any CPU
{AFE399B5-4E43-4D11-A854-5E49EEFC9F2B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AFE399B5-4E43-4D11-A854-5E49EEFC9F2B}.Release|Any CPU.Build.0 = Release|Any CPU
{AFE399B5-4E43-4D11-A854-5E49EEFC9F2B}.Release|x64.ActiveCfg = Release|Any CPU
{AFE399B5-4E43-4D11-A854-5E49EEFC9F2B}.Release|x64.Build.0 = Release|Any CPU
{AFE399B5-4E43-4D11-A854-5E49EEFC9F2B}.Release|x86.ActiveCfg = Release|Any CPU
{AFE399B5-4E43-4D11-A854-5E49EEFC9F2B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
31 changes: 31 additions & 0 deletions src/DocFxOpenApi/Domain/CommandlineOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Licensed to DocFX Companion Tools and contributors under one or more agreements.
// DocFX Companion Tools and contributors licenses this file to you under the MIT license.

namespace DocFxOpenApi.Domain
{
using CommandLine;

/// <summary>
/// Class for command line options.
/// </summary>
public class CommandlineOptions
{
/// <summary>
/// Gets or sets the folder with specifications.
/// </summary>
[Option('s', "specfolder", Required = true, HelpText = "Folder containing the OpenAPI specification.")]
public string? SpecFolder { get; set; }

/// <summary>
/// Gets or sets the output folder.
/// </summary>
[Option('o', "outputfolder", Required = false, HelpText = "Folder to write the resulting specifications in.")]
public string? OutputFolder { get; set; }

/// <summary>
/// Gets or sets a value indicating whether verbose information is shown in the output.
/// </summary>
[Option('v', "verbose", Required = false, HelpText = "Show verbose messages.")]
public bool Verbose { get; set; }
}
}
9 changes: 9 additions & 0 deletions src/DocFxOpenApi/GlobalSuppressions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.

using System.Diagnostics.CodeAnalysis;

[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:File should have header", Justification = "We don't use file headers in DMP.")]
[assembly: SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Literals are allowed in a tool. No need for localization.")]
61 changes: 61 additions & 0 deletions src/DocFxOpenApi/Helpers/MessageHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Licensed to DocFX Companion Tools and contributors under one or more agreements.
// DocFX Companion Tools and contributors licenses this file to you under the MIT license.

namespace DocFxOpenApi.Helpers
{
using System;
using global::DocFxOpenApi.Domain;

/// <summary>
/// Helper methods to write messages to the console.
/// </summary>
public class MessageHelper
{
private readonly CommandlineOptions _options;

/// <summary>
/// Initializes a new instance of the <see cref="MessageHelper"/> class.
/// </summary>
/// <param name="options">Command line options.</param>
public MessageHelper(CommandlineOptions options)
{
this._options = options;
}

/// <summary>
/// Helper method for verbose messages.
/// </summary>
/// <param name="message">Message to show in verbose mode.</param>
public void Verbose(string message)
{
if (this._options.Verbose)
{
Console.WriteLine(message);
}
}

/// <summary>
/// Helper method for warning messages.
/// </summary>
/// <param name="message">Message to show in verbose mode.</param>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "We want same access for all methods.")]
public void Warning(string message)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine(message);
Console.ResetColor();
}

/// <summary>
/// Helper method for error messages.
/// </summary>
/// <param name="message">Message to show in verbose mode.</param>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "We want same access for all methods.")]
public void Error(string message)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(message);
Console.ResetColor();
}
}
}
6 changes: 6 additions & 0 deletions src/DocFxOpenApi/NuGet.Config
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
</packageSources>
</configuration>
35 changes: 35 additions & 0 deletions src/DocFxOpenApi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# OpenAPI specification converter for DocFX

This tool converts existing [OpenAPI](https://www.openapis.org/) specification files into the format compatible with DocFX (OpenAPI v2 JSON files). It allows DocFX to generate HTML pages from the OpenAPI specification. OpenAPI is also known as [Swagger](https://swagger.io/).

## Usage

```text
DocFxOpenApi -s <specs folder> [-o <output folder>] [-v]
-s, --specfolder Required. Folder containing the OpenAPI specification.
-o, --outputfolder Folder to write the resulting specifications in.
-v, --verbose Show verbose messages.
--help Display this help screen.
--version Display version information.
```

The tool converts any `*.json`, `*.yaml`, `*.yml` file from the provided specification folder into the output folder. It supports JSON or YAML-format, OpenAPI v2 or v3 (including 3.0.1) format files.

If the `-o or --outputfolder` is not provided, the output folder is set to the input specs folder.


If normal return code of the tool is 0, but on error it returns 1.

## Warnings, errors and verbose

If the tool encounters situations that might need some action, a warning is written to the output. The table of contents is still created.

If the tool encounters an error, an error message is written to the output. The table of contents will not be created. The tool will return error code 1.

If you want to trace what the tool is doing, use the `-v or verbose` flag to output all details of processing the files and folders and creating the table of contents.

## Limitations and workarounds

- DocFX only supports generating documentation [from OpenAPI v2 JSON files](https://dotnet.github.io/docfx/tutorial/intro_rest_api_documentation.html) as of May 2021. Therefore the utility converts input files into that format.
- DocFX [does not include type definitions](https://github.com/dotnet/docfx/issues/2072) as of May 2021.
- The OpenAPI v2 format does not allow providing multiple examples for result payloads. OpenAPI v3 allows providing either a single example or a collection of examples. If a collection of examples is provided, the utility uses the first example as an example in the output file.

0 comments on commit 28cbbde

Please sign in to comment.