Skip to content

Commit

Permalink
Support skipping output generation based on API specification checksum (
Browse files Browse the repository at this point in the history
  • Loading branch information
lahma authored Apr 14, 2021
1 parent 74cacba commit d690850
Show file tree
Hide file tree
Showing 14 changed files with 169 additions and 37 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,5 @@ src/packages/**
/src/NSwag.Console/Properties/launchSettings.json

# Ignore files from JetBrainds Rider
/src/.idea/
/src/.idea/
_ReSharper.Caches/
4 changes: 2 additions & 2 deletions src/NSwag.AspNetCore/OpenApiDocumentProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public OpenApiDocumentProvider(IServiceProvider serviceProvider, IEnumerable<Ope
_documents = documents ?? throw new ArgumentNullException(nameof(documents));
}

public async Task<OpenApiDocument> GenerateAsync(string documentName)
public Task<OpenApiDocument> GenerateAsync(string documentName)
{
if (documentName == null)
{
Expand All @@ -53,7 +53,7 @@ public async Task<OpenApiDocument> GenerateAsync(string documentName)
$"Add with the AddSwagger()/AddOpenApi() methods in ConfigureServices().");
}

return await document.Generator.GenerateAsync(_serviceProvider);
return document.Generator.GenerateAsync(_serviceProvider);
}

// Called by the <c>dotnet-getdocument</c> tool from the Microsoft.Extensions.ApiDescription.Server package.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,14 @@ public CSharpFileTemplateModel(
_clientCode = clientTypes.Concatenate();

Classes = dtoTypes.Concatenate();
SourceSha = _settings.ChecksumCacheEnabled ? _document.GetChecksum() : "";
}

/// <summary>
/// Gets the checksum for the document that was used to produce the file.
/// </summary>
public string SourceSha { get; }

/// <summary>Gets the namespace.</summary>
public string Namespace => _settings.CSharpGeneratorSettings.Namespace ?? string.Empty;

Expand Down
27 changes: 15 additions & 12 deletions src/NSwag.CodeGeneration.CSharp/Templates/File.liquid
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
//----------------------
// <auto-generated>
// Generated using the NSwag toolchain v{{ ToolchainVersion }} (http://NSwag.org)
{% if SourceSha != "" -%}
// SourceSHA: {{ SourceSha }}
{% endif -%}
// </auto-generated>
//----------------------

Expand All @@ -22,7 +25,7 @@ using {{ usage }};
namespace {{ Namespace }}
{
using System = global::System;

{{ Clients | tab }}

{% if GenerateContracts -%}
Expand Down Expand Up @@ -100,14 +103,14 @@ namespace {{ Namespace }}
public FileResponse(int statusCode, System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> headers, System.IO.Stream stream, System.IDisposable client, System.IDisposable response)
{% endif -%}
{
StatusCode = statusCode;
Headers = headers;
Stream = stream;
_client = client;
StatusCode = statusCode;
Headers = headers;
Stream = stream;
_client = client;
_response = response;
}

public void Dispose()
public void Dispose()
{
Stream.Dispose();
if (_response != null)
Expand All @@ -126,10 +129,10 @@ namespace {{ Namespace }}
public int StatusCode { get; private set; }

public System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> Headers { get; private set; }
public {{ responseClassName }}(int statusCode, System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> headers)

public {{ responseClassName }}(int statusCode, System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> headers)
{
StatusCode = statusCode;
StatusCode = statusCode;
Headers = headers;
}
}
Expand All @@ -138,8 +141,8 @@ namespace {{ Namespace }}
public partial class {{ responseClassName }}<TResult> : {{ responseClassName }}
{
public TResult Result { get; private set; }
public {{ responseClassName }}(int statusCode, System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> headers, TResult result)

public {{ responseClassName }}(int statusCode, System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> headers, TResult result)
: base(statusCode, headers)
{
Result = result;
Expand Down Expand Up @@ -171,7 +174,7 @@ namespace {{ Namespace }}
: base(message + "\n\nStatus: " + statusCode + "\nResponse: \n" + ((response == null) ? "(null)" : response.Substring(0, response.Length >= 512 ? 512 : response.Length)), innerException)
{
StatusCode = statusCode;
Response = response;
Response = response;
Headers = headers;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,14 @@ public TypeScriptFileTemplateModel(
Types = dtoTypes.OrderByBaseDependency().Concatenate();
ExtensionCodeBottom = GenerateExtensionCodeAfter();
Framework = new TypeScriptFrameworkModel(settings);
SourceSha = _settings.ChecksumCacheEnabled ? _document.GetChecksum() : "";
}

/// <summary>
/// Gets the checksum for the document that was used to produce the file.
/// </summary>
public string SourceSha { get; }

/// <summary>Gets framework specific information.</summary>
public TypeScriptFrameworkModel Framework { get; set; }

Expand Down Expand Up @@ -127,8 +133,8 @@ public IEnumerable<string> ResponseClassNames
public bool RequiresFileParameterInterface =>
!_settings.TypeScriptGeneratorSettings.ExcludedTypeNames.Contains("FileParameter") &&
(_document.Operations.Any(o => o.Operation.ActualParameters.Any(p => p.ActualTypeSchema.IsBinary)) ||
_document.Operations.Any(o => o.Operation?.RequestBody?.Content?.Any(c => c.Value.Schema?.IsBinary == true ||
c.Value.Schema?.ActualProperties.Any(p => p.Value.IsBinary ||
_document.Operations.Any(o => o.Operation?.RequestBody?.Content?.Any(c => c.Value.Schema?.IsBinary == true ||
c.Value.Schema?.ActualProperties.Any(p => p.Value.IsBinary ||
p.Value.Item?.IsBinary == true ||
p.Value.Items.Any(i => i.IsBinary)
) == true) == true));
Expand Down
3 changes: 3 additions & 0 deletions src/NSwag.CodeGeneration.TypeScript/Templates/File.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
//----------------------
// <auto-generated>
// Generated using the NSwag toolchain v{{ ToolchainVersion }} (http://NSwag.org)
{% if SourceSha != "" -%}
// SourceSHA: {{ SourceSha }}
{% endif -%}
// </auto-generated>
//----------------------
// ReSharper disable InconsistentNaming
Expand Down
70 changes: 70 additions & 0 deletions src/NSwag.CodeGeneration/ClientGeneratorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//-----------------------------------------------------------------------

using System.Collections.Generic;
using System.IO;
using System.Linq;
using NJsonSchema;
using NJsonSchema.CodeGeneration;
Expand Down Expand Up @@ -70,6 +71,12 @@ public string GenerateFile()
/// <returns>The code</returns>
public string GenerateFile(ClientGeneratorOutputType outputType)
{
if (!OutputRequiresRefresh(BaseSettings.OutputFilePath))
{
// just give back the original file as it hasn't changed
return File.ReadAllText(BaseSettings.OutputFilePath);
}

var clientTypes = GenerateAllClientTypes();

var dtoTypes = BaseSettings.GenerateDtoTypes ?
Expand All @@ -92,6 +99,69 @@ public string GenerateFile(ClientGeneratorOutputType outputType)
.Replace("\n\n\n", "\n\n");
}

private bool OutputRequiresRefresh(string outputFilePath)
{
if (!BaseSettings.ChecksumCacheEnabled || string.IsNullOrWhiteSpace(outputFilePath) || !File.Exists(outputFilePath))
{
// no point in checking
return true;
}

var sourceDocumentChecksum = _document.GetChecksum();
if (string.IsNullOrWhiteSpace(sourceDocumentChecksum))
{
return true;
}

// if we can match both source checksum and toolchain, we can presume file hasn't been changed
var checksumMatches = false;
var toolchainVersionMatches = false;
const string checksumMarker = "SourceSHA: ";
const string toolchainVersionMarker = "toolchain v";

try
{
using (var reader = new StreamReader(new FileStream(outputFilePath, FileMode.Open, FileAccess.Read, FileShare.Read)))
{
// read multiple lines as we can have headers before version info
var counter = 10;
string line;
while ((line = reader.ReadLine()) != null && counter > 0)
{
counter--;
var checksumStartIndex = line.IndexOf(checksumMarker);
if (checksumStartIndex > -1)
{
var startIndex = checksumStartIndex + checksumMarker.Length;
var fileSourceSha = line.Substring(startIndex).Trim();
checksumMatches = fileSourceSha == sourceDocumentChecksum;
}

var toolchainStartIndex = line.IndexOf(toolchainVersionMarker);
if (toolchainStartIndex > -1)
{
var startIndex = toolchainStartIndex + toolchainVersionMarker.Length;
var length = line.IndexOf(" ", startIndex) - startIndex;
var fileToolchainVersion = line.Substring(startIndex, length).Trim();
toolchainVersionMatches = fileToolchainVersion == OpenApiDocument.ToolchainVersion;
}

if (checksumMatches && toolchainVersionMatches)
{
// we are good to go with cached version, no need to refresh
return false;
}
}
}
}
catch
{
// no-go
}

return true;
}

/// <summary>Generates the file.</summary>
/// <param name="clientTypes">The client types.</param>
/// <param name="dtoTypes">The DTO types.</param>
Expand Down
6 changes: 6 additions & 0 deletions src/NSwag.CodeGeneration/ClientGeneratorBaseSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,11 @@ public string GenerateControllerName(string controllerName)

/// <summary>Gets or sets the name of the response class (supports the '{controller}' placeholder).</summary>
public string ResponseClass { get; set; }

/// <summary>Gets or sets the output file name, used to detect changes.</summary>
public string OutputFilePath { get; set; }

/// <summary>Gets or sets whether checksum based output caching can be used.
public bool ChecksumCacheEnabled { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ public string TemplateDirectory
[Argument(Name = "EnumNameGeneratorType", IsRequired = false, Description = "The custom IEnumNameGenerator implementation type in the form 'assemblyName:fullTypeName' or 'fullTypeName').")]
public string EnumNameGeneratorType { get; set; }

[Argument(Name = nameof(ChecksumCacheEnabled), IsRequired = false, Description = "Whether output generation can be skipped if source document checksum matches existing output (default: false).")]
public bool ChecksumCacheEnabled
{
get { return Settings.ChecksumCacheEnabled; }
set { Settings.ChecksumCacheEnabled = value; }
}

// TODO: Use InitializeCustomTypes method
public void InitializeCustomTypes(AssemblyLoader.AssemblyLoader assemblyLoader)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,9 +254,9 @@ public override async Task<object> RunAsync(CommandLineProcessor processor, ICon
return result;
}

public async Task<Dictionary<string, string>> RunAsync()
public Task<Dictionary<string, string>> RunAsync()
{
return await Task.Run(async () =>
return Task.Run(async () =>
{
var document = await GetInputSwaggerDocument().ConfigureAwait(false);
var clientGenerator = new CSharpClientGenerator(document, Settings);
Expand All @@ -270,6 +270,8 @@ public async Task<Dictionary<string, string>> RunAsync()
}
else
{
// when generating single file allow caching
Settings.OutputFilePath = OutputFilePath;
return new Dictionary<string, string>
{
{ OutputFilePath ?? "Full", clientGenerator.GenerateFile(ClientGeneratorOutputType.Full) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ public TypeScriptEnumStyle EnumStyle
get { return Settings.TypeScriptGeneratorSettings.EnumStyle; }
set { Settings.TypeScriptGeneratorSettings.EnumStyle = value; }
}

[Argument(Name = "UseLeafType", IsRequired = false, Description = "Generate leaf types for an object with discriminator (default: false).")]
public bool UseLeafType
{
Expand Down Expand Up @@ -394,16 +394,17 @@ public override async Task<object> RunAsync(CommandLineProcessor processor, ICon
return code;
}

public async Task<string> RunAsync()
public Task<string> RunAsync()
{
return await Task.Run(async () =>
return Task.Run(async () =>
{
var additionalCode = ExtensionCode ?? string.Empty;
if (DynamicApis.FileExists(additionalCode))
{
additionalCode = DynamicApis.FileReadAllText(additionalCode);
}

Settings.OutputFilePath = OutputFilePath;
Settings.TypeScriptGeneratorSettings.ExtensionCode = additionalCode;

var document = await GetInputSwaggerDocument().ConfigureAwait(false);
Expand Down
14 changes: 7 additions & 7 deletions src/NSwag.Commands/Commands/OutputCommandBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public abstract class OutputCommandBase : IOutputCommand

public abstract Task<object> RunAsync(CommandLineProcessor processor, IConsoleHost host);

protected async Task<OpenApiDocument> ReadSwaggerDocumentAsync(string input)
protected Task<OpenApiDocument> ReadSwaggerDocumentAsync(string input)
{
if (!IsJson(input) && !IsYaml(input))
{
Expand All @@ -34,35 +34,35 @@ protected async Task<OpenApiDocument> ReadSwaggerDocumentAsync(string input)
if (input.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase) ||
input.EndsWith(".yml", StringComparison.OrdinalIgnoreCase))
{
return await OpenApiYamlDocument.FromUrlAsync(input).ConfigureAwait(false);
return OpenApiYamlDocument.FromUrlAsync(input);
}
else
{
return await OpenApiDocument.FromUrlAsync(input).ConfigureAwait(false);
return OpenApiDocument.FromUrlAsync(input);
}
}
else
{
if (input.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase) ||
input.EndsWith(".yml", StringComparison.OrdinalIgnoreCase))
{
return await OpenApiYamlDocument.FromFileAsync(input).ConfigureAwait(false);
return OpenApiYamlDocument.FromFileAsync(input);
}
else
{
return await OpenApiDocument.FromFileAsync(input).ConfigureAwait(false);
return OpenApiDocument.FromFileAsync(input);
}
}
}
else
{
if (IsYaml(input))
{
return await OpenApiYamlDocument.FromYamlAsync(input).ConfigureAwait(false);
return OpenApiYamlDocument.FromYamlAsync(input);
}
else
{
return await OpenApiDocument.FromJsonAsync(input).ConfigureAwait(false);
return OpenApiDocument.FromJsonAsync(input);
}
}
}
Expand Down
Loading

0 comments on commit d690850

Please sign in to comment.