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

Updated MS Task with swagger/swashbuckle install #6

Merged
merged 13 commits into from
Nov 2, 2023
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ bld/
[Oo]bj/
[Ll]og/
[Ll]ogs/
[Tt]ool/

# Visual Studio 2015/2017 cache/options directory
.vs/
Expand Down
42 changes: 42 additions & 0 deletions src/Workleap.OpenApi.MSBuild/HttpClientWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System.Net;

namespace Workleap.OpenApi.MSBuild;

internal sealed class HttpClientWrapper : IHttpClientWrapper, IDisposable
{
private readonly HttpClient _httpClient = new();

public async Task DownloadFileToDestinationAsync(string url, string destination, CancellationToken cancellationToken)
{
using var responseStream = await this._httpClient.GetStreamAsync(url);

if (responseStream == null)
{
using var retryResponseStream = await this._httpClient.GetStreamAsync(url);
if (retryResponseStream != null)
{
await SaveFileFromResponseAsync(destination, retryResponseStream, cancellationToken);
}
else
{
throw new OpenApiTaskFailedException("Spectral could not be downloaded.");
}
}
else
{
await SaveFileFromResponseAsync(destination, responseStream, cancellationToken);
}
}

private static async Task SaveFileFromResponseAsync(string destination, Stream responseStream, CancellationToken cancellationToken)
{
using var fileTarget = new FileStream(destination, FileMode.Create, FileAccess.Write, FileShare.None);

await responseStream.CopyToAsync(fileTarget, 81920, cancellationToken);
}

public void Dispose()
{
this._httpClient.Dispose();
}
}
6 changes: 6 additions & 0 deletions src/Workleap.OpenApi.MSBuild/IHttpClientWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Workleap.OpenApi.MSBuild;

internal interface IHttpClientWrapper
{
Task DownloadFileToDestinationAsync(string url, string destination, CancellationToken cancellationToken);
}
10 changes: 10 additions & 0 deletions src/Workleap.OpenApi.MSBuild/ILoggerWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Microsoft.Build.Utilities;

namespace Workleap.OpenApi.MSBuild;

public interface ILoggerWrapper
Zav marked this conversation as resolved.
Show resolved Hide resolved
{
void LogWarning(string message, params object[] messageArgs);

void LogMessage(string message, params object[] messageArgs);
}
6 changes: 6 additions & 0 deletions src/Workleap.OpenApi.MSBuild/IProcessWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Workleap.OpenApi.MSBuild;

public interface IProcessWrapper
{
public Task<int> RunProcessAsync(string filename, string[] arguments, CancellationToken cancellationToken);
}
6 changes: 6 additions & 0 deletions src/Workleap.OpenApi.MSBuild/ISpectralManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Workleap.OpenApi.MSBuild;

public interface ISpectralManager
{
public Task InstallSpectralAsync(CancellationToken cancellationToken);
}
10 changes: 10 additions & 0 deletions src/Workleap.OpenApi.MSBuild/ISwaggerManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Workleap.OpenApi.MSBuild;

public interface ISwaggerManager
{
Task RunSwaggerAsync(string[] openApiSwaggerDocumentNames, CancellationToken cancellationToken);

Task InstallSwaggerCliAsync(CancellationToken cancellationToken);

Task GenerateOpenApiSpecAsync(string swaggerExePath, string outputOpenApiSpecPath, string documentName, CancellationToken cancellationToken);
}
23 changes: 23 additions & 0 deletions src/Workleap.OpenApi.MSBuild/LoggerWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Microsoft.Build.Utilities;

namespace Workleap.OpenApi.MSBuild;

internal sealed class LoggerWrapper : ILoggerWrapper
{
public LoggerWrapper(TaskLoggingHelper helper)
{
this.Helper = helper;
}

public TaskLoggingHelper Helper { get; set; }

public void LogWarning(string message, params object[] messageArgs)
{
this.Helper.LogWarning(message, messageArgs);
}

public void LogMessage(string message, params object[] messageArgs)
{
this.Helper.LogMessage(message, messageArgs);
}
}
8 changes: 8 additions & 0 deletions src/Workleap.OpenApi.MSBuild/OpenApiTaskFailedException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Workleap.OpenApi.MSBuild;

public class OpenApiTaskFailedException : Exception
{
public OpenApiTaskFailedException(string message) : base(message)
{
}
}
27 changes: 27 additions & 0 deletions src/Workleap.OpenApi.MSBuild/ProcessWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using CliWrap;
using CliWrap.Buffered;

namespace Workleap.OpenApi.MSBuild;

internal sealed class ProcessWrapper : IProcessWrapper
{
private readonly string _workingDirectory;

public ProcessWrapper(string workingDirectory)
{
this._workingDirectory = workingDirectory;
}

public async Task<int> RunProcessAsync(string filename, string[] arguments, CancellationToken cancellationToken)
Zav marked this conversation as resolved.
Show resolved Hide resolved
{
var result = await Cli.Wrap(filename)
.WithWorkingDirectory(this._workingDirectory)
.WithValidation(CommandResultValidation.None)
.WithArguments(arguments)
.WithStandardOutputPipe(PipeTarget.ToStream(Console.OpenStandardOutput()))
.WithStandardErrorPipe(PipeTarget.ToStream(Console.OpenStandardError()))
.ExecuteBufferedAsync(cancellationToken);

return result.ExitCode;
}
}
89 changes: 89 additions & 0 deletions src/Workleap.OpenApi.MSBuild/SpectralManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System.Runtime.InteropServices;
using Microsoft.Build.Framework;

namespace Workleap.OpenApi.MSBuild;

internal sealed class SpectralManager : ISpectralManager
{
private const string SpectralVersion = "6.11.0";

private readonly ILoggerWrapper _loggerWrapper;
private readonly IHttpClientWrapper _httpClientWrapper;
private readonly string _toolDirectory;

public SpectralManager(LoggerWrapper loggerWrapper, string openApiToolsDirectoryPath, IHttpClientWrapper httpClientWrapper)
{
this._loggerWrapper = loggerWrapper;
this._httpClientWrapper = httpClientWrapper;
this._toolDirectory = Path.Combine(openApiToolsDirectoryPath, "spectral", SpectralVersion);
}

public async Task InstallSpectralAsync(CancellationToken cancellationToken)
{
Directory.CreateDirectory(this._toolDirectory);

var executableFileName = GetSpectralFileName();
var url = $"https://github.com/stoplightio/spectral/releases/download/v{SpectralVersion}/{executableFileName}";

await this._httpClientWrapper.DownloadFileToDestinationAsync(url, Path.Combine(this._toolDirectory, executableFileName), cancellationToken);
}

private static string GetSpectralFileName()
{
var osType = GetOperatingSystem();
var architecture = GetArchitecture();

if (osType == "linux")
{
var distro = File.Exists("/etc/os-release") ? File.ReadAllText("/etc/os-release") : string.Empty;
Zav marked this conversation as resolved.
Show resolved Hide resolved
if (distro.Contains("Alpine Linux"))
{
osType = "alpine";
}
}

var fileName = $"spectral-{osType}-{architecture}";

if (osType == "win")
{
fileName = "spectral.exe";
}

return fileName;
}

private static string GetOperatingSystem()
ruvyas marked this conversation as resolved.
Show resolved Hide resolved
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return "linux";
}

if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return "macos";
}

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return "win";
}

throw new OpenApiTaskFailedException("Unknown operating system encountered");
}

private static string GetArchitecture()
ruvyas marked this conversation as resolved.
Show resolved Hide resolved
{
if (RuntimeInformation.OSArchitecture == Architecture.X64)
{
return "x64";
}

if (RuntimeInformation.OSArchitecture == Architecture.Arm64)
{
return "arm64";
}

throw new OpenApiTaskFailedException("Unknown processor architecture encountered");
}
}
67 changes: 67 additions & 0 deletions src/Workleap.OpenApi.MSBuild/SwaggerManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System.Runtime.InteropServices;

namespace Workleap.OpenApi.MSBuild;

internal sealed class SwaggerManager : ISwaggerManager
{
private const string SwaggerVersion = "6.5.0";
private readonly IProcessWrapper _processWrapper;
private readonly ILoggerWrapper _loggerWrapper;
private readonly string _openApiWebApiAssemblyPath;
private readonly string _swaggerDirectory;

public SwaggerManager(IProcessWrapper processWrapper, ILoggerWrapper loggerWrapper, string openApiToolsDirectoryPath, string openApiWebApiAssemblyPath)
{
this._processWrapper = processWrapper;
this._loggerWrapper = loggerWrapper;
this._openApiWebApiAssemblyPath = openApiWebApiAssemblyPath;
this._swaggerDirectory = Path.Combine(openApiToolsDirectoryPath, "swagger", SwaggerVersion);
}

public async Task RunSwaggerAsync(string[] openApiSwaggerDocumentNames, CancellationToken cancellationToken)
{
var swaggerExePath = Path.Combine(this._swaggerDirectory, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "swagger.exe" : "swagger");

foreach (var documentName in openApiSwaggerDocumentNames)
{
var outputOpenApiSpecName = $"openapi-{documentName.ToLowerInvariant()}.yaml";

var outputOpenApiSpecPath = Path.Combine(this._swaggerDirectory, outputOpenApiSpecName);
Zav marked this conversation as resolved.
Show resolved Hide resolved

await this.GenerateOpenApiSpecAsync(swaggerExePath, outputOpenApiSpecPath, documentName, cancellationToken);
}
}

public async Task InstallSwaggerCliAsync(CancellationToken cancellationToken)
Zav marked this conversation as resolved.
Show resolved Hide resolved
{
var retryCount = 0;
while (retryCount < 2)
{
var exitCode = await this._processWrapper.RunProcessAsync("dotnet", new[] { "tool", "update", "Swashbuckle.AspNetCore.Cli", "--tool-path", this._swaggerDirectory, "--version", SwaggerVersion }, cancellationToken);

if (exitCode != 0 && retryCount != 1)
{
this._loggerWrapper.LogWarning("Swashbuckle download failed. Retrying once more...");
retryCount++;
continue;
}

if (retryCount == 1 && exitCode != 0)
{
throw new OpenApiTaskFailedException("Swashbuckle CLI could not be installed.");
}

break;
}
}

public async Task GenerateOpenApiSpecAsync(string swaggerExePath, string outputOpenApiSpecPath, string documentName, CancellationToken cancellationToken)
{
var exitCode = await this._processWrapper.RunProcessAsync(swaggerExePath, new[] { "tofile", "--output", outputOpenApiSpecPath, "--yaml", this._openApiWebApiAssemblyPath, documentName }, cancellationToken);

if (exitCode != 0)
{
throw new OpenApiTaskFailedException("OpenApi file could not be created.");
Zav marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
53 changes: 46 additions & 7 deletions src/Workleap.OpenApi.MSBuild/ValidateOpenApiTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,53 @@ public sealed class ValidateOpenApiTask : CancelableAsyncTask
[Required]
public string[] OpenApiSpecificationFiles { get; set; } = Array.Empty<string>();

protected override Task<bool> ExecuteAsync(CancellationToken cancellationToken)
protected override async Task<bool> ExecuteAsync(CancellationToken cancellationToken)
{
this.Log.LogMessage(MessageImportance.High, "OpenApiWebApiAssemblyPath = '{0}'", this.OpenApiWebApiAssemblyPath);
this.Log.LogMessage(MessageImportance.High, "OpenApiToolsDirectoryPath = '{0}'", this.OpenApiToolsDirectoryPath);
this.Log.LogMessage(MessageImportance.High, "OpenApiSpectralRulesetUrl = '{0}'", this.OpenApiSpectralRulesetUrl);
this.Log.LogMessage(MessageImportance.High, "OpenApiSwaggerDocumentNames = '{0}'", string.Join(", ", this.OpenApiSwaggerDocumentNames));
this.Log.LogMessage(MessageImportance.High, "OpenApiSpecificationFiles = '{0}'", string.Join(", ", this.OpenApiSpecificationFiles));
var loggerWrapper = new LoggerWrapper(this.Log);
var processWrapper = new ProcessWrapper(this.OpenApiToolsDirectoryPath);
var swaggerManager = new SwaggerManager(processWrapper, loggerWrapper, this.OpenApiToolsDirectoryPath, this.OpenApiToolsDirectoryPath);
using var httpClientWrapper = new HttpClientWrapper();

return Task.FromResult(true);
var spectralManager = new SpectralManager(loggerWrapper, this.OpenApiToolsDirectoryPath, httpClientWrapper);

this.Log.LogMessage(MessageImportance.Low, "{0} = '{1}'", nameof(this.OpenApiWebApiAssemblyPath), this.OpenApiWebApiAssemblyPath);
this.Log.LogMessage(MessageImportance.Low, "{0} = '{1}'", nameof(this.OpenApiToolsDirectoryPath), this.OpenApiToolsDirectoryPath);
this.Log.LogMessage(MessageImportance.Low, "{0} = '{1}'", nameof(this.OpenApiSpectralRulesetUrl), this.OpenApiSpectralRulesetUrl);
this.Log.LogMessage(MessageImportance.Low, "{0} = '{1}'", nameof(this.OpenApiSwaggerDocumentNames), string.Join(", ", this.OpenApiSwaggerDocumentNames));
this.Log.LogMessage(MessageImportance.Low, "{0} = '{1}'", nameof(this.OpenApiSpecificationFiles), string.Join(", ", this.OpenApiSpecificationFiles));

if (this.OpenApiSpecificationFiles.Length != this.OpenApiSwaggerDocumentNames.Length)
{
this.Log.LogWarning("You must provide the same amount of open api specification file names and swagger document file names");

return false;
}

try
{
await this.GeneratePublicNugetSource();

await swaggerManager.InstallSwaggerCliAsync(cancellationToken);
await spectralManager.InstallSpectralAsync(cancellationToken);

// await swaggerManager.RunSwaggerAsync(this.OpenApiSwaggerDocumentNames, cancellationToken);
}
catch (OpenApiTaskFailedException e)
{
this.Log.LogWarning("An error occured while validating the OpenAPI specification: {0}", e.Message);
}

return true;
}

private async Task GeneratePublicNugetSource()
{
Directory.CreateDirectory(this.OpenApiToolsDirectoryPath);

if (!File.Exists(Path.Combine(this.OpenApiToolsDirectoryPath, "nuget.config")))
{
var path = Path.Combine(this.OpenApiToolsDirectoryPath, "nuget.config");
File.WriteAllText(path, "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<configuration>\n <packageSources>\n <clear />\n <add key=\"nuget\" value=\"https://api.nuget.org/v3/index.json\" />\n </packageSources>\n</configuration>");
}
}
}