diff --git a/src/Workleap.OpenApi.MSBuild/HttpClientWrapper.cs b/src/Workleap.OpenApi.MSBuild/HttpClientWrapper.cs index 1146a1e..142df59 100644 --- a/src/Workleap.OpenApi.MSBuild/HttpClientWrapper.cs +++ b/src/Workleap.OpenApi.MSBuild/HttpClientWrapper.cs @@ -1,30 +1,36 @@ -using System.Net; - namespace Workleap.OpenApi.MSBuild; internal sealed class HttpClientWrapper : IHttpClientWrapper, IDisposable { - private readonly HttpClient _httpClient = new(); + private readonly HttpClient _httpClient = new() { Timeout = TimeSpan.FromSeconds(10) }; public async Task DownloadFileToDestinationAsync(string url, string destination, CancellationToken cancellationToken) { - using var responseStream = await this._httpClient.GetStreamAsync(url); - - if (responseStream == null) + try { - using var retryResponseStream = await this._httpClient.GetStreamAsync(url); - if (retryResponseStream != null) + using var responseStream = await this._httpClient.GetStreamAsync(url); + + if (responseStream == null) { - await SaveFileFromResponseAsync(destination, retryResponseStream, cancellationToken); + 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 { - throw new OpenApiTaskFailedException("Spectral could not be downloaded."); + await SaveFileFromResponseAsync(destination, responseStream, cancellationToken); } } - else + catch (Exception) { - await SaveFileFromResponseAsync(destination, responseStream, cancellationToken); + File.Delete(destination); + throw; } } diff --git a/src/Workleap.OpenApi.MSBuild/ISpectralManager.cs b/src/Workleap.OpenApi.MSBuild/ISpectralManager.cs index 11fd5db..09abf05 100644 --- a/src/Workleap.OpenApi.MSBuild/ISpectralManager.cs +++ b/src/Workleap.OpenApi.MSBuild/ISpectralManager.cs @@ -3,4 +3,6 @@ namespace Workleap.OpenApi.MSBuild; public interface ISpectralManager { public Task InstallSpectralAsync(CancellationToken cancellationToken); + + public Task RunSpectralAsync(IEnumerable swaggerDocumentPaths, string rulesetUrl, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Workleap.OpenApi.MSBuild/ISwaggerManager.cs b/src/Workleap.OpenApi.MSBuild/ISwaggerManager.cs index d58c4f7..112b89b 100644 --- a/src/Workleap.OpenApi.MSBuild/ISwaggerManager.cs +++ b/src/Workleap.OpenApi.MSBuild/ISwaggerManager.cs @@ -2,9 +2,9 @@ namespace Workleap.OpenApi.MSBuild; public interface ISwaggerManager { - Task RunSwaggerAsync(string[] openApiSwaggerDocumentNames, CancellationToken cancellationToken); + Task> RunSwaggerAsync(string[] openApiSwaggerDocumentNames, CancellationToken cancellationToken); Task InstallSwaggerCliAsync(CancellationToken cancellationToken); - Task GenerateOpenApiSpecAsync(string swaggerExePath, string outputOpenApiSpecPath, string documentName, CancellationToken cancellationToken); + Task GenerateOpenApiSpecAsync(string swaggerExePath, string outputOpenApiSpecPath, string documentName, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Workleap.OpenApi.MSBuild/SpectralManager.cs b/src/Workleap.OpenApi.MSBuild/SpectralManager.cs index 339e42f..19f7415 100644 --- a/src/Workleap.OpenApi.MSBuild/SpectralManager.cs +++ b/src/Workleap.OpenApi.MSBuild/SpectralManager.cs @@ -1,5 +1,4 @@ using System.Runtime.InteropServices; -using Microsoft.Build.Framework; namespace Workleap.OpenApi.MSBuild; @@ -9,23 +8,49 @@ internal sealed class SpectralManager : ISpectralManager private readonly ILoggerWrapper _loggerWrapper; private readonly IHttpClientWrapper _httpClientWrapper; - private readonly string _toolDirectory; - - public SpectralManager(LoggerWrapper loggerWrapper, string openApiToolsDirectoryPath, IHttpClientWrapper httpClientWrapper) + private readonly string _spectralDirectory; + private readonly string _openApiToolsDirectory; + private readonly IProcessWrapper _processWrapper; + + public SpectralManager(ILoggerWrapper loggerWrapper, string openApiToolsDirectoryPath, IHttpClientWrapper httpClientWrapper, IProcessWrapper processWrapper) { this._loggerWrapper = loggerWrapper; this._httpClientWrapper = httpClientWrapper; - this._toolDirectory = Path.Combine(openApiToolsDirectoryPath, "spectral", SpectralVersion); + this._openApiToolsDirectory = openApiToolsDirectoryPath; + this._spectralDirectory = Path.Combine(openApiToolsDirectoryPath, "spectral", SpectralVersion); + this._processWrapper = processWrapper; } + + private string ExecutablePath { get; set; } = string.Empty; public async Task InstallSpectralAsync(CancellationToken cancellationToken) { - Directory.CreateDirectory(this._toolDirectory); + Directory.CreateDirectory(this._spectralDirectory); + + this.ExecutablePath = GetSpectralFileName(); + var url = $"https://github.com/stoplightio/spectral/releases/download/v{SpectralVersion}/{this.ExecutablePath}"; + var destination = Path.Combine(this._spectralDirectory, this.ExecutablePath); + + if (!File.Exists(destination)) + { + await this._httpClientWrapper.DownloadFileToDestinationAsync(url, destination, cancellationToken); + } + } - var executableFileName = GetSpectralFileName(); - var url = $"https://github.com/stoplightio/spectral/releases/download/v{SpectralVersion}/{executableFileName}"; + public async Task RunSpectralAsync(IEnumerable swaggerDocumentPaths, string rulesetUrl, CancellationToken cancellationToken) + { + this._loggerWrapper.LogMessage("Starting Spectral report generation."); + + var spectralExecutePath = Path.Combine(this._spectralDirectory, this.ExecutablePath); + var reportsPath = Path.Combine(this._openApiToolsDirectory, "reports"); + Directory.CreateDirectory(reportsPath); - await this._httpClientWrapper.DownloadFileToDestinationAsync(url, Path.Combine(this._toolDirectory, executableFileName), cancellationToken); + foreach (var documentPath in swaggerDocumentPaths) + { + var documentName = Path.GetFileNameWithoutExtension(documentPath); + var outputSpectralReportName = $"spectral-{documentName}.html"; + await this.GenerateSpectralReport(spectralExecutePath, documentPath, rulesetUrl, Path.Combine(reportsPath, outputSpectralReportName), cancellationToken); + } } private static string GetSpectralFileName() @@ -86,4 +111,29 @@ private static string GetArchitecture() throw new OpenApiTaskFailedException("Unknown processor architecture encountered"); } + + private async Task GenerateSpectralReport(string spectralExecutePath, string swaggerDocumentPath, string rulesetUrl, string htmlReportPath, CancellationToken cancellationToken) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + await this.AssignExecutePermission(spectralExecutePath, cancellationToken); + } + + var exitCode = await this._processWrapper.RunProcessAsync(spectralExecutePath, new[] { "lint", swaggerDocumentPath, "--ruleset", rulesetUrl, "--format", "html", "--output.html", htmlReportPath }, cancellationToken); + if (exitCode != 0) + { + throw new OpenApiTaskFailedException($"Spectral report for {swaggerDocumentPath} could not be created."); + } + + this._loggerWrapper.LogMessage("Spectral report generated. {0}", htmlReportPath); + } + + private async Task AssignExecutePermission(string spectralExecutePath, CancellationToken cancellationToken) + { + var chmodExitCode = await this._processWrapper.RunProcessAsync("chmod", new[] { "+x", spectralExecutePath }, cancellationToken); + if (chmodExitCode != 0) + { + throw new OpenApiTaskFailedException($"Failed to provide execute permission to {spectralExecutePath}"); + } + } } \ No newline at end of file diff --git a/src/Workleap.OpenApi.MSBuild/SwaggerManager.cs b/src/Workleap.OpenApi.MSBuild/SwaggerManager.cs index 24edf62..8fba982 100644 --- a/src/Workleap.OpenApi.MSBuild/SwaggerManager.cs +++ b/src/Workleap.OpenApi.MSBuild/SwaggerManager.cs @@ -9,27 +9,32 @@ internal sealed class SwaggerManager : ISwaggerManager private readonly ILoggerWrapper _loggerWrapper; private readonly string _openApiWebApiAssemblyPath; private readonly string _swaggerDirectory; + private readonly string _openApiToolsDirectoryPath; public SwaggerManager(IProcessWrapper processWrapper, ILoggerWrapper loggerWrapper, string openApiToolsDirectoryPath, string openApiWebApiAssemblyPath) { this._processWrapper = processWrapper; this._loggerWrapper = loggerWrapper; this._openApiWebApiAssemblyPath = openApiWebApiAssemblyPath; + this._openApiToolsDirectoryPath = openApiToolsDirectoryPath; this._swaggerDirectory = Path.Combine(openApiToolsDirectoryPath, "swagger", SwaggerVersion); } - public async Task RunSwaggerAsync(string[] openApiSwaggerDocumentNames, CancellationToken cancellationToken) + public async Task> RunSwaggerAsync(string[] openApiSwaggerDocumentNames, CancellationToken cancellationToken) { var swaggerExePath = Path.Combine(this._swaggerDirectory, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "swagger.exe" : "swagger"); + var taskList = new List>(); foreach (var documentName in openApiSwaggerDocumentNames) { var outputOpenApiSpecName = $"openapi-{documentName.ToLowerInvariant()}.yaml"; - var outputOpenApiSpecPath = Path.Combine(this._swaggerDirectory, outputOpenApiSpecName); + var outputOpenApiSpecPath = Path.Combine(this._openApiToolsDirectoryPath, outputOpenApiSpecName); - await this.GenerateOpenApiSpecAsync(swaggerExePath, outputOpenApiSpecPath, documentName, cancellationToken); + taskList.Add(this.GenerateOpenApiSpecAsync(swaggerExePath, outputOpenApiSpecPath, documentName, cancellationToken)); } + + return await Task.WhenAll(taskList); } public async Task InstallSwaggerCliAsync(CancellationToken cancellationToken) @@ -55,13 +60,15 @@ public async Task InstallSwaggerCliAsync(CancellationToken cancellationToken) } } - public async Task GenerateOpenApiSpecAsync(string swaggerExePath, string outputOpenApiSpecPath, string documentName, CancellationToken cancellationToken) + 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."); + throw new OpenApiTaskFailedException($"OpenApi file {outputOpenApiSpecPath} could not be created."); } + + return outputOpenApiSpecPath; } } \ No newline at end of file diff --git a/src/Workleap.OpenApi.MSBuild/ValidateOpenApiTask.cs b/src/Workleap.OpenApi.MSBuild/ValidateOpenApiTask.cs index bb96563..56d8f69 100644 --- a/src/Workleap.OpenApi.MSBuild/ValidateOpenApiTask.cs +++ b/src/Workleap.OpenApi.MSBuild/ValidateOpenApiTask.cs @@ -1,4 +1,4 @@ -using Microsoft.Build.Framework; +using Microsoft.Build.Framework; namespace Workleap.OpenApi.MSBuild; @@ -28,10 +28,10 @@ protected override async Task ExecuteAsync(CancellationToken cancellationT { var loggerWrapper = new LoggerWrapper(this.Log); var processWrapper = new ProcessWrapper(this.OpenApiToolsDirectoryPath); - var swaggerManager = new SwaggerManager(processWrapper, loggerWrapper, this.OpenApiToolsDirectoryPath, this.OpenApiToolsDirectoryPath); + var swaggerManager = new SwaggerManager(processWrapper, loggerWrapper, this.OpenApiToolsDirectoryPath, this.OpenApiWebApiAssemblyPath); using var httpClientWrapper = new HttpClientWrapper(); - var spectralManager = new SpectralManager(loggerWrapper, this.OpenApiToolsDirectoryPath, httpClientWrapper); + var spectralManager = new SpectralManager(loggerWrapper, this.OpenApiToolsDirectoryPath, httpClientWrapper, processWrapper); this.Log.LogMessage(MessageImportance.Low, "{0} = '{1}'", nameof(this.OpenApiWebApiAssemblyPath), this.OpenApiWebApiAssemblyPath); this.Log.LogMessage(MessageImportance.Low, "{0} = '{1}'", nameof(this.OpenApiToolsDirectoryPath), this.OpenApiToolsDirectoryPath); @@ -53,11 +53,12 @@ protected override async Task ExecuteAsync(CancellationToken cancellationT await swaggerManager.InstallSwaggerCliAsync(cancellationToken); await spectralManager.InstallSpectralAsync(cancellationToken); - // await swaggerManager.RunSwaggerAsync(this.OpenApiSwaggerDocumentNames, cancellationToken); + var swaggerDocPaths = await swaggerManager.RunSwaggerAsync(this.OpenApiSwaggerDocumentNames, cancellationToken); + await spectralManager.RunSpectralAsync(swaggerDocPaths, this.OpenApiSpectralRulesetUrl, cancellationToken); } catch (OpenApiTaskFailedException e) { - this.Log.LogWarning("An error occured while validating the OpenAPI specification: {0}", e.Message); + this.Log.LogWarning("An error occurred while validating the OpenAPI specification: {0}", e.Message); } return true;