diff --git a/THIRD_PARTY_NOTICES.txt b/THIRD_PARTY_NOTICES.txt index 38c85b8..bc21946 100644 --- a/THIRD_PARTY_NOTICES.txt +++ b/THIRD_PARTY_NOTICES.txt @@ -2,8 +2,8 @@ THIRD-PARTY SOFTWARE NOTICES AND INFORMATION This product incorporates components from the projects listed below. The original copyright notices and the licenses under which Dyalog Ltd. received such components -are set forth below. Dyalog Ltd. licenses these components to you under the terms -described in this file; Dyalog Ltd. reserves all other rights not expressly granted. +are set forth below. These components are licensed to you under their respective +licenses as set below. Dyalog Ltd. reserves all other rights not expressly granted. ======================================================================= diff --git a/mkdocs.yml b/mkdocs.yml index b735f8f..975a3b2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -22,6 +22,10 @@ extra_javascript: extra: generator: false + version_maj: 20 + version_min: 0 + version_majmin: 20.0 + version_condensed: 200 nav: - Home: index.md diff --git a/src/OpenAPIDyalog/Constants/GeneratorConstants.cs b/src/OpenAPIDyalog/Constants/GeneratorConstants.cs index 316372b..35c1f2b 100644 --- a/src/OpenAPIDyalog/Constants/GeneratorConstants.cs +++ b/src/OpenAPIDyalog/Constants/GeneratorConstants.cs @@ -17,7 +17,8 @@ public static class GeneratorConstants public const string VersionTemplate = "APLSource/Version.aplf.scriban"; public const string ReadmeTemplate = "README.md.scriban"; public const string ModelTemplate = "APLSource/models/model.aplc.scriban"; - public const string HttpCommandResource = "APLSource/HttpCommand.aplc"; + public const string HttpCommandResource = "APLSource/HttpCommand.aplc"; + public const string ThirdPartyNoticesResource = "OpenAPIDyalog.THIRD_PARTY_NOTICES.txt"; // ── Content types ───────────────────────────────────────────────────────── public const string ContentTypeJson = "application/json"; diff --git a/src/OpenAPIDyalog/GeneratorApplication.cs b/src/OpenAPIDyalog/GeneratorApplication.cs index 26b6a40..19def38 100644 --- a/src/OpenAPIDyalog/GeneratorApplication.cs +++ b/src/OpenAPIDyalog/GeneratorApplication.cs @@ -1,3 +1,4 @@ +using System.Reflection; using Microsoft.Extensions.Logging; using OpenAPIDyalog.Constants; using OpenAPIDyalog.Models; @@ -12,6 +13,18 @@ public static class GeneratorApplication { public static async Task RunAsync(string[] args) { + if (args.Contains("--help") || args.Contains("-h")) + { + DisplayUsage(); + return 0; + } + + if (args.Contains("--third-party-notices")) + { + DisplayThirdPartyNotices(); + return 0; + } + if (args.Length == 0) { DisplayUsage(); @@ -147,6 +160,15 @@ public static async Task RunAsync(string[] args) }; } + private static void DisplayThirdPartyNotices() + { + using var stream = Assembly.GetExecutingAssembly() + .GetManifestResourceStream(GeneratorConstants.ThirdPartyNoticesResource) + ?? throw new InvalidOperationException("Third-party notices resource not found. This is a build defect."); + using var reader = new StreamReader(stream); + Console.Write(reader.ReadToEnd()); + } + // DisplayUsage is UI output, not a logging concern — Console.WriteLine is intentional. private static void DisplayUsage() { @@ -159,7 +181,9 @@ private static void DisplayUsage() Console.WriteLine(" [output-directory] Directory for generated files (default: ./generated)"); Console.WriteLine(); Console.WriteLine("Options:"); - Console.WriteLine(" --no-validation, -nv Disable OpenAPI specification validation rules"); + Console.WriteLine(" --help, -h Show this help message and exit"); + Console.WriteLine(" --no-validation, -nv Disable OpenAPI specification validation rules"); + Console.WriteLine(" --third-party-notices Print third-party software notices and exit"); Console.WriteLine(); Console.WriteLine("Examples:"); Console.WriteLine(" OpenAPIDyalog openapispec.json"); diff --git a/src/OpenAPIDyalog/Models/TemplateContext.cs b/src/OpenAPIDyalog/Models/TemplateContext.cs index 0dc5781..0262231 100644 --- a/src/OpenAPIDyalog/Models/TemplateContext.cs +++ b/src/OpenAPIDyalog/Models/TemplateContext.cs @@ -1,4 +1,5 @@ using Microsoft.OpenApi; +using OpenAPIDyalog.Constants; namespace OpenAPIDyalog.Models; @@ -117,15 +118,24 @@ public IEnumerable GetAllTags() { if (Paths == null) return Enumerable.Empty(); - return Paths.Values + var operations = Paths.Values .Where(path => path.Operations != null) .SelectMany(path => path.Operations!.Values) - .Where(op => op.Tags != null) + .ToList(); + + var explicitTags = operations + .Where(op => op.Tags is { Count: > 0 }) .SelectMany(op => op.Tags!) .Select(tag => tag.Name) .Where(name => name != null) .Cast() .Distinct(); + + var hasTaglessOperations = operations.Any(op => op.Tags == null || op.Tags.Count == 0); + if (hasTaglessOperations) + return explicitTags.Append(GeneratorConstants.DefaultTagName).Distinct(); + + return explicitTags; } /// diff --git a/src/OpenAPIDyalog/OpenAPIDyalog.csproj b/src/OpenAPIDyalog/OpenAPIDyalog.csproj index 9b96e44..4df6983 100644 --- a/src/OpenAPIDyalog/OpenAPIDyalog.csproj +++ b/src/OpenAPIDyalog/OpenAPIDyalog.csproj @@ -23,6 +23,7 @@ + diff --git a/src/OpenAPIDyalog/Services/ArtifactGeneratorService.cs b/src/OpenAPIDyalog/Services/ArtifactGeneratorService.cs index 1c92fd9..aff8278 100644 --- a/src/OpenAPIDyalog/Services/ArtifactGeneratorService.cs +++ b/src/OpenAPIDyalog/Services/ArtifactGeneratorService.cs @@ -51,22 +51,36 @@ public async Task GenerateVersionAsync(OpenApiDocument document, string outputDi } /// - /// Copies the embedded HttpCommand.aplc binary resource to the output directory. + /// Copies the embedded HttpCommand.aplc binary resource to the output directory, + /// skipping the write if the existing file is already identical. /// public async Task CopyHttpCommandAsync(string outputDirectory) { var destPath = Path.Combine(outputDirectory, GeneratorConstants.AplSourceDir, "HttpCommand.aplc"); Directory.CreateDirectory(Path.GetDirectoryName(destPath)!); - using var srcStream = _templateService.GetEmbeddedResourceStream(GeneratorConstants.HttpCommandResource); - using var destStream = File.Create(destPath); - await srcStream.CopyToAsync(destStream); + using var srcStream = _templateService.GetEmbeddedResourceStream(GeneratorConstants.HttpCommandResource); + using var memStream = new MemoryStream(); + await srcStream.CopyToAsync(memStream); + var srcBytes = memStream.ToArray(); + if (File.Exists(destPath)) + { + var destBytes = await File.ReadAllBytesAsync(destPath); + if (srcBytes.SequenceEqual(destBytes)) + { + _logger.LogInformation("Unchanged: {AplSourceDir}/HttpCommand.aplc", GeneratorConstants.AplSourceDir); + return; + } + } + + await File.WriteAllBytesAsync(destPath, srcBytes); _logger.LogInformation("Copied: {AplSourceDir}/HttpCommand.aplc", GeneratorConstants.AplSourceDir); } /// - /// Copies the OpenAPI specification file to the output directory. + /// Copies the OpenAPI specification file to the output directory, + /// skipping the write if the existing file is already identical. /// /// Re-thrown after logging if the copy fails. public async Task CopySpecificationAsync(string sourcePath, string outputDirectory) @@ -76,9 +90,20 @@ public async Task CopySpecificationAsync(string sourcePath, string outputDirecto try { - File.Copy(sourcePath, destPath, overwrite: true); + var srcBytes = await File.ReadAllBytesAsync(sourcePath); + + if (File.Exists(destPath)) + { + var destBytes = await File.ReadAllBytesAsync(destPath); + if (srcBytes.SequenceEqual(destBytes)) + { + _logger.LogInformation("Unchanged: {FileName}", fileName); + return; + } + } + + await File.WriteAllBytesAsync(destPath, srcBytes); _logger.LogInformation("Copied: {FileName}", fileName); - await Task.CompletedTask; // async for consistent caller pattern } catch (Exception ex) { diff --git a/src/OpenAPIDyalog/Services/TemplateService.cs b/src/OpenAPIDyalog/Services/TemplateService.cs index 39b2d40..99b5d26 100644 --- a/src/OpenAPIDyalog/Services/TemplateService.cs +++ b/src/OpenAPIDyalog/Services/TemplateService.cs @@ -71,7 +71,7 @@ public string Render(Template template, object context) try { var scriptObject = BuildScriptObject(context); - var templateContext = new TemplateContext(); + var templateContext = new TemplateContext { LoopLimit = int.MaxValue }; templateContext.PushGlobal(scriptObject); return template.Render(templateContext); } @@ -89,7 +89,7 @@ public async Task RenderAsync(Template template, object context) try { var scriptObject = BuildScriptObject(context); - var templateContext = new TemplateContext(); + var templateContext = new TemplateContext { LoopLimit = int.MaxValue }; templateContext.PushGlobal(scriptObject); return await template.RenderAsync(templateContext); } @@ -109,7 +109,7 @@ public async Task LoadAndRenderAsync(string templateName, object context } /// - /// Saves rendered output to a file. + /// Saves rendered output to a file, skipping the write if the existing content is identical. /// public async Task SaveOutputAsync(string output, string outputPath) { @@ -119,6 +119,9 @@ public async Task SaveOutputAsync(string output, string outputPath) Directory.CreateDirectory(directory); } + if (File.Exists(outputPath) && await File.ReadAllTextAsync(outputPath) == output) + return; + await File.WriteAllTextAsync(outputPath, output); } diff --git a/src/OpenAPIDyalog/Templates/APLSource/Client.aplc.scriban b/src/OpenAPIDyalog/Templates/APLSource/Client.aplc.scriban index 0546dfc..89bf21c 100644 --- a/src/OpenAPIDyalog/Templates/APLSource/Client.aplc.scriban +++ b/src/OpenAPIDyalog/Templates/APLSource/Client.aplc.scriban @@ -1,5 +1,4 @@ :Class {{ class_name ?? "Client" }} - ⍝ Generated on {{ generated_at | date.to_string "%Y-%m-%d %H:%M:%S" }} UTC ⍝ {{ title }} - Version {{ version }} {{~ if description ~}} {{ comment_lines description }} diff --git a/src/OpenAPIDyalog/Templates/APLSource/utils.apln.scriban b/src/OpenAPIDyalog/Templates/APLSource/utils.apln.scriban index f28abcc..b4cb38a 100644 --- a/src/OpenAPIDyalog/Templates/APLSource/utils.apln.scriban +++ b/src/OpenAPIDyalog/Templates/APLSource/utils.apln.scriban @@ -1,5 +1,4 @@ :Namespace utils - ⍝ Generated on {{ generated_at | date.to_string "%Y-%m-%d %H:%M:%S" }} UTC ⍝ {{ title }} - Version {{ version }} ⍝ Utility functions for HTTP requests and parameter validation @@ -98,12 +97,12 @@ ∇ - isValidPathParam←{ + isValidPathParam←{ ⍝ True if argument is a character vector or a scalar number - isChar←(0=10|⎕DR)⍵ + isChar←(1=≢⍴1∘/⍵)∧(0=10|⎕DR)⍵ isScalarNum←(0=≢⍴⍵)∧2|⎕DR ⍵ - isChar ∨ isScalarNum - } + isChar∨isScalarNum + } base64←{⎕IO ⎕ML←0 1 ⍝ Base64 encoding and decoding as used in MIME.