Add YAML verification to Smoke Tests#51
Conversation
christianhelle
commented
May 1, 2026
- Add YAML OpenAPI parsing
- Preserve YAML scalar quoting
- Differentiate unsupported OpenAPI versions
- Remove stale YAML roadmap item
- Fix vendored zig-yaml README snippets
- Tighten vendored zig-yaml parser coverage
- test: cover YAML smoke sweep
- docs: align YAML smoke coverage
- build: add YAML smoke artifacts
- docs: clarify YAML smoke scope
- chore: record YAML smoke decisions
- chore: update squad session records
- chore(squad): record yaml smoke closeout
- chore: clean up squad state
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (5)
📒 Files selected for processing (24)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 0/1 reviews remaining, refill in 16 minutes and 43 seconds.Comment |
There was a problem hiding this comment.
Pull request overview
Adds YAML input support and YAML verification to openapi2zig’s smoke testing pipeline by vendoring zig-yaml, introducing YAML→JSON normalization, and updating the curated generated harness + documentation to reflect the expanded format coverage.
Changes:
- Vendor
zig-yamland wire it into the build so the CLI can accept.yaml/.ymlby converting YAML→JSON before OpenAPI/Swagger version detection and generation. - Expand the broad smoke sweep (
test/smoke-tests.ps1) to discover JSON+YAML fixtures and avoid JSON/YAML sibling output collisions via__<format>__filenames. - Extend curated generation artifacts and harness imports to include YAML-generated clients (v2/v3/v3.1), plus update README + squad records to document scope/decisions.
Reviewed changes
Copilot reviewed 23 out of 45 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| vendor/zig-yaml/test/test.zig | Adds vendor library tests for YAML parsing/stringify + typed decoding. |
| vendor/zig-yaml/test/spec.zig | Adds build-step generator for YAML spec test suite runner. |
| vendor/zig-yaml/test/single_lib.tbd | Adds test fixture for directive + mapping/list parsing. |
| vendor/zig-yaml/test/simple.yaml | Adds basic YAML fixture for typed parsing. |
| vendor/zig-yaml/test/multi_lib.tbd | Adds multi-document fixture coverage for TBD parsing. |
| vendor/zig-yaml/src/stringify.zig | Adds top-level stringify helper that encodes Zig values to YAML AST then writes YAML. |
| vendor/zig-yaml/src/lib.zig | Exposes vendor library modules and re-exports stringify. |
| vendor/zig-yaml/src/Yaml.zig | Implements YAML document load/parse/stringify and YAML AST/value encoding/decoding. |
| vendor/zig-yaml/src/Tree.zig | Defines the YAML parse tree storage format and helpers (raw strings, directives, extras). |
| vendor/zig-yaml/src/Tokenizer.zig | Implements YAML tokenization with basic YAML primitives and tests. |
| vendor/zig-yaml/src/Parser.zig | Implements YAML parsing into Tree with error bundling and node encoding. |
| vendor/zig-yaml/build.zig.zon | Declares vendored package metadata (Zig min version, included paths). |
| vendor/zig-yaml/build.zig | Adds vendor module + vendor test step. |
| vendor/zig-yaml/README.md | Updates vendor README, usage examples, and spec-test instructions. |
| vendor/zig-yaml/LICENSE | Adds vendor license file. |
| test/smoke-tests.ps1 | Expands smoke discovery to include .json/.yaml/.yml, adds format counts, and collision-safe output naming. |
| src/yaml_loader.zig | Adds YAML→JSON normalization and conversion logic used by the CLI/library. |
| src/tests/yaml_loader_tests.zig | Adds unit tests for YAML→JSON conversion + version detection + key/scalar edge cases. |
| src/tests.zig | Registers YAML loader tests and generator tests in the test aggregator. |
| src/lib.zig | Exposes yamlToJson and YAML-based parsing/version-detection helpers as part of the library API. |
| src/generator.zig | Enables YAML inputs in generation path and distinguishes unsupported OpenAPI versions with a dedicated error. |
| generated/main.zig | Extends runtime harness to import/init YAML-generated clients. |
| generated/generated_v31_yaml.zig | Adds a checked-in curated YAML-generated v3.1 client artifact. |
| generated/compile_generated.zig | Extends compile-only harness to reference YAML-generated curated artifacts. |
| build.zig.zon | Adds the vendored YAML dependency and includes openapi/ + vendor/zig-yaml/ in packaged paths. |
| build.zig | Wires the YAML dependency into all root modules and adds curated YAML generation steps for v2/v3/v3.1. |
| README.md | Updates docs to state JSON+YAML coverage, describes smoke-sweep behavior, and documents curated YAML artifacts. |
| .squad/skills/smoke-harness-scope/SKILL.md | Records conventions for keeping broad sweep vs curated harness responsibilities separate. |
| .squad/skills/project-conventions/SKILL.md | Adds documentation convention about describing the two smoke layers distinctly. |
| .squad/orchestration-log/2026-05-01T09-50-14Z-starkiller.md | Records orchestration approval log entry. |
| .squad/orchestration-log/2026-05-01T09-50-14Z-lando.md | Records orchestration approval log entry. |
| .squad/orchestration-log/2026-05-01T09-50-14Z-juno.md | Records orchestration approval log entry. |
| .squad/log/2026-05-01T09-50-14Z-yaml-smoke-closeout.md | Records closeout actions for YAML smoke coordination. |
| .squad/identity/now.md | Updates focus area/status to YAML smoke coverage work. |
| .squad/decisions.md | Records decisions around YAML smoke scope, acceptance, and documentation requirements. |
| .squad/agents/starkiller/history.md | Updates agent history with YAML smoke notes and archival summary. |
| .squad/agents/starkiller/history-archive.md | Adds archived historical record. |
| .squad/agents/scribe/history.md | Records scribe closeout actions. |
| .squad/agents/lando/history.md | Records scope review + verdict details for YAML smoke work. |
| .squad/agents/juno/history.md | Records docs alignment guidance + closeout. |
| .squad/agents/fenster/history.md | Records YAML smoke implementation notes + denylist scope. |
| fn appendMarkedLine(allocator: std.mem.Allocator, out: *std.ArrayList(u8), line: []const u8) !void { | ||
| const colon_index = std.mem.indexOfScalar(u8, line, ':') orelse { | ||
| try out.appendSlice(allocator, line); | ||
| return; | ||
| }; | ||
|
|
||
| var value_start = colon_index + 1; | ||
| while (value_start < line.len and (line[value_start] == ' ' or line[value_start] == '\t')) : (value_start += 1) {} | ||
|
|
||
| if (value_start >= line.len) { | ||
| try out.appendSlice(allocator, line); | ||
| return; | ||
| } | ||
|
|
||
| const comment_index = findInlineCommentStart(line[value_start..]) orelse line[value_start..].len; | ||
| const scalar_end = value_start + comment_index; | ||
| const scalar = std.mem.trimEnd(u8, line[value_start..scalar_end], " \t\r"); | ||
| if (!shouldMarkOriginallyUnquotedScalar(scalar)) { | ||
| try out.appendSlice(allocator, line); | ||
| return; | ||
| } | ||
|
|
||
| try out.appendSlice(allocator, line[0..value_start]); | ||
| try out.appendSlice(allocator, unquoted_scalar_placeholder); | ||
| try out.appendSlice(allocator, line[value_start..]); | ||
| } | ||
|
|
There was a problem hiding this comment.
markOriginallyUnquotedScalars only marks scalars that appear on key: value lines. Any scalars in sequences (e.g. - 10, enum: [1, 2], multi-line - blocks) will never be marked as originally-unquoted, so writeJsonScalar will stringify them as JSON strings rather than numbers/bools/null. This can change OpenAPI semantics (notably schema.enum values) when converting YAML→JSON. Consider extending the marking pass to also handle sequence items (lines starting with - after indentation) and flow-sequence elements, or move the “quoted vs unquoted” decision into token-level parsing rather than a colon-line heuristic.
| fn appendMarkedLine(allocator: std.mem.Allocator, out: *std.ArrayList(u8), line: []const u8) !void { | |
| const colon_index = std.mem.indexOfScalar(u8, line, ':') orelse { | |
| try out.appendSlice(allocator, line); | |
| return; | |
| }; | |
| var value_start = colon_index + 1; | |
| while (value_start < line.len and (line[value_start] == ' ' or line[value_start] == '\t')) : (value_start += 1) {} | |
| if (value_start >= line.len) { | |
| try out.appendSlice(allocator, line); | |
| return; | |
| } | |
| const comment_index = findInlineCommentStart(line[value_start..]) orelse line[value_start..].len; | |
| const scalar_end = value_start + comment_index; | |
| const scalar = std.mem.trimEnd(u8, line[value_start..scalar_end], " \t\r"); | |
| if (!shouldMarkOriginallyUnquotedScalar(scalar)) { | |
| try out.appendSlice(allocator, line); | |
| return; | |
| } | |
| try out.appendSlice(allocator, line[0..value_start]); | |
| try out.appendSlice(allocator, unquoted_scalar_placeholder); | |
| try out.appendSlice(allocator, line[value_start..]); | |
| } | |
| fn findTopLevelColon(line: []const u8) ?usize { | |
| var in_single_quote = false; | |
| var in_double_quote = false; | |
| var escape = false; | |
| var bracket_depth: usize = 0; | |
| var brace_depth: usize = 0; | |
| for (line, 0..) |char, i| { | |
| if (in_double_quote) { | |
| if (escape) { | |
| escape = false; | |
| continue; | |
| } | |
| switch (char) { | |
| '\\' => escape = true, | |
| '"' => in_double_quote = false, | |
| else => {}, | |
| } | |
| continue; | |
| } | |
| if (in_single_quote) { | |
| if (char == '\'') in_single_quote = false; | |
| continue; | |
| } | |
| switch (char) { | |
| '\'' => in_single_quote = true, | |
| '"' => in_double_quote = true, | |
| '[' => bracket_depth += 1, | |
| ']' => if (bracket_depth > 0) bracket_depth -= 1, | |
| '{' => brace_depth += 1, | |
| '}' => if (brace_depth > 0) brace_depth -= 1, | |
| ':' => if (bracket_depth == 0 and brace_depth == 0) return i, | |
| else => {}, | |
| } | |
| } | |
| return null; | |
| } | |
| fn findFlowSequenceClose(region: []const u8, open_index: usize) ?usize { | |
| var in_single_quote = false; | |
| var in_double_quote = false; | |
| var escape = false; | |
| var bracket_depth: usize = 0; | |
| for (region[open_index..], open_index..) |char, i| { | |
| if (in_double_quote) { | |
| if (escape) { | |
| escape = false; | |
| continue; | |
| } | |
| switch (char) { | |
| '\\' => escape = true, | |
| '"' => in_double_quote = false, | |
| else => {}, | |
| } | |
| continue; | |
| } | |
| if (in_single_quote) { | |
| if (char == '\'') in_single_quote = false; | |
| continue; | |
| } | |
| switch (char) { | |
| '\'' => in_single_quote = true, | |
| '"' => in_double_quote = true, | |
| '[' => bracket_depth += 1, | |
| ']' => { | |
| if (bracket_depth == 0) return null; | |
| bracket_depth -= 1; | |
| if (bracket_depth == 0) return i; | |
| }, | |
| else => {}, | |
| } | |
| } | |
| return null; | |
| } | |
| fn appendMarkedFlowSequenceElement(allocator: std.mem.Allocator, out: *std.ArrayList(u8), element: []const u8) !void { | |
| const trimmed = std.mem.trim(u8, element, " \t\r"); | |
| if (trimmed.len == 0) { | |
| try out.appendSlice(allocator, element); | |
| return; | |
| } | |
| const leading_len = std.mem.indexOf(u8, element, trimmed) orelse 0; | |
| const trailing_start = leading_len + trimmed.len; | |
| if (trimmed[0] == '\'' or trimmed[0] == '"' or trimmed[0] == '[' or trimmed[0] == '{' or findTopLevelColon(trimmed) != null or !shouldMarkOriginallyUnquotedScalar(trimmed)) { | |
| try out.appendSlice(allocator, element); | |
| return; | |
| } | |
| try out.appendSlice(allocator, element[0..leading_len]); | |
| try out.appendSlice(allocator, unquoted_scalar_placeholder); | |
| try out.appendSlice(allocator, element[leading_len..trailing_start]); | |
| try out.appendSlice(allocator, element[trailing_start..]); | |
| } | |
| fn appendMarkedFlowSequence(allocator: std.mem.Allocator, out: *std.ArrayList(u8), region: []const u8) !void { | |
| const open_index = std.mem.indexOfScalar(u8, region, '[') orelse { | |
| try out.appendSlice(allocator, region); | |
| return; | |
| }; | |
| const close_index = findFlowSequenceClose(region, open_index) orelse { | |
| try out.appendSlice(allocator, region); | |
| return; | |
| }; | |
| try out.appendSlice(allocator, region[0 .. open_index + 1]); | |
| const inner = region[open_index + 1 .. close_index]; | |
| var in_single_quote = false; | |
| var in_double_quote = false; | |
| var escape = false; | |
| var bracket_depth: usize = 0; | |
| var brace_depth: usize = 0; | |
| var element_start: usize = 0; | |
| for (inner, 0..) |char, i| { | |
| if (in_double_quote) { | |
| if (escape) { | |
| escape = false; | |
| continue; | |
| } | |
| switch (char) { | |
| '\\' => escape = true, | |
| '"' => in_double_quote = false, | |
| else => {}, | |
| } | |
| continue; | |
| } | |
| if (in_single_quote) { | |
| if (char == '\'') in_single_quote = false; | |
| continue; | |
| } | |
| switch (char) { | |
| '\'' => in_single_quote = true, | |
| '"' => in_double_quote = true, | |
| '[' => bracket_depth += 1, | |
| ']' => if (bracket_depth > 0) bracket_depth -= 1, | |
| '{' => brace_depth += 1, | |
| '}' => if (brace_depth > 0) brace_depth -= 1, | |
| ',' => if (bracket_depth == 0 and brace_depth == 0) { | |
| try appendMarkedFlowSequenceElement(allocator, out, inner[element_start..i]); | |
| try out.append(allocator, ','); | |
| element_start = i + 1; | |
| }, | |
| else => {}, | |
| } | |
| } | |
| try appendMarkedFlowSequenceElement(allocator, out, inner[element_start..]); | |
| try out.append(allocator, ']'); | |
| try out.appendSlice(allocator, region[close_index + 1 ..]); | |
| } | |
| fn appendMarkedValueRegion(allocator: std.mem.Allocator, out: *std.ArrayList(u8), region: []const u8) !void { | |
| const trimmed = std.mem.trim(u8, region, " \t\r"); | |
| if (trimmed.len == 0) { | |
| try out.appendSlice(allocator, region); | |
| return; | |
| } | |
| if (trimmed[0] == '[') { | |
| try appendMarkedFlowSequence(allocator, out, region); | |
| return; | |
| } | |
| if (trimmed[0] == '{' or trimmed[0] == '\'' or trimmed[0] == '"' or findTopLevelColon(trimmed) != null or !shouldMarkOriginallyUnquotedScalar(trimmed)) { | |
| try out.appendSlice(allocator, region); | |
| return; | |
| } | |
| const leading_len = std.mem.indexOf(u8, region, trimmed) orelse 0; | |
| const trailing_start = leading_len + trimmed.len; | |
| try out.appendSlice(allocator, region[0..leading_len]); | |
| try out.appendSlice(allocator, unquoted_scalar_placeholder); | |
| try out.appendSlice(allocator, region[leading_len..trailing_start]); | |
| try out.appendSlice(allocator, region[trailing_start..]); | |
| } | |
| fn appendMarkedLine(allocator: std.mem.Allocator, out: *std.ArrayList(u8), line: []const u8) !void { | |
| const comment_index = findInlineCommentStart(line) orelse line.len; | |
| const content = line[0..comment_index]; | |
| const comment = line[comment_index..]; | |
| var index: usize = 0; | |
| while (index < content.len and (content[index] == ' ' or content[index] == '\t')) : (index += 1) {} | |
| if (index < content.len and content[index] == '-') { | |
| const next_index = index + 1; | |
| if (next_index == content.len or content[next_index] == ' ' or content[next_index] == '\t') { | |
| var value_start = next_index; | |
| while (value_start < content.len and (content[value_start] == ' ' or content[value_start] == '\t')) : (value_start += 1) {} | |
| try out.appendSlice(allocator, content[0..value_start]); | |
| try appendMarkedValueRegion(allocator, out, content[value_start..]); | |
| try out.appendSlice(allocator, comment); | |
| return; | |
| } | |
| } | |
| const colon_index = findTopLevelColon(content) orelse { | |
| try out.appendSlice(allocator, line); | |
| return; | |
| }; | |
| var value_start = colon_index + 1; | |
| while (value_start < content.len and (content[value_start] == ' ' or content[value_start] == '\t')) : (value_start += 1) {} | |
| try out.appendSlice(allocator, content[0..value_start]); | |
| try appendMarkedValueRegion(allocator, out, content[value_start..]); | |
| try out.appendSlice(allocator, comment); | |
| } |
Keep quoted YAML scalars as strings by marking values that were originally unquoted before coercing them during YAML-to-JSON conversion. Also preserve folded block paragraph breaks, escape backslashes in synthetic double-quoted YAML, and add regression coverage for the affected cases. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a distinct UnsupportedOpenAPIVersion generator error so callers can tell version detection failures apart from invalid file extensions. Cover the behavior with a regression test wired into the main test module. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drop the README planned-feature entry for YAML specification format support now that YAML input is implemented and documented elsewhere. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add language labels to the installation and build.zig examples and correct the raw-load snippet to operate on the loaded yaml value. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Accept escaped backslashes in vendored zig-yaml double-quoted parsing and strengthen the vendor test suite to compare booleans, UUIDs, symmetric optionals, and collection lengths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Discover YAML specs in smoke-tests, keep output filenames extension-aware so JSON/YAML siblings do not collide, and denylist the currently known YAML parser gaps to keep the sweep deterministic. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Clarify that the broad smoke matrix covers JSON and YAML fixtures, including source-format-aware output names, while the checked-in sample generation section still documents the curated JSON-backed harness. Also record the team note and reusable doc pattern for future smoke-test updates. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extend the quick generated-artifact smoke path to emit YAML-backed fixtures for the existing v2, v3, and v3.1 samples, and wire the generated harnesses to compile and initialize those modules alongside the JSON artifacts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Align the README with the final smoke-test scope: broad smoke discovery covers JSON and YAML roots without requiring sibling fixtures, while the curated quick harness stays selective and keeps OpenAPI v3.2 JSON-only until a real YAML fixture exists. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Capture the YAML smoke-layer decisions, append the implementation learnings, and record the reusable smoke-harness pattern for format-aware outputs and mode-wide denylist entries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- merge the YAML smoke decision inbox into decisions.md - record orchestration and session logs for the 2026-05-01 closeout - summarize Starkiller history and append cross-agent notes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove processed squad inbox notes and refresh current squad identity. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
466b24c to
2a05bfa
Compare