diff --git a/generated/anthropic.zig b/generated/anthropic.zig index 53656302..15c6f860 100644 --- a/generated/anthropic.zig +++ b/generated/anthropic.zig @@ -1,5 +1,6 @@ // -// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:35 UTC +// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:20 UTC +// Checksum: 1bd4817843cf8ccc // Changes to this file may cause incorrect behavior and will be lost if the code is regenerated // diff --git a/generated/generated_v2.zig b/generated/generated_v2.zig index 68ee7578..8b8ce4ab 100644 --- a/generated/generated_v2.zig +++ b/generated/generated_v2.zig @@ -1,5 +1,6 @@ // -// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:33 UTC +// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:20 UTC +// Checksum: 3ac4f500c0e3330b // Changes to this file may cause incorrect behavior and will be lost if the code is regenerated // diff --git a/generated/generated_v2_yaml.zig b/generated/generated_v2_yaml.zig index 20ce93eb..2d50a366 100644 --- a/generated/generated_v2_yaml.zig +++ b/generated/generated_v2_yaml.zig @@ -1,5 +1,6 @@ // -// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:34 UTC +// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:18 UTC +// Checksum: c910be96dc8a9e71 // Changes to this file may cause incorrect behavior and will be lost if the code is regenerated // diff --git a/generated/generated_v3.zig b/generated/generated_v3.zig index a8225ccb..7dc93be1 100644 --- a/generated/generated_v3.zig +++ b/generated/generated_v3.zig @@ -1,5 +1,6 @@ // -// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:35 UTC +// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:20 UTC +// Checksum: 72ff812aa1dcc8ae // Changes to this file may cause incorrect behavior and will be lost if the code is regenerated // diff --git a/generated/generated_v31.zig b/generated/generated_v31.zig index a1ecdfec..fed22a68 100644 --- a/generated/generated_v31.zig +++ b/generated/generated_v31.zig @@ -1,5 +1,6 @@ // -// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:34 UTC +// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:21 UTC +// Checksum: f2b912646d8fa5d // Changes to this file may cause incorrect behavior and will be lost if the code is regenerated // diff --git a/generated/generated_v31_yaml.zig b/generated/generated_v31_yaml.zig index ddc518e9..79c42823 100644 --- a/generated/generated_v31_yaml.zig +++ b/generated/generated_v31_yaml.zig @@ -1,5 +1,6 @@ // -// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:33 UTC +// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:19 UTC +// Checksum: f2b912646d8fa5d // Changes to this file may cause incorrect behavior and will be lost if the code is regenerated // diff --git a/generated/generated_v32.zig b/generated/generated_v32.zig index 66528bb9..d0efe56f 100644 --- a/generated/generated_v32.zig +++ b/generated/generated_v32.zig @@ -1,5 +1,6 @@ // -// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:35 UTC +// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:21 UTC +// Checksum: 509b078760c3c596 // Changes to this file may cause incorrect behavior and will be lost if the code is regenerated // diff --git a/generated/generated_v3_multiclient_endpoint.zig b/generated/generated_v3_multiclient_endpoint.zig index d9fde6c3..5a98c33e 100644 --- a/generated/generated_v3_multiclient_endpoint.zig +++ b/generated/generated_v3_multiclient_endpoint.zig @@ -1,5 +1,6 @@ // -// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:34 UTC +// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:19 UTC +// Checksum: 473979c39e597dcd // Changes to this file may cause incorrect behavior and will be lost if the code is regenerated // diff --git a/generated/generated_v3_multiclient_tag.zig b/generated/generated_v3_multiclient_tag.zig index 928bc2d6..3842f8eb 100644 --- a/generated/generated_v3_multiclient_tag.zig +++ b/generated/generated_v3_multiclient_tag.zig @@ -1,5 +1,6 @@ // -// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:34 UTC +// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:19 UTC +// Checksum: f3019a9f6bb963e8 // Changes to this file may cause incorrect behavior and will be lost if the code is regenerated // diff --git a/generated/generated_v3_tagfilter.zig b/generated/generated_v3_tagfilter.zig index 8ef12633..e9cdbda7 100644 --- a/generated/generated_v3_tagfilter.zig +++ b/generated/generated_v3_tagfilter.zig @@ -1,5 +1,6 @@ // -// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:34 UTC +// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:21 UTC +// Checksum: 22fee854a6f6c4c5 // Changes to this file may cause incorrect behavior and will be lost if the code is regenerated // diff --git a/generated/generated_v3_yaml.zig b/generated/generated_v3_yaml.zig index bb9302e4..37c921e7 100644 --- a/generated/generated_v3_yaml.zig +++ b/generated/generated_v3_yaml.zig @@ -1,5 +1,6 @@ // -// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:33 UTC +// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:19 UTC +// Checksum: 72ff812aa1dcc8ae // Changes to this file may cause incorrect behavior and will be lost if the code is regenerated // diff --git a/generated/lmstudio-multi/api.zig b/generated/lmstudio-multi/api.zig index 3a91bba1..2c6eb4ad 100644 --- a/generated/lmstudio-multi/api.zig +++ b/generated/lmstudio-multi/api.zig @@ -1,5 +1,6 @@ // -// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:34 UTC +// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:19 UTC +// Checksum: 25d54787f86d54bb // Changes to this file may cause incorrect behavior and will be lost if the code is regenerated // diff --git a/generated/lmstudio-multi/http.zig b/generated/lmstudio-multi/http.zig index 13aed8fa..a60c64da 100644 --- a/generated/lmstudio-multi/http.zig +++ b/generated/lmstudio-multi/http.zig @@ -1,5 +1,6 @@ // -// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:34 UTC +// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:19 UTC +// Checksum: 9a99a8c60afaf0ba // Changes to this file may cause incorrect behavior and will be lost if the code is regenerated // diff --git a/generated/lmstudio-multi/types.zig b/generated/lmstudio-multi/types.zig index 24a8ba97..bd45117a 100644 --- a/generated/lmstudio-multi/types.zig +++ b/generated/lmstudio-multi/types.zig @@ -1,5 +1,6 @@ // -// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:34 UTC +// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:19 UTC +// Checksum: 1f0cea2f8ca1edb9 // Changes to this file may cause incorrect behavior and will be lost if the code is regenerated // diff --git a/generated/lmstudio.zig b/generated/lmstudio.zig index f7ccf029..a784005d 100644 --- a/generated/lmstudio.zig +++ b/generated/lmstudio.zig @@ -1,5 +1,6 @@ // -// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:35 UTC +// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:20 UTC +// Checksum: d36d09ee15a6d678 // Changes to this file may cause incorrect behavior and will be lost if the code is regenerated // diff --git a/generated/multi/client.zig b/generated/multi/client.zig index e3a5feea..7e13325e 100644 --- a/generated/multi/client.zig +++ b/generated/multi/client.zig @@ -1,5 +1,6 @@ // -// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:34 UTC +// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:21 UTC +// Checksum: 104ec4f00cbd35c // Changes to this file may cause incorrect behavior and will be lost if the code is regenerated // diff --git a/generated/multi/models.zig b/generated/multi/models.zig index 9a926dc4..e1388c90 100644 --- a/generated/multi/models.zig +++ b/generated/multi/models.zig @@ -1,5 +1,6 @@ // -// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:34 UTC +// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:21 UTC +// Checksum: ccd4cbe4d5fdd54b // Changes to this file may cause incorrect behavior and will be lost if the code is regenerated // diff --git a/generated/multi/runtime.zig b/generated/multi/runtime.zig index 13aed8fa..1efeb7b5 100644 --- a/generated/multi/runtime.zig +++ b/generated/multi/runtime.zig @@ -1,5 +1,6 @@ // -// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:34 UTC +// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:21 UTC +// Checksum: 9a99a8c60afaf0ba // Changes to this file may cause incorrect behavior and will be lost if the code is regenerated // diff --git a/generated/multiple-clients/endpoint/client.zig b/generated/multiple-clients/endpoint/client.zig index 9b0a1f61..c167f45c 100644 --- a/generated/multiple-clients/endpoint/client.zig +++ b/generated/multiple-clients/endpoint/client.zig @@ -1,5 +1,6 @@ // -// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:33 UTC +// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:21 UTC +// Checksum: 3ba7d8d0c77648ed // Changes to this file may cause incorrect behavior and will be lost if the code is regenerated // diff --git a/generated/multiple-clients/endpoint/models.zig b/generated/multiple-clients/endpoint/models.zig index fe876314..e1388c90 100644 --- a/generated/multiple-clients/endpoint/models.zig +++ b/generated/multiple-clients/endpoint/models.zig @@ -1,5 +1,6 @@ // -// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:33 UTC +// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:21 UTC +// Checksum: ccd4cbe4d5fdd54b // Changes to this file may cause incorrect behavior and will be lost if the code is regenerated // diff --git a/generated/multiple-clients/endpoint/runtime.zig b/generated/multiple-clients/endpoint/runtime.zig index 9f843ec8..1efeb7b5 100644 --- a/generated/multiple-clients/endpoint/runtime.zig +++ b/generated/multiple-clients/endpoint/runtime.zig @@ -1,5 +1,6 @@ // -// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:33 UTC +// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:21 UTC +// Checksum: 9a99a8c60afaf0ba // Changes to this file may cause incorrect behavior and will be lost if the code is regenerated // diff --git a/generated/multiple-clients/tag/client.zig b/generated/multiple-clients/tag/client.zig index b167b989..bf3da1f5 100644 --- a/generated/multiple-clients/tag/client.zig +++ b/generated/multiple-clients/tag/client.zig @@ -1,5 +1,6 @@ // -// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:34 UTC +// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:20 UTC +// Checksum: da82a4c83980532c // Changes to this file may cause incorrect behavior and will be lost if the code is regenerated // diff --git a/generated/multiple-clients/tag/models.zig b/generated/multiple-clients/tag/models.zig index 9a926dc4..11e84483 100644 --- a/generated/multiple-clients/tag/models.zig +++ b/generated/multiple-clients/tag/models.zig @@ -1,5 +1,6 @@ // -// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:34 UTC +// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:20 UTC +// Checksum: ccd4cbe4d5fdd54b // Changes to this file may cause incorrect behavior and will be lost if the code is regenerated // diff --git a/generated/multiple-clients/tag/runtime.zig b/generated/multiple-clients/tag/runtime.zig index 13aed8fa..c011fa99 100644 --- a/generated/multiple-clients/tag/runtime.zig +++ b/generated/multiple-clients/tag/runtime.zig @@ -1,5 +1,6 @@ // -// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:34 UTC +// This code was generated by openapi2zig 0.4.2 (71bd014) on 2026-08-18 05:27:20 UTC +// Checksum: 9a99a8c60afaf0ba // Changes to this file may cause incorrect behavior and will be lost if the code is regenerated // diff --git a/generated/openai.zig b/generated/openai.zig index 514cf064..daab6194 100644 --- a/generated/openai.zig +++ b/generated/openai.zig @@ -1,5 +1,6 @@ // -// This code was generated by openapi2zig 0.4.2 (2b8b779) on 2026-08-14 16:09:34 UTC +// This code was generated by openapi2zig 0.4.2 (457e242) on 2026-08-18 05:37:54 UTC +// Checksum: 6deee3d3a176f9ed // Changes to this file may cause incorrect behavior and will be lost if the code is regenerated // diff --git a/src/generator.zig b/src/generator.zig index d5f66312..295245d6 100644 --- a/src/generator.zig +++ b/src/generator.zig @@ -137,7 +137,8 @@ fn generateCodeFromUnifiedDocument(allocator: std.mem.Allocator, io: std.Io, cwd }; defer if (!args.models_only) allocator.free(generated_code); - const header = try generated_header.renderNowFromBuildInfo(allocator, io); + const checksum = generated_header.computeChecksum(generated_code); + const header = try generated_header.renderNowWithChecksum(allocator, io, checksum); defer allocator.free(header); const output_code = try std.mem.concat(allocator, u8, &.{ header, generated_code }); defer allocator.free(output_code); @@ -146,15 +147,42 @@ fn generateCodeFromUnifiedDocument(allocator: std.mem.Allocator, io: std.Io, cwd if (std.fs.path.dirname(output_path)) |dir_path| { try cwd.createDirPath(io, dir_path); } + + const full_path = try std.fs.path.join(allocator, &.{ ".", output_path }); + defer allocator.free(full_path); + if (cwd.readFileAlloc(io, full_path, allocator, .limited(10 * 1024 * 1024))) |existing| { + defer allocator.free(existing); + if (!generated_header.hasChanged(existing, generated_code)) { + std.log.info("Skipping '{s}' (unchanged)", .{output_path}); + return; + } + } else |_| {} + const output_file = try cwd.createFile(io, output_path, .{}); defer output_file.close(io); try output_file.writeStreamingAll(io, output_code); std.log.info("Code generated successfully and written to '{s}'.", .{output_path}); } -fn writeFile(allocator: std.mem.Allocator, io: std.Io, cwd: std.Io.Dir, dir_path: []const u8, file_name: []const u8, content: []const u8) !void { +fn writeFile(allocator: std.mem.Allocator, io: std.Io, cwd: std.Io.Dir, dir_path: []const u8, file_name: []const u8, raw_code: []const u8) !void { const full_path = try std.fs.path.join(allocator, &.{ dir_path, file_name }); defer allocator.free(full_path); + + if (cwd.readFileAlloc(io, full_path, allocator, .limited(10 * 1024 * 1024))) |existing| { + defer allocator.free(existing); + if (!generated_header.hasChanged(existing, raw_code)) { + std.log.info("Skipping '{s}' (unchanged)", .{full_path}); + return; + } + } else |_| {} + + const checksum = generated_header.computeChecksum(raw_code); + const header = try generated_header.renderNowWithChecksum(allocator, io, checksum); + defer allocator.free(header); + + const content = try std.mem.concat(allocator, u8, &.{ header, raw_code }); + defer allocator.free(content); + if (std.fs.path.dirname(full_path)) |parent| { try cwd.createDirPath(io, parent); } @@ -177,12 +205,7 @@ fn generateMultipleFiles(allocator: std.mem.Allocator, io: std.Io, cwd: std.Io.D const generated_models = try model_generator.generate(unified_doc); defer allocator.free(generated_models); - const header = try generated_header.renderNowFromBuildInfo(allocator, io); - defer allocator.free(header); - - const models_content = try std.mem.concat(allocator, u8, &.{ header, generated_models }); - defer allocator.free(models_content); - try writeFile(allocator, io, cwd, dir_path, models_file, models_content); + try writeFile(allocator, io, cwd, dir_path, models_file, generated_models); if (args.models_only) return; @@ -191,9 +214,7 @@ fn generateMultipleFiles(allocator: std.mem.Allocator, io: std.Io, cwd: std.Io.D const generated_runtime = try runtime_gen.generate(); defer allocator.free(generated_runtime); - const runtime_content = try std.mem.concat(allocator, u8, &.{ header, generated_runtime }); - defer allocator.free(runtime_content); - try writeFile(allocator, io, cwd, dir_path, runtime_file, runtime_content); + try writeFile(allocator, io, cwd, dir_path, runtime_file, generated_runtime); const models_alias = try cli.deriveAlias(allocator, models_file, "models"); defer allocator.free(models_alias); @@ -226,9 +247,7 @@ fn generateMultipleFiles(allocator: std.mem.Allocator, io: std.Io, cwd: std.Io.D const generated_api = try api_generator.generateClientOnly(unified_doc); defer allocator.free(generated_api); - const client_content = try std.mem.concat(allocator, u8, &.{ header, generated_api }); - defer allocator.free(client_content); - try writeFile(allocator, io, cwd, dir_path, client_file, client_content); + try writeFile(allocator, io, cwd, dir_path, client_file, generated_api); } fn generateCodeFromDocument(allocator: std.mem.Allocator, io: std.Io, doc: anytype, args: cli.CliArgs, comptime Converter: type) !void { @@ -707,3 +726,83 @@ test "generateMultipleFiles composes per-tag client structs into the client file try std.testing.expect(std.mem.indexOf(u8, models_content, "pub const Pet") != null); try std.testing.expect(std.mem.indexOf(u8, models_content, "PetClient") == null); } + +test "generateCodeFromUnifiedDocument preserves timestamp when code unchanged" { + const test_utils = @import("tests/test_utils.zig"); + + var gpa = test_utils.createTestAllocator(); + const allocator = gpa.allocator(); + + const json = + \\{ + \\ "openapi": "3.0.0", + \\ "info": { "title": "fixture", "version": "1.0.0" }, + \\ "paths": {} + \\} + ; + + var unified = try openapi2zig.parseToUnified(allocator, json); + defer unified.deinit(allocator); + + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + + try generateCodeFromUnifiedDocument(allocator, std.testing.io, tmp.dir, unified, .{ + .input_path = "fixture.json", + .output_path = "out/api.zig", + }); + + const first = try tmp.dir.readFileAlloc(std.testing.io, "out/api.zig", allocator, .unlimited); + defer allocator.free(first); + + try generateCodeFromUnifiedDocument(allocator, std.testing.io, tmp.dir, unified, .{ + .input_path = "fixture.json", + .output_path = "out/api.zig", + }); + + const second = try tmp.dir.readFileAlloc(std.testing.io, "out/api.zig", allocator, .unlimited); + defer allocator.free(second); + + try std.testing.expectEqualStrings(first, second); +} + +test "generateMultipleFiles preserves timestamps when code unchanged" { + const test_utils = @import("tests/test_utils.zig"); + + var gpa = test_utils.createTestAllocator(); + const allocator = gpa.allocator(); + + const json = + \\{ + \\ "openapi": "3.0.0", + \\ "info": { "title": "fixture", "version": "1.0.0" }, + \\ "paths": {} + \\} + ; + + var unified = try openapi2zig.parseToUnified(allocator, json); + defer unified.deinit(allocator); + + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + + try generateMultipleFiles(allocator, std.testing.io, tmp.dir, unified, .{ + .input_path = "fixture.json", + .multiple_files = true, + .output_path = "out", + }); + + const first_models = try tmp.dir.readFileAlloc(std.testing.io, "out/models.zig", allocator, .unlimited); + defer allocator.free(first_models); + + try generateMultipleFiles(allocator, std.testing.io, tmp.dir, unified, .{ + .input_path = "fixture.json", + .multiple_files = true, + .output_path = "out", + }); + + const second_models = try tmp.dir.readFileAlloc(std.testing.io, "out/models.zig", allocator, .unlimited); + defer allocator.free(second_models); + + try std.testing.expectEqualStrings(first_models, second_models); +} diff --git a/src/generators/generated_header.zig b/src/generators/generated_header.zig index 573ee1b9..3bc05038 100644 --- a/src/generators/generated_header.zig +++ b/src/generators/generated_header.zig @@ -12,6 +12,18 @@ pub fn render(allocator: std.mem.Allocator, version: []const u8, timestamp: []co , .{ version, timestamp }); } +pub fn renderWithChecksum(allocator: std.mem.Allocator, version: []const u8, timestamp: []const u8, checksum: u64) ![]const u8 { + return try std.fmt.allocPrint(allocator, + \\// + \\// This code was generated by openapi2zig {s} on {s} + \\// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated + \\// Checksum: {x} + \\// + \\ + \\ + , .{ version, timestamp, checksum }); +} + pub fn renderNow(allocator: std.mem.Allocator, io: std.Io, version: []const u8) ![]const u8 { const timestamp = try formatUtcTimestamp(allocator, io); defer allocator.free(timestamp); @@ -24,6 +36,33 @@ pub fn renderNowFromBuildInfo(allocator: std.mem.Allocator, io: std.Io) ![]const return try renderNow(allocator, io, version); } +pub fn renderNowWithChecksum(allocator: std.mem.Allocator, io: std.Io, checksum: u64) ![]const u8 { + const version = try std.fmt.allocPrint(allocator, "{s} ({s})", .{ version_info.VERSION, version_info.GIT_COMMIT }); + defer allocator.free(version); + const timestamp = try formatUtcTimestamp(allocator, io); + defer allocator.free(timestamp); + return try renderWithChecksum(allocator, version, timestamp, checksum); +} + +pub fn computeChecksum(code: []const u8) u64 { + return std.hash.Wyhash.hash(0, code); +} + +pub fn extractChecksum(content: []const u8) ?u64 { + const marker = "// Checksum: "; + const start = std.mem.indexOf(u8, content, marker) orelse return null; + const after = content[start + marker.len ..]; + const end = std.mem.indexOf(u8, after, "\n") orelse after.len; + const hex_str = std.mem.trim(u8, after[0..end], " "); + return std.fmt.parseInt(u64, hex_str, 16) catch null; +} + +pub fn hasChanged(existing_content: []const u8, new_code: []const u8) bool { + const existing_checksum = extractChecksum(existing_content) orelse return true; + const new_checksum = computeChecksum(new_code); + return existing_checksum != new_checksum; +} + fn formatUtcTimestamp(allocator: std.mem.Allocator, io: std.Io) ![]const u8 { const now = std.Io.Clock.real.now(io); const epoch_seconds = std.time.epoch.EpochSeconds{ .secs = @as(u64, @intCast(now.toSeconds())) }; diff --git a/src/lib.zig b/src/lib.zig index 410ea918..9cefd7ae 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -241,17 +241,24 @@ pub fn generateCode(allocator: std.mem.Allocator, io: std.Io, unified_doc: Unifi const models_code = try generateModels(allocator, unified_doc); defer allocator.free(models_code); - const header = try generated_header.renderNowFromBuildInfo(allocator, io); - defer allocator.free(header); - if (args.models_only) { + const checksum = generated_header.computeChecksum(models_code); + const header = try generated_header.renderNowWithChecksum(allocator, io, checksum); + defer allocator.free(header); return try std.mem.concat(allocator, u8, &.{ header, models_code }); } const api_code = try generateApi(allocator, unified_doc, args); defer allocator.free(api_code); - return try std.mem.concat(allocator, u8, &.{ header, models_code, "\n", api_code }); + const combined = try std.mem.concat(allocator, u8, &.{ models_code, "\n", api_code }); + defer allocator.free(combined); + + const checksum = generated_header.computeChecksum(combined); + const header = try generated_header.renderNowWithChecksum(allocator, io, checksum); + defer allocator.free(header); + + return try std.mem.concat(allocator, u8, &.{ header, combined }); } /// Result of generating code in multiple-files mode. @@ -274,10 +281,11 @@ pub fn generateCodeMultiple(allocator: std.mem.Allocator, io: std.Io, unified_do const models_code = try generateModels(allocator, unified_doc); defer allocator.free(models_code); - const header = try generated_header.renderNowFromBuildInfo(allocator, io); - defer allocator.free(header); + const models_checksum = generated_header.computeChecksum(models_code); + const models_header = try generated_header.renderNowWithChecksum(allocator, io, models_checksum); + defer allocator.free(models_header); - const models_with_header = try std.mem.concat(allocator, u8, &.{ header, models_code }); + const models_with_header = try std.mem.concat(allocator, u8, &.{ models_header, models_code }); errdefer allocator.free(models_with_header); if (args.models_only) { @@ -289,7 +297,11 @@ pub fn generateCodeMultiple(allocator: std.mem.Allocator, io: std.Io, unified_do const runtime_code = try runtime_gen.generate(); defer allocator.free(runtime_code); - const runtime_with_header = try std.mem.concat(allocator, u8, &.{ header, runtime_code }); + const runtime_checksum = generated_header.computeChecksum(runtime_code); + const runtime_header = try generated_header.renderNowWithChecksum(allocator, io, runtime_checksum); + defer allocator.free(runtime_header); + + const runtime_with_header = try std.mem.concat(allocator, u8, &.{ runtime_header, runtime_code }); errdefer allocator.free(runtime_with_header); var api_gen = UnifiedApiGenerator.init(allocator, args); @@ -299,7 +311,11 @@ pub fn generateCodeMultiple(allocator: std.mem.Allocator, io: std.Io, unified_do const api_code = try api_gen.generateClientOnly(unified_doc); defer allocator.free(api_code); - const client_with_header = try std.mem.concat(allocator, u8, &.{ header, api_code }); + const client_checksum = generated_header.computeChecksum(api_code); + const client_header = try generated_header.renderNowWithChecksum(allocator, io, client_checksum); + defer allocator.free(client_header); + + const client_with_header = try std.mem.concat(allocator, u8, &.{ client_header, api_code }); errdefer allocator.free(client_with_header); return .{ diff --git a/src/tests/generated_header_tests.zig b/src/tests/generated_header_tests.zig index 4a2e5246..55d6b772 100644 --- a/src/tests/generated_header_tests.zig +++ b/src/tests/generated_header_tests.zig @@ -21,6 +21,117 @@ test "render emits expected header" { , header); } +test "checksum is deterministic for same content" { + var gpa = test_utils.createTestAllocator(); + defer std.debug.assert(gpa.deinit() == .ok); + + const code = "pub const Foo = struct {\n name: []const u8,\n};\n"; + const checksum1 = generated_header.computeChecksum(code); + const checksum2 = generated_header.computeChecksum(code); + + try std.testing.expectEqual(checksum1, checksum2); +} + +test "checksum differs for different content" { + var gpa = test_utils.createTestAllocator(); + defer std.debug.assert(gpa.deinit() == .ok); + + const code1 = "pub const Foo = struct {\n name: []const u8,\n};\n"; + const code2 = "pub const Bar = struct {\n id: i64,\n};\n"; + + const checksum1 = generated_header.computeChecksum(code1); + const checksum2 = generated_header.computeChecksum(code2); + + try std.testing.expect(checksum1 != checksum2); +} + +test "header includes checksum line" { + var gpa = test_utils.createTestAllocator(); + const allocator = gpa.allocator(); + defer std.debug.assert(gpa.deinit() == .ok); + + const code = "pub const Foo = struct {\n name: []const u8,\n};\n"; + const checksum = generated_header.computeChecksum(code); + + const header = try generated_header.renderWithChecksum(allocator, "0.6.0 (a1b2c3d)", "2026-07-12 21:49:56 UTC", checksum); + defer allocator.free(header); + + try std.testing.expect(std.mem.indexOf(u8, header, "Checksum:") != null); + try std.testing.expect(std.mem.indexOf(u8, header, "Changes to this file may cause incorrect behavior") != null); +} + +test "checksum can be extracted from rendered header" { + var gpa = test_utils.createTestAllocator(); + const allocator = gpa.allocator(); + defer std.debug.assert(gpa.deinit() == .ok); + + const code = "pub const Foo = struct {\n name: []const u8,\n};\n"; + const checksum = generated_header.computeChecksum(code); + + const header = try generated_header.renderWithChecksum(allocator, "0.6.0 (a1b2c3d)", "2026-07-12 21:49:56 UTC", checksum); + defer allocator.free(header); + + const extracted = generated_header.extractChecksum(header); + try std.testing.expect(extracted != null); + try std.testing.expectEqual(checksum, extracted.?); +} + +test "hasChanged returns false when code has not changed" { + var gpa = test_utils.createTestAllocator(); + const allocator = gpa.allocator(); + defer std.debug.assert(gpa.deinit() == .ok); + + const code = "pub const Foo = struct {\n name: []const u8,\n};\n"; + const checksum = generated_header.computeChecksum(code); + + const header = try generated_header.renderWithChecksum(allocator, "0.6.0 (a1b2c3d)", "2026-07-12 21:49:56 UTC", checksum); + defer allocator.free(header); + + const full_content = try std.mem.concat(allocator, u8, &.{ header, code }); + defer allocator.free(full_content); + + try std.testing.expect(!generated_header.hasChanged(full_content, code)); +} + +test "hasChanged returns true when code has changed" { + var gpa = test_utils.createTestAllocator(); + const allocator = gpa.allocator(); + defer std.debug.assert(gpa.deinit() == .ok); + + const original_code = "pub const Foo = struct {\n name: []const u8,\n};\n"; + const checksum = generated_header.computeChecksum(original_code); + + const header = try generated_header.renderWithChecksum(allocator, "0.6.0 (a1b2c3d)", "2026-07-12 21:49:56 UTC", checksum); + defer allocator.free(header); + + const full_content = try std.mem.concat(allocator, u8, &.{ header, original_code }); + defer allocator.free(full_content); + + const new_code = "pub const Bar = struct {\n id: i64,\n};\n"; + try std.testing.expect(generated_header.hasChanged(full_content, new_code)); +} + +test "hasChanged returns true for file without checksum header" { + var gpa = test_utils.createTestAllocator(); + const allocator = gpa.allocator(); + defer std.debug.assert(gpa.deinit() == .ok); + + const old_header = + \\// + \\// This code was generated by openapi2zig 0.5.0 (abc1234) on 2026-01-01 00:00:00 UTC + \\// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated + \\// + \\ + \\ + ; + + const code = "pub const Foo = struct {\n name: []const u8,\n};\n"; + const full_content = try std.mem.concat(allocator, u8, &.{ old_header, code }); + defer allocator.free(full_content); + + try std.testing.expect(generated_header.hasChanged(full_content, code)); +} + test "generateCode output includes generated header" { var gpa = test_utils.createTestAllocator(); const allocator = gpa.allocator();