Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions THIRD_PARTY_NOTICES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There’s trailing whitespace at the end of this line (after “respective”). It’s best to remove it to avoid noisy diffs and keep the notices file clean.

Suggested change
are set forth below. These components are licensed to you under their respective
are set forth below. These components are licensed to you under their respective

Copilot uses AI. Check for mistakes.
licenses as set below. Dyalog Ltd. reserves all other rights not expressly granted.

=======================================================================

Expand Down
4 changes: 4 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/OpenAPIDyalog/Constants/GeneratorConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
26 changes: 25 additions & 1 deletion src/OpenAPIDyalog/GeneratorApplication.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Reflection;
using Microsoft.Extensions.Logging;
using OpenAPIDyalog.Constants;
using OpenAPIDyalog.Models;
Expand All @@ -12,6 +13,18 @@ public static class GeneratorApplication
{
public static async Task<int> 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();
Expand Down Expand Up @@ -147,6 +160,15 @@ public static async Task<int> 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()
{
Expand All @@ -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");
Expand Down
14 changes: 12 additions & 2 deletions src/OpenAPIDyalog/Models/TemplateContext.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.OpenApi;
using OpenAPIDyalog.Constants;

namespace OpenAPIDyalog.Models;

Expand Down Expand Up @@ -117,15 +118,24 @@ public IEnumerable<string> GetAllTags()
{
if (Paths == null) return Enumerable.Empty<string>();

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<string>()
.Distinct();

var hasTaglessOperations = operations.Any(op => op.Tags == null || op.Tags.Count == 0);
if (hasTaglessOperations)
return explicitTags.Append(GeneratorConstants.DefaultTagName).Distinct();

Comment on lines +134 to +137
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetAllTags() now uses GeneratorConstants.DefaultTagName for tagless operations, but GetOperationsByTag() in the same class still hard-codes "default". To keep behavior consistent if the default tag name ever changes, use the constant everywhere the default tag is produced/consumed.

Copilot uses AI. Check for mistakes.
return explicitTags;
}

/// <summary>
Expand Down
1 change: 1 addition & 0 deletions src/OpenAPIDyalog/OpenAPIDyalog.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<!-- Embed all templates into the assembly for single-file distribution -->
<ItemGroup>
<EmbeddedResource Include="Templates\**\*" />
<EmbeddedResource Include="..\..\THIRD_PARTY_NOTICES.txt" LogicalName="OpenAPIDyalog.THIRD_PARTY_NOTICES.txt" />
</ItemGroup>

</Project>
39 changes: 32 additions & 7 deletions src/OpenAPIDyalog/Services/ArtifactGeneratorService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,22 +51,36 @@ public async Task GenerateVersionAsync(OpenApiDocument document, string outputDi
}

/// <summary>
/// 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.
/// </summary>
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);
}

/// <summary>
/// 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.
/// </summary>
/// <exception cref="IOException">Re-thrown after logging if the copy fails.</exception>
public async Task CopySpecificationAsync(string sourcePath, string outputDirectory)
Expand All @@ -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))
{
Comment on lines +93 to +99
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CopySpecificationAsync now reads the entire spec file into memory (and potentially the destination too) just to check for equality. For large OpenAPI specs this can be a noticeable memory spike; consider comparing file lengths first and/or doing a streaming/hash-based comparison to avoid loading both files at once.

Copilot uses AI. Check for mistakes.
_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)
{
Expand Down
9 changes: 6 additions & 3 deletions src/OpenAPIDyalog/Services/TemplateService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment on lines 73 to 76
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting Scriban TemplateContext.LoopLimit to int.MaxValue disables Scriban’s safeguard against runaway loops. Consider using a high but bounded limit (or making it configurable) so template bugs or unexpectedly large inputs can’t hang the generator indefinitely.

Copilot uses AI. Check for mistakes.
}
Expand All @@ -89,7 +89,7 @@ public async Task<string> 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);
Comment on lines 91 to 94
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same concern as Render(): using LoopLimit = int.MaxValue removes Scriban’s loop protection for async rendering as well. Recommend a bounded/configurable limit to avoid unbounded CPU time if a template loops unexpectedly.

Copilot uses AI. Check for mistakes.
}
Expand All @@ -109,7 +109,7 @@ public async Task<string> LoadAndRenderAsync(string templateName, object context
}

/// <summary>
/// Saves rendered output to a file.
/// Saves rendered output to a file, skipping the write if the existing content is identical.
/// </summary>
public async Task SaveOutputAsync(string output, string outputPath)
{
Expand All @@ -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);
}

Expand Down
1 change: 0 additions & 1 deletion src/OpenAPIDyalog/Templates/APLSource/Client.aplc.scriban
Original file line number Diff line number Diff line change
@@ -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 }}
Expand Down
9 changes: 4 additions & 5 deletions src/OpenAPIDyalog/Templates/APLSource/utils.apln.scriban
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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 ⍵
isCharisScalarNum
}
isCharisScalarNum
}
Comment on lines +100 to +105
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isValidPathParam block is indented differently from the surrounding definitions in this namespace (most are aligned at 4 spaces). Aligning indentation here would keep the generated APL source consistently formatted.

Copilot uses AI. Check for mistakes.

base64←{⎕IO ⎕ML←0 1 ⍝ Base64 encoding and decoding as used in MIME.

Expand Down