diff --git a/.squad/agents/fenster/history.md b/.squad/agents/fenster/history.md index e84e1a4..465a4cd 100644 --- a/.squad/agents/fenster/history.md +++ b/.squad/agents/fenster/history.md @@ -129,3 +129,19 @@ - Designed/implemented/validated est/smoke-tests.ps1 (88 cases: 22 specs × 4 wrapper modes), CI job updated with failure-only artifact upload, README documented. - Initial denylist: ingram-micro.json (duplicate pub const emissions in unified model generator — follow-up backend work). - Decision recorded in decisions.md (2026-04-30 entry). Session-scoped directive: agents use Claude Opus 4.7 for this session only. + +### YAML smoke coverage across both layers (2026-05-01T11:50:14.189+02:00) + +- `test/smoke-tests.ps1` now discovers `.json`, `.yaml`, and `.yml` fixtures under `openapi/v2.0`, `v3.0`, `v3.1`, and `v3.2`, and writes smoke outputs as `test/output//____.zig` so JSON/YAML siblings never overwrite each other. +- The curated harness remains separate: `build.zig` now generates `generated/generated_v2_yaml.zig`, `generated/generated_v3_yaml.zig`, and `generated/generated_v31_yaml.zig`; `generated/compile_generated.zig` and `generated/main.zig` import those YAML artifacts beside the existing JSON ones. `openapi/v3.2` stays JSON-only because there is no checked-in YAML fixture. +- Broad YAML smoke currently passes with targeted denylist entries for six mode-independent YAML `ParseFailure` fixtures: `openapi/v2.0/petstore-expanded.yaml`, `openapi/v2.0/uber.yaml`, `openapi/v3.0/api-with-examples.yaml`, `openapi/v3.0/bot.paths.yaml`, `openapi/v3.0/petstore-expanded.yaml`, and `openapi/v3.0/uspto.yaml`. + +### 2026-05-01T11:50:14.189+02:00 — Scribe closeout + +- Scribe merged Fenster's YAML smoke decisions into `decisions.md`: keep the two smoke layers separate, use `____.zig` output names, and allow deterministic parse-phase YAML denylist entries with `Mode = "*"`. +- The closeout keeps the YAML parser/normalization blocker explicit so backend follow-up can retire those denylist entries intentionally instead of hiding them behind JSON-only behavior. + +### 2026-05-01T09:50:14Z — Scribe closeout + +- Scribe recorded the YAML smoke release as shipped from commits `ac9ed97`, `e6f07ee`, and `55c260e`, preserving the two-layer split between the broad PowerShell sweep and the curated generated harness. +- The shared decision log now carries the collision-safe `____` smoke filename rule and the explicit wildcard denylist policy for deterministic pre-wrapper YAML parse failures. diff --git a/.squad/agents/juno/history.md b/.squad/agents/juno/history.md index 1150645..0c35f66 100644 --- a/.squad/agents/juno/history.md +++ b/.squad/agents/juno/history.md @@ -128,3 +128,17 @@ Users can now provide OpenAPI specs via remote URLs in addition to local file pa - Designed/implemented/validated est/smoke-tests.ps1 (88 cases: 22 specs × 4 wrapper modes), CI job updated with failure-only artifact upload, README documented. - Initial denylist: ingram-micro.json (duplicate pub const emissions in unified model generator — follow-up backend work). - Decision recorded in decisions.md (2026-04-30 entry). Session-scoped directive: agents use Claude Opus 4.7 for this session only. + +## 2026-05-01T11:50:14.189+02:00 — YAML smoke docs alignment +- Broad smoke coverage lives in `test/smoke-tests.ps1`; it should treat YAML as first-class input and encode source format in artifact names such as `__json__` / `__yaml__` to avoid sibling collisions. +- README smoke docs should distinguish the broad PowerShell sweep from the smaller curated sample harness in `build.zig` and `generated/main.zig`; broad discovery must not imply JSON/YAML twin files are required, and the curated harness should call out that `v3.2` is still JSON-only because the repo has no `v3.2` YAML root fixture. +- Useful review paths for this topic: `README.md`, `test/smoke-tests.ps1`, `build.zig`, `generated/main.zig`, and YAML fixture roots like `openapi/v2.0/petstore.yaml`, `openapi/v3.0/petstore.yaml`, `openapi/v3.1/webhook-example.yaml`, `openapi/v3.0/bot.paths.yaml`. + +### 2026-05-01T11:50:14.189+02:00 — Scribe closeout + +- Scribe recorded Juno's docs closeout: README now distinguishes the broad JSON+YAML smoke sweep from the curated harness, keeps `v3.2` JSON-only, and the doc work landed as commits `de53aaa` and `8e87cbe`. +- Closeout notes preserve the `__json__` / `__yaml__` output naming rule in the docs so artifact descriptions stay aligned with the smoke script. + +### 2026-05-01T09:50:14Z — Scribe closeout + +- Scribe merged Juno's docs guidance into the shared decision log: README smoke docs must separate the broad JSON+YAML sweep from the curated harness, avoid implying JSON/YAML sibling pairs are required, and keep `openapi/v3.2` described as JSON-only today. diff --git a/.squad/agents/lando/history.md b/.squad/agents/lando/history.md index 9eec3e3..c09349c 100644 --- a/.squad/agents/lando/history.md +++ b/.squad/agents/lando/history.md @@ -1,96 +1,96 @@ -## Project Context - -**Project:** openapi2zig — CLI tool for generating type-safe Zig API clients from OpenAPI/Swagger specs -**User:** Christian Helle -**Tech Stack:** Zig v0.15.2, OpenAPI v3.0, Swagger v2.0 -**Architecture:** Unified converter pattern (parse → normalize → generate) - -## Key Architecture - -- **Specs supported:** Swagger v2.0, OpenAPI v3.0 -- **Parsing:** Version-specific models in `src/models/v2.0/` and `src/models/v3.0/` -- **Converters:** `src/generators/converters/` transforms version-specific → unified document -- **Generation:** `src/generators/unified/` generates Zig code from unified representation -- **Memory model:** Every dynamic struct has `deinit(allocator)`. **Always use `defer parsed.deinit(allocator)`.** -- **Testing:** xUnit-style with `test_utils.createTestAllocator()`, test against both v2.0 and v3.0 - -## Known Patterns - -- Version detection happens in `src/detector.zig` by parsing JSON for `openapi` or `swagger` fields -- CLI routing in `src/cli.zig` and `src/generator.zig` -- Sample specs in `openapi/v2.0/petstore.json` and `openapi/v3.0/petstore.json` for testing -- Generated code output to `generated/main.zig` (test harness imports both versions) - -## Learnings - -*To be updated as the team works.* - -### 2026-04-28T23:01:58.137+02:00 — PR #46 generated-output docs impact - -- PR #46 (`Fix real-world OpenAPI code generation`) materially changed generated public API shape: generated clients now expose a `Client` object, `Owned(T)`, `RawResponse`, `ApiResult(T)`, parse-error raw preservation, raw/result endpoint helpers, default headers, query encoding, resource wrappers, and SSE helpers. -- README snippets around CLI output and generated API usage are architecturally stale. Docs must show `Client.init(...)`/`deinit`, passing `*Client` into operations, `Owned(T).deinit()`, and the `{operation}`, `{operation}Raw`, `{operation}Result` trio instead of old per-call `std.http.Client` examples. -- The unified converter pattern remains the documentation spine: version-specific parsers/converters normalize into `src/models/common/document.zig`, then `src/generators/unified/model_generator.zig` and `src/generators/unified/api_generator.zig` emit all current output. -- Docs must not overclaim YAML support. Current behavior is JSON file/URL support; YAML extension handling reports unsupported. -- OpenAPI 3.1 composite schema typing is currently richer than v3.0/v3.2: `allOf` merge plus preserved `oneOf`/`anyOf`/discriminator metadata. Docs should describe this as current behavior without implying full parity across all spec versions. -- Key files for review: `README.md`, `docs/index.html`, `docs/json-value-typing-policy.md`, `docs/openai-generation-issues.md`, `src/cli.zig`, `src/generator.zig`, `src/generators/unified/api_generator.zig`, `src/generators/unified/model_generator.zig`, `generated/generated_v3.zig`, `generated/compile_generated.zig`. -- User preference from this task: use GPT-5.5 for team agents during this session only; commit docs work in small logical groups, but Lando must not stage or commit for this review task. - -### 2026-04-30T16:31:22.685+02:00 — Smoke Test Architecture Investigation - -**Scope:** Architectural proposal for integrating smoke tests into openapi2zig (spec generation + compilation verification across all 41 JSON specs). - -**Findings:** - -1. **Current Test Coverage Gap** - - Only 4 specs are code-generation tested: petstore.json (v2.0, v3.0), webhook-example.json (v3.1), petstore.json (v3.2) - - 37 additional JSON specs in openapi/ folders are never touched during CI - - YAML files (8 files) are unsupported by the converter—should be skipped gracefully - -2. **Build System Architecture** - - `build.zig` uses hardcoded `run-generate-*` steps for stable reference specs - - Generated code is tested via `generated/compile_generated.zig` test harness (imports 4 generated files, uses `std.testing.refAllDecls()` pattern) - - This is correct for baseline reproducibility, but not scalable to 41 specs - -3. **Reference Pattern from refitter** - - Separate PowerShell script (`test/smoke-tests.ps1`) for spec enumeration, generation, and compilation - - Sequential generation (one spec at a time) for clarity and error isolation - - Continue-all policy: collect all failures and summarize at end (not fail-fast) - - Pattern is cross-platform (runs on Windows, Linux, macOS via PowerShell Core) - -4. **Proposed Architecture: PowerShell Script (NOT build.zig)** - - Keep hardcoded `run-generate-*` steps in build.zig for determinism and caching - - Add `test/smoke-tests.ps1` for integration testing (dynamically discovers .json specs, skips .yaml) - - Script responsibility: enumerate → generate → verify compilation → collect failures → report summary - - Each spec is compiled in isolation via `zig build-obj` to catch codegen errors - - Clean integration point: smoke test runs *after* `zig build test` succeeds - -5. **Architecture Rationale: Separation of Concerns** - - **Zig build system** (deterministic, caching-friendly) handles unit tests and reference generation - - **PowerShell** (workflow orchestration) handles dynamic spec enumeration and batch integration testing - - **Decoupling** keeps build.zig focused on compilation; PowerShell handles discovery - - Scalable: adding new specs requires only dropping files in openapi/; no build.zig changes - -6. **Key Risks & Mitigations** - - **Risk: Generated code may not compile in isolation** → Mitigate: verify compilation independently per spec, fail cleanly on errors - - **Risk: YAML unsupported** → Mitigate: skip .yaml files, log as "skipped" - - **Risk: Performance (41 specs × compile time)** → Sequential generation is intentional; can parallelize later if needed - - **Risk: json-schema/ contains non-API specs** → Mitigate: filter to v2.0/, v3.0/, v3.1/, v3.2/ folders only - -7. **CI Integration Recommendation** - - Add optional `smoke-tests` job to `.github/workflows/ci.yml` that runs after core tests pass - - Runs on PR and main branch (can be made conditional on full build if too slow) - - Captures failures in artifact for visibility - -8. **Clarifying Questions for Christian** (documented in proposal) - - PowerShell on Linux CI (pwsh availability)? - - Failure policy confirmed (continue-all, not fail-fast)? - - File cleanup strategy (temp directory or in-memory)? - - json-schema/ exclusion? - - CI timing (every PR or only on main)? - -**Architectural Decision:** Smoke test belongs in PowerShell (`test/smoke-tests.ps1`), not build.zig. This follows refitter's proven pattern and respects the unified converter → generation pipeline without coupling integration testing to the build system. - -**Decision recorded:** `.squad/decisions/inbox/lando-smoke-test-plan.md` +## Project Context + +**Project:** openapi2zig — CLI tool for generating type-safe Zig API clients from OpenAPI/Swagger specs +**User:** Christian Helle +**Tech Stack:** Zig v0.15.2, OpenAPI v3.0, Swagger v2.0 +**Architecture:** Unified converter pattern (parse → normalize → generate) + +## Key Architecture + +- **Specs supported:** Swagger v2.0, OpenAPI v3.0 +- **Parsing:** Version-specific models in `src/models/v2.0/` and `src/models/v3.0/` +- **Converters:** `src/generators/converters/` transforms version-specific → unified document +- **Generation:** `src/generators/unified/` generates Zig code from unified representation +- **Memory model:** Every dynamic struct has `deinit(allocator)`. **Always use `defer parsed.deinit(allocator)`.** +- **Testing:** xUnit-style with `test_utils.createTestAllocator()`, test against both v2.0 and v3.0 + +## Known Patterns + +- Version detection happens in `src/detector.zig` by parsing JSON for `openapi` or `swagger` fields +- CLI routing in `src/cli.zig` and `src/generator.zig` +- Sample specs in `openapi/v2.0/petstore.json` and `openapi/v3.0/petstore.json` for testing +- Generated code output to `generated/main.zig` (test harness imports both versions) + +## Learnings + +*To be updated as the team works.* + +### 2026-04-28T23:01:58.137+02:00 — PR #46 generated-output docs impact + +- PR #46 (`Fix real-world OpenAPI code generation`) materially changed generated public API shape: generated clients now expose a `Client` object, `Owned(T)`, `RawResponse`, `ApiResult(T)`, parse-error raw preservation, raw/result endpoint helpers, default headers, query encoding, resource wrappers, and SSE helpers. +- README snippets around CLI output and generated API usage are architecturally stale. Docs must show `Client.init(...)`/`deinit`, passing `*Client` into operations, `Owned(T).deinit()`, and the `{operation}`, `{operation}Raw`, `{operation}Result` trio instead of old per-call `std.http.Client` examples. +- The unified converter pattern remains the documentation spine: version-specific parsers/converters normalize into `src/models/common/document.zig`, then `src/generators/unified/model_generator.zig` and `src/generators/unified/api_generator.zig` emit all current output. +- Docs must not overclaim YAML support. Current behavior is JSON file/URL support; YAML extension handling reports unsupported. +- OpenAPI 3.1 composite schema typing is currently richer than v3.0/v3.2: `allOf` merge plus preserved `oneOf`/`anyOf`/discriminator metadata. Docs should describe this as current behavior without implying full parity across all spec versions. +- Key files for review: `README.md`, `docs/index.html`, `docs/json-value-typing-policy.md`, `docs/openai-generation-issues.md`, `src/cli.zig`, `src/generator.zig`, `src/generators/unified/api_generator.zig`, `src/generators/unified/model_generator.zig`, `generated/generated_v3.zig`, `generated/compile_generated.zig`. +- User preference from this task: use GPT-5.5 for team agents during this session only; commit docs work in small logical groups, but Lando must not stage or commit for this review task. + +### 2026-04-30T16:31:22.685+02:00 — Smoke Test Architecture Investigation + +**Scope:** Architectural proposal for integrating smoke tests into openapi2zig (spec generation + compilation verification across all 41 JSON specs). + +**Findings:** + +1. **Current Test Coverage Gap** + - Only 4 specs are code-generation tested: petstore.json (v2.0, v3.0), webhook-example.json (v3.1), petstore.json (v3.2) + - 37 additional JSON specs in openapi/ folders are never touched during CI + - YAML files (8 files) are unsupported by the converter—should be skipped gracefully + +2. **Build System Architecture** + - `build.zig` uses hardcoded `run-generate-*` steps for stable reference specs + - Generated code is tested via `generated/compile_generated.zig` test harness (imports 4 generated files, uses `std.testing.refAllDecls()` pattern) + - This is correct for baseline reproducibility, but not scalable to 41 specs + +3. **Reference Pattern from refitter** + - Separate PowerShell script (`test/smoke-tests.ps1`) for spec enumeration, generation, and compilation + - Sequential generation (one spec at a time) for clarity and error isolation + - Continue-all policy: collect all failures and summarize at end (not fail-fast) + - Pattern is cross-platform (runs on Windows, Linux, macOS via PowerShell Core) + +4. **Proposed Architecture: PowerShell Script (NOT build.zig)** + - Keep hardcoded `run-generate-*` steps in build.zig for determinism and caching + - Add `test/smoke-tests.ps1` for integration testing (dynamically discovers .json specs, skips .yaml) + - Script responsibility: enumerate → generate → verify compilation → collect failures → report summary + - Each spec is compiled in isolation via `zig build-obj` to catch codegen errors + - Clean integration point: smoke test runs *after* `zig build test` succeeds + +5. **Architecture Rationale: Separation of Concerns** + - **Zig build system** (deterministic, caching-friendly) handles unit tests and reference generation + - **PowerShell** (workflow orchestration) handles dynamic spec enumeration and batch integration testing + - **Decoupling** keeps build.zig focused on compilation; PowerShell handles discovery + - Scalable: adding new specs requires only dropping files in openapi/; no build.zig changes + +6. **Key Risks & Mitigations** + - **Risk: Generated code may not compile in isolation** → Mitigate: verify compilation independently per spec, fail cleanly on errors + - **Risk: YAML unsupported** → Mitigate: skip .yaml files, log as "skipped" + - **Risk: Performance (41 specs × compile time)** → Sequential generation is intentional; can parallelize later if needed + - **Risk: json-schema/ contains non-API specs** → Mitigate: filter to v2.0/, v3.0/, v3.1/, v3.2/ folders only + +7. **CI Integration Recommendation** + - Add optional `smoke-tests` job to `.github/workflows/ci.yml` that runs after core tests pass + - Runs on PR and main branch (can be made conditional on full build if too slow) + - Captures failures in artifact for visibility + +8. **Clarifying Questions for Christian** (documented in proposal) + - PowerShell on Linux CI (pwsh availability)? + - Failure policy confirmed (continue-all, not fail-fast)? + - File cleanup strategy (temp directory or in-memory)? + - json-schema/ exclusion? + - CI timing (every PR or only on main)? + +**Architectural Decision:** Smoke test belongs in PowerShell (`test/smoke-tests.ps1`), not build.zig. This follows refitter's proven pattern and respects the unified converter → generation pipeline without coupling integration testing to the build system. + +**Decision recorded:** `.squad/decisions/inbox/lando-smoke-test-plan.md` ### 2026-04-30T16:31:22.685+02:00 — Smoke Test Implementation Review (APPROVE) @@ -98,19 +98,46 @@ - Implementation matches all four confirmed decisions (temp denylist, all 4 wrapper modes, keep petstore harness, output under test/output/). - Cross-platform handling is sound: repo-root via $PSCommandPath, $IsWindows for exe suffix, slash normalization after Resolve-Path -Relative, `shell: pwsh` in CI. - CI scoping is correct: appended to existing smoke-tests job; artifact upload guarded by if: failure(); petstore harness preserved. -- Known limitation (acknowledged in plan): zig test relies on Zig's lazy analysis; deeply unused decls may escape compile-check. Acceptable for v1; can harden later with explicit efAllDeclsRecursive wrapper if generator gaps slip through. -- Reminder for staging: `test/smoke-tests.ps1` is currently untracked; it must be added in the same commit as the CI change. - -## 2026-04-30 — Smoke-test commit strategy - -**Commit strategy learning:** -- Three logical groups is the right granularity for a feature like this: infrastructure (script + gitignore), CI wiring, documentation. Each group is independently reviewable and revertable. -- The `.squad/` state was already committed in a prior session (4812b92), so Group 4 was correctly skipped — always verify actual git status before assuming a group has changes. -- Untracked files (like `test/smoke-tests.ps1`) must be staged with `git add` explicitly; relying on `-a` would have mixed groups. Use per-file staging to keep groups clean. -- Commit messages benefit from the "what + why + scope" structure: first line is the imperative summary, body enumerates the specific behavioral changes, not just file names. -- The `Co-authored-by` trailer belongs on every commit regardless of group size; it is a project invariant, not optional. - +- Known limitation (acknowledged in plan): zig test relies on Zig's lazy analysis; deeply unused decls may escape compile-check. Acceptable for v1; can harden later with explicit +efAllDeclsRecursive wrapper if generator gaps slip through. +- Reminder for staging: `test/smoke-tests.ps1` is currently untracked; it must be added in the same commit as the CI change. + +## 2026-04-30 — Smoke-test commit strategy + +**Commit strategy learning:** +- Three logical groups is the right granularity for a feature like this: infrastructure (script + gitignore), CI wiring, documentation. Each group is independently reviewable and revertable. +- The `.squad/` state was already committed in a prior session (4812b92), so Group 4 was correctly skipped — always verify actual git status before assuming a group has changes. +- Untracked files (like `test/smoke-tests.ps1`) must be staged with `git add` explicitly; relying on `-a` would have mixed groups. Use per-file staging to keep groups clean. +- Commit messages benefit from the "what + why + scope" structure: first line is the imperative summary, body enumerates the specific behavioral changes, not just file names. +- The `Co-authored-by` trailer belongs on every commit regardless of group size; it is a project invariant, not optional. + ## 2026-04-30 — Smoke-test harness shipped - Designed/implemented/validated est/smoke-tests.ps1 (88 cases: 22 specs × 4 wrapper modes), CI job updated with failure-only artifact upload, README documented. - Initial denylist: ingram-micro.json (duplicate pub const emissions in unified model generator — follow-up backend work). -- Decision recorded in decisions.md (2026-04-30 entry). Session-scoped directive: agents use Claude Opus 4.7 for this session only. +- Decision recorded in decisions.md (2026-04-30 entry). Session-scoped directive: agents use Claude Opus 4.7 for this session only. + +### 2026-05-01T11:50:14.189+02:00 — YAML smoke-test scope review + +- Scope confirmation: YAML must be exercised in both smoke layers, but the architecture split stays the same — `test/smoke-tests.ps1` is the dynamic sweep, and `build.zig` + `generated/compile_generated.zig` + `generated/main.zig` remain the curated harness. +- Commit boundary pattern: safest order is (1) broad smoke script behavior, (2) curated harness wiring plus new checked-in generated YAML artifacts, (3) README updates after validation. +- Naming rule: smoke outputs need a source-format segment (`__json__` / `__yaml__`) because many fixtures exist as JSON/YAML siblings under the same version directory (`petstore`, `petstore-expanded`, `api-with-examples`, `webhook-example`, etc.). +- Harness risk: import aliases and generated filenames must stay unique; adding YAML fixtures beside `generated_v2.zig`, `generated_v3.zig`, `generated_v31.zig`, `generated_v32.zig` requires distinct names such as `generated_v2_yaml.zig`. +- Scope guardrail: do not invent a new v3.2 YAML sample just to force parity — `openapi/v3.2` currently only ships JSON canonical fixtures. +- Discovery guardrail: do not equate “has JSON sibling” with “is a valid YAML smoke candidate”; `openapi/v3.0/bot.paths.yaml` is YAML-only and should not be silently excluded by a sibling-only heuristic. +- Key review files for this decision: `test/smoke-tests.ps1`, `build.zig`, `generated/compile_generated.zig`, `generated/main.zig`, `README.md`, `openapi/v2.0/petstore.yaml`, `openapi/v3.0/petstore.yaml`, `openapi/v3.1/webhook-example.yaml`, `openapi/v3.0/bot.paths.yaml`. + +### 2026-05-01T11:50:14.189+02:00 — YAML smoke implementation verdict + +- Release verdict: approve as scoped. The implementation reaches both smoke layers, keeps the architecture split intact, and does not overreach into parser rewrites. +- The denylist is acceptable at release time because the skipped YAML cases are explicit, path-scoped, and described as parser/normalization gaps rather than being silently omitted from discovery. +- Commit boundaries were sound: broad sweep behavior, curated harness wiring with checked-in YAML artifacts, then README clarifications. +- `openapi/v3.2` remaining JSON-only is architecturally correct because the repository still lacks a canonical v3.2 YAML root fixture. + +### 2026-05-01T11:50:14.189+02:00 — Scribe closeout + +- Scribe recorded Lando's rule set for YAML smoke scope: both smoke layers stay in scope, the curated harness remains curated, and `openapi/v3.2` stays JSON-only until a real YAML root fixture exists. +- This closeout merged the scope decision into `decisions.md` and linked the follow-up review surface across `test/smoke-tests.ps1`, `build.zig`, `generated/compile_generated.zig`, `generated/main.zig`, and `README.md`. + +### 2026-05-01T09:50:14Z — Scribe closeout + +- Scribe recorded Lando's release approval: the curated-vs-broad smoke split holds, JSON/YAML sibling output collisions are prevented by format-aware names, and the `openapi/v3.2` boundary stays JSON-only until a real YAML root fixture exists. diff --git a/.squad/agents/scribe/history.md b/.squad/agents/scribe/history.md index 5ba3474..0be734d 100644 --- a/.squad/agents/scribe/history.md +++ b/.squad/agents/scribe/history.md @@ -27,3 +27,9 @@ Initial setup complete. - Designed/implemented/validated est/smoke-tests.ps1 (88 cases: 22 specs × 4 wrapper modes), CI job updated with failure-only artifact upload, README documented. - Initial denylist: ingram-micro.json (duplicate pub const emissions in unified model generator — follow-up backend work). - Decision recorded in decisions.md (2026-04-30 entry). Session-scoped directive: agents use Claude Opus 4.7 for this session only. + +## 2026-05-01T09:50:14Z — YAML smoke scribe closeout + +- Pre-check recorded `decisions.md` at 8263 bytes with 4 decision inbox files. +- Archive gate did not trigger; decisions stayed below 20480 bytes. +- Merged and deduplicated YAML smoke decisions, deleted processed inbox files, wrote orchestration/session logs, and checked the history summarization gate. diff --git a/.squad/agents/starkiller/history-archive.md b/.squad/agents/starkiller/history-archive.md new file mode 100644 index 0000000..d451745 --- /dev/null +++ b/.squad/agents/starkiller/history-archive.md @@ -0,0 +1,170 @@ +# Starkiller History Archive + +## Archived 2026-05-01T09:50:14Z + +### PR #46 documentation verification (2026-04-28T23:01:58.137+02:00) + +**Verification result:** README and docs now match the current generated runtime shape after a small correction. Static checks compared `README.md` and `docs/index.html` against `src/cli.zig`, `src/generator.zig`, `generated/generated_v2.zig`, `generated/generated_v3.zig`, `generated/generated_v31.zig`, `generated/generated_v32.zig`, `generated/compile_generated.zig`, and `generated/main.zig`. + +**Blocked validations:** `zig version`, `zig build run-generate`, `zig build test`, `zig test generated\compile_generated.zig`, and `zig build-exe generated\main.zig -fno-emit-bin` are blocked locally because `C:\Users\chris\AppData\Local\Microsoft\WinGet\Links\zig.exe` fails to start with "No application is associated with the specified file for this operation." + +**Docs fixed:** `README.md` had invalid Zig string literals in the generated-code calling example because `\n` escapes had become literal line breaks inside strings. `docs/index.html` now names `ParseErrorResponse` and describes raw/typed SSE plus OpenAI stream helpers consistently with the PR #46 acceptance criteria. + +### Input Loader Testing Strategy (2026-03-20) + +**Test File:** `src/tests/test_input_loader.zig` + +**Hybrid Testing Approach (Per Lando's Architecture):** +- **Unit tests:** Fast, deterministic, no network dependencies + - Mock/error scenarios with unreachable IPs (192.0.2.1 - RFC 5737) + - File path validation and error handling + - Memory leak detection on all code paths +- **Integration tests:** Validate real-world HTTP functionality + - Marked to skip in development via `SKIP_INTEGRATION_TESTS` env var + - Test against public OpenAPI endpoints (petstore3.swagger.io, petstore.swagger.io) + - Verify full pipeline: fetch → parse → convert → generate + +**Both-Spec Validation:** +- Every test covers BOTH v2.0 (Swagger) AND v3.0 (OpenAPI) +- File-based and URL-based loading tested for both versions +- Side-by-side comparison ensures consistency between file and URL sources +- Full pipeline tests: load → detect version → parse → validate structure + +**HTTP Client API (Zig 0.15.2):** +- Uses `std.http.Client.request()` API (not `.fetch()`) +- Pattern: `client.request()` → `sendBodiless()` → `receiveHead()` → `reader.readAlloc()` +- Proper error handling for: invalid URL, connection failure, 404, timeouts +- Memory ownership: caller owns returned body, must free with `allocator.free()` + +**Memory Management:** +- All tests use `test_utils.createTestAllocator()` for leak detection +- Comprehensive `defer` cleanup on all code paths +- Tests verify no leaks even on error paths +- Switch statements must handle `.Unsupported` version case + +**Edge Cases Covered:** +- Large file handling (api-with-examples.json ~6KB) +- Non-existent files and URLs +- Invalid URL schemes (ftp://, file://) +- Connection timeouts (unreachable IPs) +- HTTP 404 responses +- Malformed URLs +- Empty response bodies + +**Integration Test Design:** +- Tests skip via environment variable: `SKIP_INTEGRATION_TESTS=1` +- Real endpoints tested: + - `https://petstore3.swagger.io/api/v3/openapi.json` (v3.0) + - `https://petstore.swagger.io/v2/swagger.json` (v2.0) +- Verify network fetch produces same parseable structure as file load +- Full validation chain ensures end-to-end correctness + +**Test Organization:** +- Logical sections with clear comments +- Unit tests first (fast feedback) +- Integration tests last (skip in dev) +- Each test has descriptive name and focused scope +- Tests validate both positive and negative cases + +### Smoke Test Coverage Design (2026-04-30) + +**Task:** Design `test/smoke-tests.ps1` for PR verification — enumerate all specs under `openapi/`, generate code, compile-check each. + +**Key findings:** + +**Spec inventory:** 22 JSON specs are eligible: 8 in `v2.0/`, 10 in `v3.0/`, 2 in `v3.1/`, 2 in `v3.2/`. Total spec count across all YAML equivalents is ~40 files. + +**Skip rules (hard):** +- All `*.yaml` / `*.yml` — YAML returns `UnsupportedExtension` from `generator.zig`; would produce 100% false failures. +- `openapi/json-schema/**` — JSON Schema meta-documents, not OpenAPI/Swagger specs. + +**Compile check technique:** Generated files are library-style (no `main`). Use `zig build-lib -fno-emit-bin` — compiles as static lib, skips output emit, catches type/semantic errors. Does not require a `main` function. + +**CI integration note:** The existing `smoke-tests` job already exists in `.github/workflows/ci.yml` (lines 127–160) but only tests the petstore pair via `zig build run-generate`. The new script augments it — keep the petstore pair as a fast sanity check and add the PS script as a spec sweep step. `pwsh` is available on `ubuntu-latest` without extra setup. + +**Risk specs:** +- `v3.0/ingram-micro.json` (755 KB) — may expose large-spec codegen edge cases +- `v3.0/callback-example.json` — callbacks may not be fully supported +- `v3.0/link-example.json` — link objects may produce incomplete output + +**Open question for Christian:** start with a denylist (skip known-broken, keep CI green) or allowlist (only known-passing, add others as fixed)? This is the primary governance question. + +**Proposal written:** `.squad/decisions/inbox/starkiller-smoke-test-plan.md` + +### Smoke Test Validation — Fenster's `test/smoke-tests.ps1` (2026-04-30) + +**Outcome: PASS** (script behavior). Generator gap surfaced on `openapi/v3.0/ingram-micro.json` is a separate triage item, not a script defect. + +**Static review against the approved plan — every requirement met:** +- Repo root resolution: uses `$PSCommandPath` → `Split-Path -Parent` → `Resolve-Path "$ScriptDir/.."` and `Set-Location $RepoRoot`, so it runs correctly regardless of caller's CWD (CI uses `working-directory: test`). +- Zig version check: `Find-Zig` resolves `Get-Command zig`, prints `zig version` before build; missing zig → red error + `exit 1`. +- ReleaseFast build: default `-Optimize ReleaseFast`, invoked as `zig build -Doptimize=$Optimize`, build failure → `exit 1`. +- Spec discovery: enumerates only `openapi/v2.0`, `v3.0`, `v3.1`, `v3.2`. `json-schema/` correctly excluded by not being in `$IncludedDirs`. YAML is filtered via lowercased extension check and counted into `$SkippedYaml` for the summary. +- Sequential matrix: nested `foreach ($spec) { foreach ($mode) }`, indices `[idx/total]`, colored case labels. +- Output dir: `$OutputDir = test/output`, wiped unless `-KeepOutput`, parent directories auto-created per (version, base, mode). +- Compile check: `zig test ` — correct technique because generated files are library modules without `main()`. (Cleaner than `build-lib -fno-emit-bin`; `zig test` reports "All 0 tests passed" on success.) +- Continue-all summary: per-case results stored in `$results`, summary prints Pass/Fail/Skip/Total + a failing-cases list grouped by phase (`generate`/`compile`). +- Denylist: `Test-Denylisted` with `Spec` (path or `*`) × `Mode` (name or `*`) × `Reason`; intentionally empty with explanatory comment. +- Non-zero exit: `exit 1` iff any non-denylisted case fails. + +**PowerShell parse check:** `[System.Management.Automation.Language.Parser]::ParseFile()` → no errors. + +**Focused run** (`-Filter "petstore" -Modes paths`, 9 cases): 9 PASS / 0 FAIL, exit 0. + +**Full sweep** (`pwsh test/smoke-tests.ps1`, 88 cases = 22 specs × 4 modes, ~5m14s on Windows + Zig 0.16.0): +- 84 PASS / 4 FAIL / 0 SKIP, exit 1. +- All 4 failures are `openapi/v3.0/ingram-micro.json` × {none,tags,paths,hybrid}, all in the `compile` phase. +- Root cause is generator-side: emits multiple `pub const = struct { ... }` declarations in the same Zig file (duplicate struct member name errors). Names like `QuoteDetailsRequestQuoteProductsRequestRetrieveQuoteProductsRequest` collide because the inline-schema name composer produces the same identifier from different schema sites. This is the exact "huge spec" risk I called out in the pre-plan inventory. +- Same pattern across all four resource-wrapper modes → bug is in the unified model generator's name-uniquing, not the API/wrapper layer. + +**Governance call (open question for Lando + Christian):** the script is correct and the bug is real, so CI will go red on this PR. Three options: (a) Fenster fixes the duplicate-name path in `src/generators/unified/model_generator.zig` before merge; (b) populate `$Denylist` with the four ingram-micro entries, link a tracking issue, ship the script; (c) restrict full sweep to a curated allowlist for now. Recommendation: (b) — the smoke harness is valuable signal *now*, and the duplicate-name fix is its own focused PR. + +**CI integration (`.github/workflows/ci.yml`):** +- Workflow file is `./smoke-tests.ps1` with `working-directory: test` and `shell: pwsh` → resolves to `test/smoke-tests.ps1` ✓. +- `pwsh` is preinstalled on `ubuntu-latest` GitHub runners ✓. +- Linux binary path: script uses `$IsWindows` to pick `openapi2zig` vs `openapi2zig.exe` ✓. +- `actions/upload-artifact@v4` path `test/output/` matches `$OutputDir` ✓; `if: failure()` only — good, keeps green runs lean. +- Job depends on `lint-and-format` and is gated on PR or `main`, alongside the existing `zig build run-generate` + `zig run generated/main.zig` petstore harness — that's the right layering: fast petstore sanity first, broad sweep second. +- `.gitignore` correctly adds `test/output/`. +- README "Smoke tests" section accurately describes the pwsh invocation and CI behavior. + +**No script defects found.** Validation artifacts (`test/output/`, `test/smoke-full.log`) cleaned up. + +### Smoke Test Final Gate — Post-Denylist (2026-04-30T16:31:22+02:00) + +**Outcome: PASS.** Final validation gate after Fenster added the `ingram-micro` denylist entry. + +**Denylist scoping check (static, `test/smoke-tests.ps1` lines 67–84):** +- Single entry: `Spec="openapi/v3.0/ingram-micro.json"` (literal, slash-normalized) × `Mode="*"` × clear reason. +- `Test-Denylisted` requires both `specMatch` AND `modeMatch`; literal spec string only matches that exact relpath, so other v3.0 specs are unaffected. `Mode="*"` correctly fans out across `none|tags|paths|hybrid`. +- Reason string includes "(all wrapper modes)" — explicit and audit-friendly. +- Comment block above the entry explains the generator gap and merge gate intent. + +**Focused runs (Windows + Zig 0.16.0, ReleaseFast):** +- `pwsh -NoProfile -File test/smoke-tests.ps1 -Filter "v3.0/ingram-micro.json"` → 0 pass / 0 fail / 4 skip, **exit 0**. All four modes correctly skipped with denylist reason. +- `pwsh -NoProfile -File test/smoke-tests.ps1 -Filter "v3.0/petstore.json" -Modes paths` → 1 pass / 0 fail / 0 skip, **exit 0**. Denylist does not bleed into unrelated specs. + +**Docs/CI alignment:** +- `.github/workflows/ci.yml` smoke-tests job (lines 127–172) unchanged and still correct: invokes `./smoke-tests.ps1` from `test/`, uploads `test/output/` on failure only. +- `README.md` (lines 177–194) already documents the denylist mechanism: "Honors a temporary denylist for known-unsupported spec/mode combinations so the PR gate can stay green while generator gaps are tracked explicitly." No doc drift. +- `.gitignore` still covers `test/output/`. + +**No script defects, no doc drift, no CI drift.** Validation artifacts cleaned up. + +**Follow-up (separate work, not blocking this gate):** the duplicate `pub const` emission in `src/generators/unified/model_generator.zig` for `openapi/v3.0/ingram-micro.json` remains a real generator bug. Recommend a tracking issue so the denylist entry has a clear retire-when condition; until then the entry self-documents the gap. + +### YAML smoke coverage review (2026-05-01T11:50:14.189+02:00) + +**Smoke script scope:** `test/smoke-tests.ps1` now discovers both JSON and YAML specs and writes outputs as `____.zig`, which is required because 15 fixture basenames exist in both JSON and YAML form across `openapi/v2.0`, `openapi/v3.0`, and `openapi/v3.1`. + +**Validation results:** Focused YAML generation/compile passed for `openapi/v2.0/petstore.yaml` and `openapi/v3.0/petstore.yaml`, but the broader petstore-family smoke sweep still failed on `openapi/v2.0/petstore-expanded.yaml` and `openapi/v3.0/petstore-expanded.yaml` with `error.ParseFailure` during generation. YAML smoke coverage is therefore not ready to call green yet. + +**Harness caveat:** `generated/main.zig` still acts as a compile/init smoke harness more than a deterministic runtime proof. In local validation it printed `Generated models build and run !!` and initialized the YAML clients, but the live Swagger v2 call returned `error.ResponseError` and the program returned early without surfacing a non-zero failure, so broad smoke script results are the stronger acceptance signal for this feature. + +**Required completeness note:** `generated/compile_generated.zig` and `generated/main.zig` now import `generated_v2_yaml.zig`, `generated_v3_yaml.zig`, and `generated_v31_yaml.zig`, so those generated fixtures must be committed or produced before `zig build test` on a clean checkout. + +**Explicit risk checks from Lando's guidance:** JSON/YAML sibling collision protection is present in `test/smoke-tests.ps1` via `____.zig`, so sibling fixtures like `petstore.json` / `petstore.yaml` no longer overwrite each other. Generated harness aliases are also distinct (`v2`, `v2_yaml`, `v3`, `v3_yaml`, `v31`, `v31_yaml`, `v32`), so there is no alias shadowing in `generated/compile_generated.zig` or `generated/main.zig`. + +**YAML-only root coverage:** Smoke discovery does not require a JSON twin; it includes any `.json`, `.yaml`, or `.yml` under the allowed version roots. Confirmed with `openapi/v3.0/bot.paths.yaml`, which is discovered as a single YAML case and currently reports `SKIP` only because it is explicitly denylisted for a known YAML normalization `ParseFailure`, not because discovery dropped it. + +*To be updated as the team works.* diff --git a/.squad/agents/starkiller/history.md b/.squad/agents/starkiller/history.md index fe2eee8..8ee8da3 100644 --- a/.squad/agents/starkiller/history.md +++ b/.squad/agents/starkiller/history.md @@ -22,160 +22,31 @@ ## Learnings -### PR #46 documentation verification (2026-04-28T23:01:58.137+02:00) +### Historical summary (archived 2026-05-01T09:50:14Z) -**Verification result:** README and docs now match the current generated runtime shape after a small correction. Static checks compared `README.md` and `docs/index.html` against `src/cli.zig`, `src/generator.zig`, `generated/generated_v2.zig`, `generated/generated_v3.zig`, `generated/generated_v31.zig`, `generated/generated_v32.zig`, `generated/compile_generated.zig`, and `generated/main.zig`. +- **PR #46 documentation verification (2026-04-28T23:01:58.137+02:00):** README and docs now match the current generated runtime shape after a small correction. Static checks compared `README.md` and `docs/index.html` against `src/cli.zig`, `src/generator.zig`, `generated/generated_v2.zig`, `generated/generated_v3.zig`, `generated/generated_v31.zig`, `generated/generated_v32.zig`, `generated/compile_generated.zig`, and `generated/main.zig`. +- **Input Loader Testing Strategy (2026-03-20):** `src/tests/test_input_loader.zig` +- **Smoke Test Coverage Design (2026-04-30):** Design `test/smoke-tests.ps1` for PR verification — enumerate all specs under `openapi/`, generate code, compile-check each. +- **Smoke Test Validation — Fenster's `test/smoke-tests.ps1` (2026-04-30):** (script behavior). Generator gap surfaced on `openapi/v3.0/ingram-micro.json` is a separate triage item, not a script defect. +- **Smoke Test Final Gate — Post-Denylist (2026-04-30T16:31:22+02:00):** Final validation gate after Fenster added the `ingram-micro` denylist entry. +- **YAML smoke coverage review (2026-05-01T11:50:14.189+02:00):** `test/smoke-tests.ps1` now discovers both JSON and YAML specs and writes outputs as `____.zig`, which is required because 15 fixture basenames exist in both JSON and YAML form across `openapi/v2.0`, `openapi/v3.0`, and `openapi/v3.1`. -**Blocked validations:** `zig version`, `zig build run-generate`, `zig build test`, `zig test generated\compile_generated.zig`, and `zig build-exe generated\main.zig -fno-emit-bin` are blocked locally because `C:\Users\chris\AppData\Local\Microsoft\WinGet\Links\zig.exe` fails to start with "No application is associated with the specified file for this operation." - -**Docs fixed:** `README.md` had invalid Zig string literals in the generated-code calling example because `\n` escapes had become literal line breaks inside strings. `docs/index.html` now names `ParseErrorResponse` and describes raw/typed SSE plus OpenAI stream helpers consistently with the PR #46 acceptance criteria. - -### Input Loader Testing Strategy (2026-03-20) - -**Test File:** `src/tests/test_input_loader.zig` - -**Hybrid Testing Approach (Per Lando's Architecture):** -- **Unit tests:** Fast, deterministic, no network dependencies - - Mock/error scenarios with unreachable IPs (192.0.2.1 - RFC 5737) - - File path validation and error handling - - Memory leak detection on all code paths -- **Integration tests:** Validate real-world HTTP functionality - - Marked to skip in development via `SKIP_INTEGRATION_TESTS` env var - - Test against public OpenAPI endpoints (petstore3.swagger.io, petstore.swagger.io) - - Verify full pipeline: fetch → parse → convert → generate - -**Both-Spec Validation:** -- Every test covers BOTH v2.0 (Swagger) AND v3.0 (OpenAPI) -- File-based and URL-based loading tested for both versions -- Side-by-side comparison ensures consistency between file and URL sources -- Full pipeline tests: load → detect version → parse → validate structure - -**HTTP Client API (Zig 0.15.2):** -- Uses `std.http.Client.request()` API (not `.fetch()`) -- Pattern: `client.request()` → `sendBodiless()` → `receiveHead()` → `reader.readAlloc()` -- Proper error handling for: invalid URL, connection failure, 404, timeouts -- Memory ownership: caller owns returned body, must free with `allocator.free()` - -**Memory Management:** -- All tests use `test_utils.createTestAllocator()` for leak detection -- Comprehensive `defer` cleanup on all code paths -- Tests verify no leaks even on error paths -- Switch statements must handle `.Unsupported` version case - -**Edge Cases Covered:** -- Large file handling (api-with-examples.json ~6KB) -- Non-existent files and URLs -- Invalid URL schemes (ftp://, file://) -- Connection timeouts (unreachable IPs) -- HTTP 404 responses -- Malformed URLs -- Empty response bodies - -**Integration Test Design:** -- Tests skip via environment variable: `SKIP_INTEGRATION_TESTS=1` -- Real endpoints tested: - - `https://petstore3.swagger.io/api/v3/openapi.json` (v3.0) - - `https://petstore.swagger.io/v2/swagger.json` (v2.0) -- Verify network fetch produces same parseable structure as file load -- Full validation chain ensures end-to-end correctness - -**Test Organization:** -- Logical sections with clear comments -- Unit tests first (fast feedback) -- Integration tests last (skip in dev) -- Each test has descriptive name and focused scope -- Tests validate both positive and negative cases - -### Smoke Test Coverage Design (2026-04-30) - -**Task:** Design `test/smoke-tests.ps1` for PR verification — enumerate all specs under `openapi/`, generate code, compile-check each. - -**Key findings:** - -**Spec inventory:** 22 JSON specs are eligible: 8 in `v2.0/`, 10 in `v3.0/`, 2 in `v3.1/`, 2 in `v3.2/`. Total spec count across all YAML equivalents is ~40 files. - -**Skip rules (hard):** -- All `*.yaml` / `*.yml` — YAML returns `UnsupportedExtension` from `generator.zig`; would produce 100% false failures. -- `openapi/json-schema/**` — JSON Schema meta-documents, not OpenAPI/Swagger specs. - -**Compile check technique:** Generated files are library-style (no `main`). Use `zig build-lib -fno-emit-bin` — compiles as static lib, skips output emit, catches type/semantic errors. Does not require a `main` function. - -**CI integration note:** The existing `smoke-tests` job already exists in `.github/workflows/ci.yml` (lines 127–160) but only tests the petstore pair via `zig build run-generate`. The new script augments it — keep the petstore pair as a fast sanity check and add the PS script as a spec sweep step. `pwsh` is available on `ubuntu-latest` without extra setup. - -**Risk specs:** -- `v3.0/ingram-micro.json` (755 KB) — may expose large-spec codegen edge cases -- `v3.0/callback-example.json` — callbacks may not be fully supported -- `v3.0/link-example.json` — link objects may produce incomplete output - -**Open question for Christian:** start with a denylist (skip known-broken, keep CI green) or allowlist (only known-passing, add others as fixed)? This is the primary governance question. - -**Proposal written:** `.squad/decisions/inbox/starkiller-smoke-test-plan.md` - -### Smoke Test Validation — Fenster's `test/smoke-tests.ps1` (2026-04-30) - -**Outcome: PASS** (script behavior). Generator gap surfaced on `openapi/v3.0/ingram-micro.json` is a separate triage item, not a script defect. - -**Static review against the approved plan — every requirement met:** -- Repo root resolution: uses `$PSCommandPath` → `Split-Path -Parent` → `Resolve-Path "$ScriptDir/.."` and `Set-Location $RepoRoot`, so it runs correctly regardless of caller's CWD (CI uses `working-directory: test`). -- Zig version check: `Find-Zig` resolves `Get-Command zig`, prints `zig version` before build; missing zig → red error + `exit 1`. -- ReleaseFast build: default `-Optimize ReleaseFast`, invoked as `zig build -Doptimize=$Optimize`, build failure → `exit 1`. -- Spec discovery: enumerates only `openapi/v2.0`, `v3.0`, `v3.1`, `v3.2`. `json-schema/` correctly excluded by not being in `$IncludedDirs`. YAML is filtered via lowercased extension check and counted into `$SkippedYaml` for the summary. -- Sequential matrix: nested `foreach ($spec) { foreach ($mode) }`, indices `[idx/total]`, colored case labels. -- Output dir: `$OutputDir = test/output`, wiped unless `-KeepOutput`, parent directories auto-created per (version, base, mode). -- Compile check: `zig test ` — correct technique because generated files are library modules without `main()`. (Cleaner than `build-lib -fno-emit-bin`; `zig test` reports "All 0 tests passed" on success.) -- Continue-all summary: per-case results stored in `$results`, summary prints Pass/Fail/Skip/Total + a failing-cases list grouped by phase (`generate`/`compile`). -- Denylist: `Test-Denylisted` with `Spec` (path or `*`) × `Mode` (name or `*`) × `Reason`; intentionally empty with explanatory comment. -- Non-zero exit: `exit 1` iff any non-denylisted case fails. - -**PowerShell parse check:** `[System.Management.Automation.Language.Parser]::ParseFile()` → no errors. - -**Focused run** (`-Filter "petstore" -Modes paths`, 9 cases): 9 PASS / 0 FAIL, exit 0. - -**Full sweep** (`pwsh test/smoke-tests.ps1`, 88 cases = 22 specs × 4 modes, ~5m14s on Windows + Zig 0.16.0): -- 84 PASS / 4 FAIL / 0 SKIP, exit 1. -- All 4 failures are `openapi/v3.0/ingram-micro.json` × {none,tags,paths,hybrid}, all in the `compile` phase. -- Root cause is generator-side: emits multiple `pub const = struct { ... }` declarations in the same Zig file (duplicate struct member name errors). Names like `QuoteDetailsRequestQuoteProductsRequestRetrieveQuoteProductsRequest` collide because the inline-schema name composer produces the same identifier from different schema sites. This is the exact "huge spec" risk I called out in the pre-plan inventory. -- Same pattern across all four resource-wrapper modes → bug is in the unified model generator's name-uniquing, not the API/wrapper layer. - -**Governance call (open question for Lando + Christian):** the script is correct and the bug is real, so CI will go red on this PR. Three options: (a) Fenster fixes the duplicate-name path in `src/generators/unified/model_generator.zig` before merge; (b) populate `$Denylist` with the four ingram-micro entries, link a tracking issue, ship the script; (c) restrict full sweep to a curated allowlist for now. Recommendation: (b) — the smoke harness is valuable signal *now*, and the duplicate-name fix is its own focused PR. - -**CI integration (`.github/workflows/ci.yml`):** -- Workflow file is `./smoke-tests.ps1` with `working-directory: test` and `shell: pwsh` → resolves to `test/smoke-tests.ps1` ✓. -- `pwsh` is preinstalled on `ubuntu-latest` GitHub runners ✓. -- Linux binary path: script uses `$IsWindows` to pick `openapi2zig` vs `openapi2zig.exe` ✓. -- `actions/upload-artifact@v4` path `test/output/` matches `$OutputDir` ✓; `if: failure()` only — good, keeps green runs lean. -- Job depends on `lint-and-format` and is gated on PR or `main`, alongside the existing `zig build run-generate` + `zig run generated/main.zig` petstore harness — that's the right layering: fast petstore sanity first, broad sweep second. -- `.gitignore` correctly adds `test/output/`. -- README "Smoke tests" section accurately describes the pwsh invocation and CI behavior. - -**No script defects found.** Validation artifacts (`test/output/`, `test/smoke-full.log`) cleaned up. - -### Smoke Test Final Gate — Post-Denylist (2026-04-30T16:31:22+02:00) - -**Outcome: PASS.** Final validation gate after Fenster added the `ingram-micro` denylist entry. - -**Denylist scoping check (static, `test/smoke-tests.ps1` lines 67–84):** -- Single entry: `Spec="openapi/v3.0/ingram-micro.json"` (literal, slash-normalized) × `Mode="*"` × clear reason. -- `Test-Denylisted` requires both `specMatch` AND `modeMatch`; literal spec string only matches that exact relpath, so other v3.0 specs are unaffected. `Mode="*"` correctly fans out across `none|tags|paths|hybrid`. -- Reason string includes "(all wrapper modes)" — explicit and audit-friendly. -- Comment block above the entry explains the generator gap and merge gate intent. +## 2026-04-30 — Smoke-test harness shipped +- Designed/implemented/validated est/smoke-tests.ps1 (88 cases: 22 specs × 4 wrapper modes), CI job updated with failure-only artifact upload, README documented. +- Initial denylist: ingram-micro.json (duplicate pub const emissions in unified model generator — follow-up backend work). +- Decision recorded in decisions.md (2026-04-30 entry). Session-scoped directive: agents use Claude Opus 4.7 for this session only. -**Focused runs (Windows + Zig 0.16.0, ReleaseFast):** -- `pwsh -NoProfile -File test/smoke-tests.ps1 -Filter "v3.0/ingram-micro.json"` → 0 pass / 0 fail / 4 skip, **exit 0**. All four modes correctly skipped with denylist reason. -- `pwsh -NoProfile -File test/smoke-tests.ps1 -Filter "v3.0/petstore.json" -Modes paths` → 1 pass / 0 fail / 0 skip, **exit 0**. Denylist does not bleed into unrelated specs. +### 2026-05-01T11:50:14.189+02:00 — Scribe closeout -**Docs/CI alignment:** -- `.github/workflows/ci.yml` smoke-tests job (lines 127–172) unchanged and still correct: invokes `./smoke-tests.ps1` from `test/`, uploads `test/output/` on failure only. -- `README.md` (lines 177–194) already documents the denylist mechanism: "Honors a temporary denylist for known-unsupported spec/mode combinations so the PR gate can stay green while generator gaps are tracked explicitly." No doc drift. -- `.gitignore` still covers `test/output/`. +- Scribe recorded Starkiller's final gate: JSON/YAML naming and generated-import collision protections are in place, but broad YAML smoke still needs normalization/denylist work before sign-off. +- The closeout preserves the testing rule that `generated/main.zig` is only a compile/init sanity check; the PowerShell sweep remains the authoritative YAML acceptance gate. -**No script defects, no doc drift, no CI drift.** Validation artifacts cleaned up. +### 2026-05-01T11:50:14.189+02:00 — Reviewer verdict on YAML smoke scope -**Follow-up (separate work, not blocking this gate):** the duplicate `pub const` emission in `src/generators/unified/model_generator.zig` for `openapi/v3.0/ingram-micro.json` remains a real generator bug. Recommend a tracking issue so the denylist entry has a clear retire-when condition; until then the entry self-documents the gap. +- Verdict for the user request **"Update smoke tests to also test YAML files"**: **approve**. +- Reason: YAML is now exercised in both smoke layers, curated YAML artifacts for v2/v3/v3.1 are checked in, `v3.2` correctly stays JSON-only, collision-safe smoke filenames prevent JSON/YAML overwrites, alias names stay distinct in generated harness imports, and smoke discovery still includes YAML-only roots such as `openapi/v3.0/bot.paths.yaml`. +- Remaining denylisted YAML parser/normalization gaps are acceptable for this request because they are explicitly tracked as unsupported smoke cases rather than silently skipped, and the reported green validation (`14 pass / 2 skip / 0 fail` focused, `128 pass / 28 skip / 0 fail` full) matches the scoped acceptance criteria. -*To be updated as the team works.* +### 2026-05-01T09:50:14Z — Scribe closeout -## 2026-04-30 — Smoke-test harness shipped -- Designed/implemented/validated est/smoke-tests.ps1 (88 cases: 22 specs × 4 wrapper modes), CI job updated with failure-only artifact upload, README documented. -- Initial denylist: ingram-micro.json (duplicate pub const emissions in unified model generator — follow-up backend work). -- Decision recorded in decisions.md (2026-04-30 entry). Session-scoped directive: agents use Claude Opus 4.7 for this session only. +- Scribe recorded Starkiller's acceptance gate: YAML is exercised in both smoke layers, `openapi/v3.2` remains JSON-only, and the remaining denylisted YAML parser/normalization gaps are follow-up backend work rather than blockers for this scoped smoke-coverage release. diff --git a/.squad/decisions.md b/.squad/decisions.md index dd8414d..2616b69 100644 --- a/.squad/decisions.md +++ b/.squad/decisions.md @@ -46,3 +46,15 @@ - **Follow-up (out of scope):** Track and fix the unified model generator's duplicate `pub const` emission for shared/nested schemas; remove the ingram-micro denylist entry in the same PR. - **Session-scoped directive (2026-04-30T16:31:22.685+02:00, Christian Helle via Copilot):** All agents use Claude Opus 4.7 for the rest of this session only. - **Sources merged:** lando-smoke-test-plan.md, starkiller-smoke-test-plan.md, juno-smoke-test-plan.md, juno-smoke-ci-docs.md, fenster-smoke-test-plan.md, fenster-smoke-implementation.md, fenster-smoke-denylist-ingram-micro.md, starkiller-smoke-validation.md, copilot-directive-2026-04-30T16-31-22-685+02-00.md. + +### 2026-05-01: YAML smoke coverage scope and acceptance gate + +- **Decision:** YAML smoke coverage must exist in both current smoke layers while preserving the split: `test/smoke-tests.ps1` remains the broad dynamic sweep, and `build.zig` + `generated/compile_generated.zig` + `generated/main.zig` remain the curated harness. +- **Broad sweep contract:** Discover `.json`, `.yaml`, and `.yml` fixtures under `openapi/v2.0`, `openapi/v3.0`, `openapi/v3.1`, and `openapi/v3.2` without requiring JSON/YAML sibling pairs; keep YAML-only roots such as `openapi/v3.0/bot.paths.yaml`; write smoke outputs as `____.zig` to prevent sibling collisions. +- **Curated harness contract:** Check in YAML-generated harness artifacts only for real fixture roots that exist today (`generated_v2_yaml.zig`, `generated_v3_yaml.zig`, `generated_v31_yaml.zig`); `openapi/v3.2` remains JSON-only until the repo has a real YAML fixture. +- **Validation gate:** Do not call YAML smoke coverage complete until the broad JSON+YAML PowerShell sweep is green; the curated `zig build run-generate` + `zig run generated/main.zig` path is a compile/init sanity check only. +- **Temporary failure policy:** When YAML failures are deterministic in the pre-wrapper parse/normalization phase, track them as explicit broad-smoke denylist entries with `Mode = "*"` rather than hiding them behind JSON-only rules. +- **Documentation contract:** README smoke docs must distinguish the broad sweep from the curated harness and state that `v3.2` is currently JSON-only. +- **Release gate:** Lando and Starkiller approved release as scoped: YAML is exercised in both smoke layers, and remaining denylisted YAML parser/normalization gaps stay explicit follow-up backend work rather than blocking this smoke-coverage expansion. +- **Session-scoped directive:** Use GPT-5.5 for the rest of this session only. +- **Sources merged:** `copilot-directive-2026-05-01T11-50-14.md`, `fenster-yaml-smoke.md`, `juno-yaml-smoke-docs.md`, `lando-yaml-smoke-verdict.md`, `starkiller-yaml-validation.md`. diff --git a/.squad/identity/now.md b/.squad/identity/now.md index cea4fb3..a0c0429 100644 --- a/.squad/identity/now.md +++ b/.squad/identity/now.md @@ -1,9 +1,9 @@ --- -updated_at: 2026-03-20T15:07:45.433Z -focus_area: Initial setup +updated_at: 2026-05-01T11:50:14.189+02:00 +focus_area: Implement YAML smoke-test coverage active_issues: [] --- # What We're Focused On -Getting started. Updated by coordinator at session start. +Implementing the saved plan to add YAML coverage to both smoke-test layers, then validating and documenting the result. diff --git a/.squad/log/2026-05-01T09-50-14Z-yaml-smoke-closeout.md b/.squad/log/2026-05-01T09-50-14Z-yaml-smoke-closeout.md new file mode 100644 index 0000000..524189e --- /dev/null +++ b/.squad/log/2026-05-01T09-50-14Z-yaml-smoke-closeout.md @@ -0,0 +1,20 @@ +## 2026-05-01T09:50:14Z — YAML smoke coordination closeout + +**Requested by:** Christian Helle +**Scribe:** Session ledger and archive maintenance for spawned YAML smoke work. + +### Pre-check +- decisions.md before merge: 6444 bytes. +- Inbox files before merge: 5. +- Archive gate: not triggered; size was below 20480 bytes. + +### Actions +- Merged and deduplicated decision inbox into decisions.md. +- Deleted processed inbox files: 5. +- Wrote orchestration logs for Lando, Starkiller, and Juno. +- Appended cross-agent closeout notes to Lando, Fenster, Juno, and Starkiller histories. + +### Health report +- decisions.md after merge: 8263 bytes. +- Inbox files processed: 5. +- History files summarized: 1 (.squad/agents/starkiller/history.md). diff --git a/.squad/orchestration-log/2026-05-01T09-50-14Z-juno.md b/.squad/orchestration-log/2026-05-01T09-50-14Z-juno.md new file mode 100644 index 0000000..fb5b1d0 --- /dev/null +++ b/.squad/orchestration-log/2026-05-01T09-50-14Z-juno.md @@ -0,0 +1,13 @@ +## 2026-05-01T09:50:14Z — Juno: YAML docs closeout + +**Agent:** Juno +**Requested by:** Christian Helle +**Scribe action:** Logged spawned-agent outcome and synchronized `.squad/` records. + +### Outcome +- Updated README coverage language so broad JSON+YAML smoke is documented accurately while the curated harness stays selective. +- Kept `v3.2` JSON-only in the docs and recorded doc commits `de53aaa` and `8e87cbe`. + +### Status +- Decision inbox merged into `decisions.md`. +- Agent history updated for cross-team continuity. diff --git a/.squad/orchestration-log/2026-05-01T09-50-14Z-lando.md b/.squad/orchestration-log/2026-05-01T09-50-14Z-lando.md new file mode 100644 index 0000000..7a87aa8 --- /dev/null +++ b/.squad/orchestration-log/2026-05-01T09-50-14Z-lando.md @@ -0,0 +1,7 @@ +# Orchestration Log + +- **Timestamp (UTC):** 2026-05-01T09:50:14Z +- **Agent:** Lando +- **Role:** Lead / Architect +- **Summary:** Approved release as scoped. +- **Key details:** Confirmed the curated-vs-broad smoke split, collision handling, and the `openapi/v3.2` JSON-only boundary are sound. diff --git a/.squad/orchestration-log/2026-05-01T09-50-14Z-starkiller.md b/.squad/orchestration-log/2026-05-01T09-50-14Z-starkiller.md new file mode 100644 index 0000000..6f17343 --- /dev/null +++ b/.squad/orchestration-log/2026-05-01T09-50-14Z-starkiller.md @@ -0,0 +1,7 @@ +# Orchestration Log + +- **Timestamp (UTC):** 2026-05-01T09:50:14Z +- **Agent:** Starkiller +- **Role:** Tester +- **Summary:** Approved the scoped YAML smoke request. +- **Key details:** Confirmed YAML is exercised in both smoke layers and that remaining denylisted parser/normalization gaps are follow-up backend work, not blockers for this smoke-coverage change. diff --git a/.squad/skills/project-conventions/SKILL.md b/.squad/skills/project-conventions/SKILL.md index 48a1861..0f9d17f 100644 --- a/.squad/skills/project-conventions/SKILL.md +++ b/.squad/skills/project-conventions/SKILL.md @@ -12,6 +12,10 @@ source: "template" ## Patterns +### Smoke-test documentation split + +When documenting verification, describe the broad `pwsh test/smoke-tests.ps1` sweep separately from the curated `zig build run-generate*` sample targets. Mirror the exact artifact names each layer produces, and call out source-format suffixes when JSON/YAML sibling fixtures would otherwise collide. + ### [Pattern Name] Describe a key convention or practice used in this codebase. Be specific about what to do and why. diff --git a/.squad/skills/smoke-harness-scope/SKILL.md b/.squad/skills/smoke-harness-scope/SKILL.md new file mode 100644 index 0000000..cd2b3fe --- /dev/null +++ b/.squad/skills/smoke-harness-scope/SKILL.md @@ -0,0 +1,60 @@ +--- +name: "smoke-harness-scope" +description: "How to extend smoke coverage without collapsing dynamic sweeps into the curated generated harness" +domain: "testing-architecture" +confidence: "high" +source: "lando review" +--- + +## Context + +openapi2zig uses two smoke layers on purpose: + +1. `test/smoke-tests.ps1` for broad, dynamically discovered fixture coverage +2. `build.zig` + `generated/*` for a small, stable curated harness used by normal build/test flows + +When expanding coverage (for example, JSON → YAML), keep both layers, but do not merge their responsibilities. + +## Patterns + +### Keep the two smoke layers separate + +- Put discovery, per-spec iteration, denylist handling, and matrix execution in `test/smoke-tests.ps1` +- Keep `build.zig` limited to a curated set of stable fixtures with checked-in generated outputs +- Update `generated/compile_generated.zig` and `generated/main.zig` only for those curated artifacts + +### Encode source format in generated smoke filenames + +If the same fixture basename exists as both JSON and YAML in the same version folder, artifact names must include the source format: + +- `petstore__json__paths.zig` +- `petstore__yaml__paths.zig` + +Without that segment, one format overwrites the other. + +### Curated harness commits must be atomic + +If `build.zig` or `generated/*` starts importing a new generated file, that same commit must also add the checked-in generated artifact. Otherwise `zig build test` breaks immediately on missing imports. + +### Treat `generated/main.zig` as a sanity harness, not the final smoke authority + +- `zig build run-generate` + `zig run generated/main.zig` is useful to prove curated generated modules still build, initialize, and can exercise a tiny happy path. +- Do **not** use it as the only acceptance signal for broader smoke work; it still depends on live endpoints and can print runtime errors while returning early. +- For format-expansion work (like YAML coverage), the authoritative pass/fail signal must come from `test/smoke-tests.ps1`. + +### Do not use “has JSON sibling” as the YAML inclusion rule + +Some valid YAML smoke candidates may be YAML-only roots. Prefer explicit inclusion plus collision-safe naming. If a file is truly non-runnable, skip or denylist it with a reason instead of silently dropping all YAML-only files. + +### Denylist format-specific parse failures by exact fixture path + +- If a new format expansion (such as YAML) exposes parser failures before wrapper-specific generation runs, add explicit denylist entries for the failing fixture paths instead of backing out discovery. +- Use `Mode = "*"` when every wrapper mode fails from the same pre-generation cause, and make the `Reason` describe the parser/normalization gap rather than only saying "generation failed". +- Keep the rest of the format family running so the smoke sweep still proves coverage for the supported fixtures. + +## Anti-Patterns + +- Moving broad fixture enumeration into `build.zig` +- Reusing the same output filename for JSON and YAML siblings +- Importing YAML harness artifacts with aliases that shadow existing JSON aliases +- Forcing version parity by inventing new canonical fixtures that do not exist in `openapi/` diff --git a/README.md b/README.md index 7dc1d7f..d72e613 100644 --- a/README.md +++ b/README.md @@ -184,14 +184,14 @@ pwsh test/smoke-tests.ps1 What it does: -- Validates all eligible JSON API specs under `openapi/v2.0`, `openapi/v3.0`, `openapi/v3.1`, and `openapi/v3.2`. +- Validates all eligible JSON and YAML API specs under `openapi/v2.0`, `openapi/v3.0`, `openapi/v3.1`, and `openapi/v3.2`. - Runs each spec through every resource-wrapper mode: `none`, `tags`, `paths`, and `hybrid`. -- Skips YAML files and the meta-schema documents under `openapi/json-schema/`. -- Writes generated outputs to `test/output/` (gitignored), with paths and filenames that include the resource-wrapper mode so variants do not collide. +- Ignores the meta-schema documents under `openapi/json-schema/`, which are outside the smoke-test discovery roots. +- Writes generated outputs to `test/output/` (gitignored), with filenames shaped like `____.zig` so JSON/YAML sibling fixtures do not collide. - Continues through individual failures and prints a final summary listing every failing spec/mode combination, then exits non-zero if any case failed. - Honors a temporary denylist for known-unsupported spec/mode combinations so the PR gate can stay green while generator gaps are tracked explicitly. -In CI, the same script runs in the `smoke-tests` job on pull requests and `main`, alongside the existing `zig build run-generate` + `zig run generated/main.zig` petstore harness. When the smoke-tests job fails, `test/output/` is uploaded as a workflow artifact for triage. +In CI, the same script runs in the `smoke-tests` job on pull requests and `main`, alongside the existing `zig build run-generate` + `zig run generated/main.zig` curated sample harness. The broad smoke discovery does not require JSON/YAML twins: YAML-only roots such as `openapi/v3.0/bot.paths.yaml` are still included when they live under the covered version folders. When the smoke-tests job fails, `test/output/` is uploaded as a workflow artifact for triage. ### Cross-compilation @@ -214,7 +214,7 @@ zig build -Dtarget=aarch64-linux openapi2zig generate [options] ``` -The `generate` command reads a JSON OpenAPI/Swagger document from a local file or `http`/`https` URL, auto-detects the spec version, and writes one Zig source file containing models, runtime helpers, and API functions. +The `generate` command reads a JSON or YAML OpenAPI/Swagger document from a local file or `http`/`https` URL, auto-detects the spec version, and writes one Zig source file containing models, runtime helpers, and API functions. ### Options @@ -254,17 +254,20 @@ openapi2zig generate -i openapi/v3.0/petstore.json -o api.zig --resource-wrapper ### Generated sample files -The build script includes sample generation targets used by the test suite: +The build script also includes curated sample-generation targets used by the checked-in generated harness: ```bash zig build run-generate-v2 # openapi/v2.0/petstore.json -> generated/generated_v2.zig +zig build run-generate-v2-yaml # openapi/v2.0/petstore.yaml -> generated/generated_v2_yaml.zig zig build run-generate-v3 # openapi/v3.0/petstore.json -> generated/generated_v3.zig +zig build run-generate-v3-yaml # openapi/v3.0/petstore.yaml -> generated/generated_v3_yaml.zig zig build run-generate-v31 # openapi/v3.1/webhook-example.json -> generated/generated_v31.zig +zig build run-generate-v31-yaml # openapi/v3.1/webhook-example.yaml -> generated/generated_v31_yaml.zig zig build run-generate-v32 # openapi/v3.2/petstore.json -> generated/generated_v32.zig zig build run-generate # runs all of the above ``` -`generated/main.zig` imports the v2 and v3 petstore outputs, initializes `Client` values, and exercises memory-managed endpoint calls. When Zig is available, validate generated examples with: +This quick harness is intentionally selective: it covers the curated v2/v3 petstore JSON+YAML outputs, the v3.1 webhook JSON+YAML outputs, and the v3.2 JSON output. `openapi/v3.2` remains JSON-only here because the repository does not currently ship a v3.2 YAML root fixture. For broader JSON+YAML fixture coverage across the sample tree, use `pwsh test/smoke-tests.ps1`. `generated/main.zig` imports the curated v2/v3 JSON+YAML modules plus the v3.1 YAML module, initializes `Client` values, and exercises memory-managed endpoint calls. `generated/compile_generated.zig` extends compile coverage across all curated generated artifacts. When Zig is available, validate generated examples with: ```bash zig build run-generate @@ -596,7 +599,6 @@ This project is in active development with solid foundation for OpenAPI/Swagger Planned features and enhancements: -- YAML specification format support - Enhanced authentication/authorization client support - Automatic API documentation generation - Performance optimizations for large specifications diff --git a/build.zig b/build.zig index 125edee..1bd5f82 100644 --- a/build.zig +++ b/build.zig @@ -86,6 +86,19 @@ pub fn build(b: *std.Build) void { const run_generate_v2_step = b.step("run-generate-v2", "Run the app with generate command"); run_generate_v2_step.dependOn(&run_generate_v2_cmd.step); + const run_generate_v2_yaml_cmd = b.addRunArtifact(exe); + run_generate_v2_yaml_cmd.addArgs(&.{ + "generate", + "-i", + "openapi/v2.0/petstore.yaml", + "-o", + "generated/generated_v2_yaml.zig", + "--base-url", + "https://petstore.swagger.io/v2", + }); + const run_generate_v2_yaml_step = b.step("run-generate-v2-yaml", "Run the app with generate command for Swagger v2.0 YAML"); + run_generate_v2_yaml_step.dependOn(&run_generate_v2_yaml_cmd.step); + const run_generate_v32_cmd = b.addRunArtifact(exe); run_generate_v32_cmd.addArgs(&.{ "generate", @@ -110,11 +123,38 @@ pub fn build(b: *std.Build) void { const run_generate_v31_step = b.step("run-generate-v31", "Run the app with generate command for OpenAPI v3.1"); run_generate_v31_step.dependOn(&run_generate_v31_cmd.step); + const run_generate_v31_yaml_cmd = b.addRunArtifact(exe); + run_generate_v31_yaml_cmd.addArgs(&.{ + "generate", + "-i", + "openapi/v3.1/webhook-example.yaml", + "-o", + "generated/generated_v31_yaml.zig", + }); + const run_generate_v31_yaml_step = b.step("run-generate-v31-yaml", "Run the app with generate command for OpenAPI v3.1 YAML"); + run_generate_v31_yaml_step.dependOn(&run_generate_v31_yaml_cmd.step); + + const run_generate_v3_yaml_cmd = b.addRunArtifact(exe); + run_generate_v3_yaml_cmd.addArgs(&.{ + "generate", + "-i", + "openapi/v3.0/petstore.yaml", + "-o", + "generated/generated_v3_yaml.zig", + "--base-url", + "https://petstore3.swagger.io/api/v3", + }); + const run_generate_v3_yaml_step = b.step("run-generate-v3-yaml", "Run the app with generate command for OpenAPI v3.0 YAML"); + run_generate_v3_yaml_step.dependOn(&run_generate_v3_yaml_cmd.step); + const run_generate = b.step("run-generate", "Run the app with generate commands"); run_generate.dependOn(&run_generate_v3_cmd.step); + run_generate.dependOn(&run_generate_v3_yaml_cmd.step); run_generate.dependOn(&run_generate_v2_cmd.step); + run_generate.dependOn(&run_generate_v2_yaml_cmd.step); run_generate.dependOn(&run_generate_v32_cmd.step); run_generate.dependOn(&run_generate_v31_cmd.step); + run_generate.dependOn(&run_generate_v31_yaml_cmd.step); const tests_mod = b.createModule(.{ .root_source_file = b.path("src/tests.zig"), diff --git a/generated/compile_generated.zig b/generated/compile_generated.zig index 226bd5a..d1e1f37 100644 --- a/generated/compile_generated.zig +++ b/generated/compile_generated.zig @@ -1,14 +1,20 @@ const std = @import("std"); const v2 = @import("generated_v2.zig"); +const v2_yaml = @import("generated_v2_yaml.zig"); const v3 = @import("generated_v3.zig"); +const v3_yaml = @import("generated_v3_yaml.zig"); const v31 = @import("generated_v31.zig"); +const v31_yaml = @import("generated_v31_yaml.zig"); const v32 = @import("generated_v32.zig"); test "generated clients compile" { std.testing.refAllDecls(v2); + std.testing.refAllDecls(v2_yaml); std.testing.refAllDecls(v3); + std.testing.refAllDecls(v3_yaml); std.testing.refAllDecls(v31); + std.testing.refAllDecls(v31_yaml); std.testing.refAllDecls(v32); } diff --git a/generated/generated_v2_yaml.zig b/generated/generated_v2_yaml.zig new file mode 100644 index 0000000..23663ef --- /dev/null +++ b/generated/generated_v2_yaml.zig @@ -0,0 +1,1147 @@ +const std = @import("std"); + +/////////////////////////////////////////// +// Generated Zig structures from OpenAPI +/////////////////////////////////////////// + +pub const Category = struct { + id: ?i64 = null, + name: ?[]const u8 = null, +}; + +pub const Pet = struct { + status: ?[]const u8 = null, + tags: ?[]const Tag = null, + category: ?Category = null, + id: ?i64 = null, + name: []const u8, + photoUrls: []const []const u8, +}; + +pub const ApiResponse = struct { + @"type": ?[]const u8 = null, + message: ?[]const u8 = null, + code: ?i64 = null, +}; + +pub const Tag = struct { + id: ?i64 = null, + name: ?[]const u8 = null, +}; + +pub const Order = struct { + status: ?[]const u8 = null, + petId: ?i64 = null, + complete: ?bool = null, + id: ?i64 = null, + quantity: ?i64 = null, + shipDate: ?[]const u8 = null, +}; + +pub const User = struct { + password: ?[]const u8 = null, + userStatus: ?i64 = null, + username: ?[]const u8 = null, + email: ?[]const u8 = null, + firstName: ?[]const u8 = null, + id: ?i64 = null, + lastName: ?[]const u8 = null, + phone: ?[]const u8 = null, +}; + + +/////////////////////////////////////////// +// Generated Zig API client from OpenAPI +/////////////////////////////////////////// + + +pub fn Owned(comptime T: type) type { + return struct { + allocator: std.mem.Allocator, + body: []u8, + parsed: std.json.Parsed(T), + + pub fn deinit(self: *@This()) void { + self.parsed.deinit(); + self.allocator.free(self.body); + } + + pub fn value(self: *@This()) *T { + return &self.parsed.value; + } + }; +} + +pub const RawResponse = struct { + allocator: std.mem.Allocator, + status: std.http.Status, + body: []u8, + + pub fn deinit(self: *@This()) void { + self.allocator.free(self.body); + } +}; + +pub const ParseErrorResponse = struct { + raw: RawResponse, + error_name: []const u8, +}; + +pub fn ApiResult(comptime T: type) type { + return union(enum) { + ok: Owned(T), + api_error: RawResponse, + parse_error: ParseErrorResponse, + + pub fn deinit(self: *@This()) void { + switch (self.*) { + .ok => |*value| value.deinit(), + .api_error => |*value| value.deinit(), + .parse_error => |*value| value.raw.deinit(), + } + } + }; +} + +pub const Client = struct { + allocator: std.mem.Allocator, + io: std.Io, + http: std.http.Client, + api_key: []const u8, + base_url: []const u8 = "https://petstore.swagger.io/v2", + organization: ?[]const u8 = null, + project: ?[]const u8 = null, + default_headers: []const std.http.Header = &.{}, + + pub fn init(allocator: std.mem.Allocator, io: std.Io, api_key: []const u8) Client { + return .{ + .allocator = allocator, + .io = io, + .http = .{ .allocator = allocator, .io = io }, + .api_key = api_key, + }; + } + + pub fn deinit(self: *Client) void { + self.http.deinit(); + } + + pub fn withBaseUrl(self: *Client, base_url: []const u8) void { + self.base_url = base_url; + } +}; + +fn isQueryChar(c: u8) bool { + return std.ascii.isAlphanumeric(c) or switch (c) { + '-', '.', '_', '~' => true, + else => false, + }; +} + +fn writeQueryComponent(writer: *std.Io.Writer, value: []const u8) !void { + try std.Uri.Component.percentEncode(writer, value, isQueryChar); +} + +fn writeQueryValue(writer: *std.Io.Writer, value: anytype) !void { + const T = @TypeOf(value); + switch (@typeInfo(T)) { + .pointer => |ptr| { + if (ptr.size == .slice and ptr.child == u8) { + try writeQueryComponent(writer, value); + } else { + try std.json.Stringify.value(value, .{}, writer); + } + }, + .int, .comptime_int, .float, .comptime_float, .bool => try writer.print("{}", .{value}), + .@"enum" => try writeQueryComponent(writer, @tagName(value)), + else => try std.json.Stringify.value(value, .{}, writer), + } +} + +fn appendQueryParam(writer: *std.Io.Writer, first_query: *bool, name: []const u8, value: anytype) !void { + if (first_query.*) { + try writer.writeByte('?'); + first_query.* = false; + } else { + try writer.writeByte('&'); + } + try writeQueryComponent(writer, name); + try writer.writeByte('='); + try writeQueryValue(writer, value); +} + +pub fn requestRaw(client: *Client, method: std.http.Method, url: []const u8, payload: ?[]const u8) !RawResponse { + const allocator = client.allocator; + var headers = std.ArrayList(std.http.Header).empty; + defer headers.deinit(allocator); + const auth_header = try appendClientHeaders(allocator, &headers, client, payload != null, "application/json"); + defer if (auth_header) |value| allocator.free(value); + + const uri = try std.Uri.parse(url); + var response_body: std.Io.Writer.Allocating = .init(allocator); + defer response_body.deinit(); + + const result = try client.http.fetch(.{ + .location = .{ .uri = uri }, + .method = method, + .extra_headers = headers.items, + .payload = payload, + .response_writer = &response_body.writer, + }); + + return .{ + .allocator = allocator, + .status = result.status, + .body = try response_body.toOwnedSlice(), + }; +} + +pub fn getRaw(client: *Client, path: []const u8) !RawResponse { + const url = try std.fmt.allocPrint(client.allocator, "{s}{s}", .{ client.base_url, path }); + defer client.allocator.free(url); + return requestRaw(client, .GET, url, null); +} + +pub fn postJsonRaw(client: *Client, path: []const u8, payload: anytype) !RawResponse { + const allocator = client.allocator; + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(payload, .{ .emit_null_optional_fields = false }, &str.writer); + + const url = try std.fmt.allocPrint(allocator, "{s}{s}", .{ client.base_url, path }); + defer allocator.free(url); + return requestRaw(client, .POST, url, str.written()); +} + +pub fn parseRawResponse(comptime T: type, raw: RawResponse) !ApiResult(T) { + if (raw.status.class() != .success) return .{ .api_error = raw }; + const parsed = std.json.parseFromSlice(T, raw.allocator, raw.body, .{ .ignore_unknown_fields = true }) catch |err| { + return .{ .parse_error = .{ .raw = raw, .error_name = @errorName(err) } }; + }; + return .{ .ok = .{ .allocator = raw.allocator, .body = raw.body, .parsed = parsed } }; +} + +pub fn getJsonResult(comptime T: type, client: *Client, path: []const u8) !ApiResult(T) { + return parseRawResponse(T, try getRaw(client, path)); +} + +pub fn postJsonResult(comptime T: type, client: *Client, path: []const u8, payload: anytype) !ApiResult(T) { + return parseRawResponse(T, try postJsonRaw(client, path, payload)); +} + +const max_sse_line_size = 256 * 1024; +const max_sse_event_size = 1024 * 1024; + +pub fn parseSseBytes(allocator: std.mem.Allocator, bytes: []const u8, callback: anytype) !void { + var reader: std.Io.Reader = .fixed(bytes); + try parseSseReader(allocator, &reader, callback); +} + +pub fn parseSseReader(allocator: std.mem.Allocator, reader: *std.Io.Reader, callback: anytype) !void { + var line_buf: std.Io.Writer.Allocating = .init(allocator); + defer line_buf.deinit(); + + var event_data: std.Io.Writer.Allocating = .init(allocator); + defer event_data.deinit(); + + while (true) { + line_buf.clearRetainingCapacity(); + + _ = reader.streamDelimiterLimit(&line_buf.writer, '\n', .limited(max_sse_line_size)) catch |err| switch (err) { + error.StreamTooLong => return error.SseLineTooLong, + error.ReadFailed => return err, + error.WriteFailed => return err, + }; + + const ended_with_delimiter = blk: { + const byte = reader.peekByte() catch |err| switch (err) { + error.EndOfStream => break :blk false, + error.ReadFailed => return err, + }; + if (byte == '\n') { + _ = try reader.takeByte(); + break :blk true; + } + break :blk false; + }; + + if (try processSseLine(&event_data, line_buf.written(), callback)) return; + if (!ended_with_delimiter) break; + } + + _ = try dispatchSseEvent(&event_data, callback); +} + +fn processSseLine(event_data: *std.Io.Writer.Allocating, raw_line: []const u8, callback: anytype) !bool { + const line = std.mem.trimEnd(u8, raw_line, "\r"); + if (line.len == 0) return try dispatchSseEvent(event_data, callback); + if (line[0] == ':') return false; + + const colon = std.mem.indexOfScalar(u8, line, ':') orelse return false; + const field = line[0..colon]; + if (!std.mem.eql(u8, field, "data")) return false; + + var value = line[colon + 1 ..]; + if (value.len > 0 and value[0] == ' ') value = value[1..]; + const separator_len: usize = if (event_data.written().len == 0) 0 else 1; + if (event_data.written().len + separator_len + value.len > max_sse_event_size) return error.SseEventTooLong; + if (separator_len != 0) try event_data.writer.writeByte('\n'); + try event_data.writer.writeAll(value); + return false; +} + +fn dispatchSseEvent(event_data: *std.Io.Writer.Allocating, callback: anytype) !bool { + const data = event_data.written(); + if (data.len == 0) return false; + defer event_data.clearRetainingCapacity(); + + if (std.mem.eql(u8, data, "[DONE]")) return true; + try callback.event(data); + return false; +} + +fn TypedSseCallback(comptime T: type, comptime Callback: type) type { + return struct { + allocator: std.mem.Allocator, + callback: *Callback, + + pub fn event(self: *@This(), data: []const u8) !void { + var parsed = try std.json.parseFromSlice(T, self.allocator, data, .{ .ignore_unknown_fields = true }); + defer parsed.deinit(); + try self.callback.event(&parsed.value); + } + }; +} + +pub fn parseSseBytesTyped(comptime T: type, allocator: std.mem.Allocator, bytes: []const u8, callback: anytype) !void { + const Callback = @TypeOf(callback.*); + var typed_callback: TypedSseCallback(T, Callback) = .{ .allocator = allocator, .callback = callback }; + try parseSseBytes(allocator, bytes, &typed_callback); +} + +pub fn parseSseReaderTyped(comptime T: type, allocator: std.mem.Allocator, reader: *std.Io.Reader, callback: anytype) !void { + const Callback = @TypeOf(callback.*); + var typed_callback: TypedSseCallback(T, Callback) = .{ .allocator = allocator, .callback = callback }; + try parseSseReader(allocator, reader, &typed_callback); +} + +fn stringifyStreamRequest(allocator: std.mem.Allocator, requestBody: anytype) ![]u8 { + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, str.written(), .{ .ignore_unknown_fields = true }); + defer parsed.deinit(); + + if (parsed.value == .object) { + try parsed.value.object.put(parsed.arena.allocator(), "stream", .{ .bool = true }); + } + + var out: std.Io.Writer.Allocating = .init(allocator); + errdefer out.deinit(); + try std.json.Stringify.value(parsed.value, .{ .emit_null_optional_fields = false }, &out.writer); + return try out.toOwnedSlice(); +} + +fn streamJsonTyped(comptime T: type, client: *Client, path: []const u8, requestBody: anytype, callback: anytype) !void { + const Callback = @TypeOf(callback.*); + var typed_callback: TypedSseCallback(T, Callback) = .{ .allocator = client.allocator, .callback = callback }; + try streamJson(client, path, requestBody, &typed_callback); +} + +fn streamJson(client: *Client, path: []const u8, requestBody: anytype, callback: anytype) !void { + const allocator = client.allocator; + const payload = try stringifyStreamRequest(allocator, requestBody); + defer allocator.free(payload); + + var headers = std.ArrayList(std.http.Header).empty; + defer headers.deinit(allocator); + const auth_header = try appendClientHeaders(allocator, &headers, client, true, "text/event-stream"); + defer if (auth_header) |value| allocator.free(value); + + const url = try std.fmt.allocPrint(allocator, "{s}{s}", .{ client.base_url, path }); + defer allocator.free(url); + const uri = try std.Uri.parse(url); + + var req = try client.http.request(.POST, uri, .{ + .redirect_behavior = .unhandled, + .headers = .{ .accept_encoding = .{ .override = "identity" } }, + .extra_headers = headers.items, + }); + defer req.deinit(); + + req.transfer_encoding = .{ .content_length = payload.len }; + var body = try req.sendBodyUnflushed(&.{}); + try body.writer.writeAll(payload); + try body.end(); + try req.connection.?.flush(); + + var response = try req.receiveHead(&.{}); + if (response.head.status.class() != .success) return error.ResponseError; + + var transfer_buffer: [8 * 1024]u8 = undefined; + const reader = response.reader(&transfer_buffer); + parseSseReader(allocator, reader, callback) catch |err| switch (err) { + error.ReadFailed => return response.bodyErr() orelse err, + else => return err, + }; +} + +fn appendClientHeaders(allocator: std.mem.Allocator, headers: *std.ArrayList(std.http.Header), client: *Client, include_content_type: bool, accept: []const u8) !?[]u8 { + if (include_content_type) { + try headers.append(allocator, .{ .name = "Content-Type", .value = "application/json" }); + } + try headers.append(allocator, .{ .name = "Accept", .value = accept }); + + var auth_header: ?[]u8 = null; + if (client.api_key.len > 0) { + auth_header = try std.fmt.allocPrint(allocator, "Bearer {s}", .{client.api_key}); + try headers.append(allocator, .{ .name = "Authorization", .value = auth_header.? }); + } + if (client.organization) |organization| { + try headers.append(allocator, .{ .name = "OpenAI-Organization", .value = organization }); + } + if (client.project) |project| { + try headers.append(allocator, .{ .name = "OpenAI-Project", .value = project }); + } + for (client.default_headers) |header| { + try headers.append(allocator, header); + } + return auth_header; +} + +///////////////// +// Summary: +// Place an order for a pet +// +// Description: +// +// +pub fn placeOrder(client: *Client, requestBody: Order) !Owned(Order) { + var result = try placeOrderResult(client, requestBody); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} + +pub fn placeOrderRaw(client: *Client, requestBody: Order) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/store/order", .{client.base_url}); + + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); + const payload: ?[]const u8 = str.written(); + + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); +} + +pub fn placeOrderResult(client: *Client, requestBody: Order) !ApiResult(Order) { + return parseRawResponse(Order, try placeOrderRaw(client, requestBody)); +} + +///////////////// +// Summary: +// uploads an image +// +// Description: +// +// +pub fn uploadFile(client: *Client, petId: i64, additionalMetadata: []const u8, file: []const u8) !Owned(ApiResponse) { + var result = try uploadFileResult(client, petId, additionalMetadata, file); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} + +pub fn uploadFileRaw(client: *Client, petId: i64, additionalMetadata: []const u8, file: []const u8) !RawResponse { + const allocator = client.allocator; + _ = additionalMetadata; + _ = file; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet/{d}/uploadImage", .{client.base_url, petId}); + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); +} + +pub fn uploadFileResult(client: *Client, petId: i64, additionalMetadata: []const u8, file: []const u8) !ApiResult(ApiResponse) { + return parseRawResponse(ApiResponse, try uploadFileRaw(client, petId, additionalMetadata, file)); +} + +///////////////// +// Summary: +// Find pet by ID +// +// Description: +// Returns a single pet +// +pub fn getPetById(client: *Client, petId: i64) !Owned(Pet) { + var result = try getPetByIdResult(client, petId); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} + +pub fn getPetByIdRaw(client: *Client, petId: i64) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet/{d}", .{client.base_url, petId}); + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} + +pub fn getPetByIdResult(client: *Client, petId: i64) !ApiResult(Pet) { + return parseRawResponse(Pet, try getPetByIdRaw(client, petId)); +} + +///////////////// +// Summary: +// Updates a pet in the store with form data +// +// Description: +// +// +pub fn updatePetWithForm(client: *Client, petId: i64, name: []const u8, status: []const u8) !void { + var raw = try updatePetWithFormRaw(client, petId, name, status); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} + +pub fn updatePetWithFormRaw(client: *Client, petId: i64, name: []const u8, status: []const u8) !RawResponse { + const allocator = client.allocator; + _ = name; + _ = status; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet/{d}", .{client.base_url, petId}); + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); +} + +///////////////// +// Summary: +// Deletes a pet +// +// Description: +// +// +pub fn deletePet(client: *Client, api_key: []const u8, petId: i64) !void { + var raw = try deletePetRaw(client, api_key, petId); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} + +pub fn deletePetRaw(client: *Client, api_key: []const u8, petId: i64) !RawResponse { + const allocator = client.allocator; + _ = api_key; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet/{d}", .{client.base_url, petId}); + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.DELETE, uri_buf.written(), payload); +} + +///////////////// +// Summary: +// Finds Pets by tags +// +// Description: +// Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. +// +pub fn findPetsByTags(client: *Client, tags: []const std.json.Value) !Owned([]const std.json.Value) { + var result = try findPetsByTagsResult(client, tags); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} + +pub fn findPetsByTagsRaw(client: *Client, tags: []const std.json.Value) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet/findByTags", .{client.base_url}); + var first_query = true; + try appendQueryParam(&uri_buf.writer, &first_query, "tags", tags); + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} + +pub fn findPetsByTagsResult(client: *Client, tags: []const std.json.Value) !ApiResult([]const std.json.Value) { + return parseRawResponse([]const std.json.Value, try findPetsByTagsRaw(client, tags)); +} + +///////////////// +// Summary: +// Logs user into the system +// +// Description: +// +// +pub fn loginUser(client: *Client, username: []const u8, password: []const u8) !Owned([]const u8) { + var result = try loginUserResult(client, username, password); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} + +pub fn loginUserRaw(client: *Client, username: []const u8, password: []const u8) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/user/login", .{client.base_url}); + var first_query = true; + try appendQueryParam(&uri_buf.writer, &first_query, "username", username); + try appendQueryParam(&uri_buf.writer, &first_query, "password", password); + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} + +pub fn loginUserResult(client: *Client, username: []const u8, password: []const u8) !ApiResult([]const u8) { + return parseRawResponse([]const u8, try loginUserRaw(client, username, password)); +} + +///////////////// +// Summary: +// Creates list of users with given input array +// +// Description: +// +// +pub fn createUsersWithArrayInput(client: *Client, requestBody: []const std.json.Value) !void { + var raw = try createUsersWithArrayInputRaw(client, requestBody); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} + +pub fn createUsersWithArrayInputRaw(client: *Client, requestBody: []const std.json.Value) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/user/createWithArray", .{client.base_url}); + + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); + const payload: ?[]const u8 = str.written(); + + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); +} + +///////////////// +// Summary: +// Finds Pets by status +// +// Description: +// Multiple status values can be provided with comma separated strings +// +pub fn findPetsByStatus(client: *Client, status: []const std.json.Value) !Owned([]const std.json.Value) { + var result = try findPetsByStatusResult(client, status); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} + +pub fn findPetsByStatusRaw(client: *Client, status: []const std.json.Value) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet/findByStatus", .{client.base_url}); + var first_query = true; + try appendQueryParam(&uri_buf.writer, &first_query, "status", status); + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} + +pub fn findPetsByStatusResult(client: *Client, status: []const std.json.Value) !ApiResult([]const std.json.Value) { + return parseRawResponse([]const std.json.Value, try findPetsByStatusRaw(client, status)); +} + +///////////////// +// Summary: +// Returns pet inventories by status +// +// Description: +// Returns a map of status codes to quantities +// +pub fn getInventory(client: *Client) !Owned(std.json.Value) { + var result = try getInventoryResult(client); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} + +pub fn getInventoryRaw(client: *Client) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/store/inventory", .{client.base_url}); + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} + +pub fn getInventoryResult(client: *Client) !ApiResult(std.json.Value) { + return parseRawResponse(std.json.Value, try getInventoryRaw(client)); +} + +///////////////// +// Summary: +// Get user by user name +// +// Description: +// +// +pub fn getUserByName(client: *Client, username: []const u8) !Owned(User) { + var result = try getUserByNameResult(client, username); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} + +pub fn getUserByNameRaw(client: *Client, username: []const u8) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/user/{s}", .{client.base_url, username}); + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} + +pub fn getUserByNameResult(client: *Client, username: []const u8) !ApiResult(User) { + return parseRawResponse(User, try getUserByNameRaw(client, username)); +} + +///////////////// +// Summary: +// Updated user +// +// Description: +// This can only be done by the logged in user. +// +pub fn updateUser(client: *Client, username: []const u8, requestBody: User) !void { + var raw = try updateUserRaw(client, username, requestBody); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} + +pub fn updateUserRaw(client: *Client, username: []const u8, requestBody: User) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/user/{s}", .{client.base_url, username}); + + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); + const payload: ?[]const u8 = str.written(); + + return requestRaw(client, std.http.Method.PUT, uri_buf.written(), payload); +} + +///////////////// +// Summary: +// Delete user +// +// Description: +// This can only be done by the logged in user. +// +pub fn deleteUser(client: *Client, username: []const u8) !void { + var raw = try deleteUserRaw(client, username); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} + +pub fn deleteUserRaw(client: *Client, username: []const u8) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/user/{s}", .{client.base_url, username}); + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.DELETE, uri_buf.written(), payload); +} + +///////////////// +// Summary: +// Create user +// +// Description: +// This can only be done by the logged in user. +// +pub fn createUser(client: *Client, requestBody: User) !void { + var raw = try createUserRaw(client, requestBody); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} + +pub fn createUserRaw(client: *Client, requestBody: User) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/user", .{client.base_url}); + + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); + const payload: ?[]const u8 = str.written(); + + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); +} + +///////////////// +// Summary: +// Creates list of users with given input array +// +// Description: +// +// +pub fn createUsersWithListInput(client: *Client, requestBody: []const std.json.Value) !void { + var raw = try createUsersWithListInputRaw(client, requestBody); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} + +pub fn createUsersWithListInputRaw(client: *Client, requestBody: []const std.json.Value) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/user/createWithList", .{client.base_url}); + + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); + const payload: ?[]const u8 = str.written(); + + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); +} + +///////////////// +// Summary: +// Add a new pet to the store +// +// Description: +// +// +pub fn addPet(client: *Client, requestBody: Pet) !void { + var raw = try addPetRaw(client, requestBody); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} + +pub fn addPetRaw(client: *Client, requestBody: Pet) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet", .{client.base_url}); + + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); + const payload: ?[]const u8 = str.written(); + + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); +} + +///////////////// +// Summary: +// Update an existing pet +// +// Description: +// +// +pub fn updatePet(client: *Client, requestBody: Pet) !void { + var raw = try updatePetRaw(client, requestBody); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} + +pub fn updatePetRaw(client: *Client, requestBody: Pet) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet", .{client.base_url}); + + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); + const payload: ?[]const u8 = str.written(); + + return requestRaw(client, std.http.Method.PUT, uri_buf.written(), payload); +} + +///////////////// +// Summary: +// Find purchase order by ID +// +// Description: +// For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions +// +pub fn getOrderById(client: *Client, orderId: i64) !Owned(Order) { + var result = try getOrderByIdResult(client, orderId); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} + +pub fn getOrderByIdRaw(client: *Client, orderId: i64) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/store/order/{d}", .{client.base_url, orderId}); + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} + +pub fn getOrderByIdResult(client: *Client, orderId: i64) !ApiResult(Order) { + return parseRawResponse(Order, try getOrderByIdRaw(client, orderId)); +} + +///////////////// +// Summary: +// Delete purchase order by ID +// +// Description: +// For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors +// +pub fn deleteOrder(client: *Client, orderId: i64) !void { + var raw = try deleteOrderRaw(client, orderId); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} + +pub fn deleteOrderRaw(client: *Client, orderId: i64) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/store/order/{d}", .{client.base_url, orderId}); + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.DELETE, uri_buf.written(), payload); +} + +///////////////// +// Summary: +// Logs out current logged in user session +// +// Description: +// +// +pub fn logoutUser(client: *Client) !void { + var raw = try logoutUserRaw(client); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} + +pub fn logoutUserRaw(client: *Client) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/user/logout", .{client.base_url}); + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} + +pub const resources = struct { + pub const pet = struct { + pub fn addpet(client: *Client, requestBody: Pet) !void { + return addPet(client, requestBody); + } + pub fn delete(client: *Client, api_key: []const u8, petId: i64) !void { + return deletePet(client, api_key, petId); + } + pub fn get(client: *Client, petId: i64) !Owned(Pet) { + return getPetById(client, petId); + } + pub fn getResult(client: *Client, petId: i64) !ApiResult(Pet) { + return getPetByIdResult(client, petId); + } + pub fn updatepet_(client: *Client, requestBody: Pet) !void { + return updatePet(client, requestBody); + } + pub fn updatepetwithform_(client: *Client, petId: i64, name: []const u8, status: []const u8) !void { + return updatePetWithForm(client, petId, name, status); + } + pub const findbystatus = struct { + pub fn findpetsbystatus(client: *Client, status: []const std.json.Value) !Owned([]const std.json.Value) { + return findPetsByStatus(client, status); + } + pub fn findpetsbystatusResult(client: *Client, status: []const std.json.Value) !ApiResult([]const std.json.Value) { + return findPetsByStatusResult(client, status); + } + }; + pub const findbytags = struct { + pub fn findpetsbytags(client: *Client, tags: []const std.json.Value) !Owned([]const std.json.Value) { + return findPetsByTags(client, tags); + } + pub fn findpetsbytagsResult(client: *Client, tags: []const std.json.Value) !ApiResult([]const std.json.Value) { + return findPetsByTagsResult(client, tags); + } + }; + pub const uploadimage = struct { + pub fn uploadfile(client: *Client, petId: i64, additionalMetadata: []const u8, file: []const u8) !Owned(ApiResponse) { + return uploadFile(client, petId, additionalMetadata, file); + } + pub fn uploadfileResult(client: *Client, petId: i64, additionalMetadata: []const u8, file: []const u8) !ApiResult(ApiResponse) { + return uploadFileResult(client, petId, additionalMetadata, file); + } + }; + }; + pub const store = struct { + pub const inventory = struct { + pub fn get(client: *Client) !Owned(std.json.Value) { + return getInventory(client); + } + pub fn getResult(client: *Client) !ApiResult(std.json.Value) { + return getInventoryResult(client); + } + }; + pub const order = struct { + pub fn delete(client: *Client, orderId: i64) !void { + return deleteOrder(client, orderId); + } + pub fn get(client: *Client, orderId: i64) !Owned(Order) { + return getOrderById(client, orderId); + } + pub fn getResult(client: *Client, orderId: i64) !ApiResult(Order) { + return getOrderByIdResult(client, orderId); + } + pub fn placeorder(client: *Client, requestBody: Order) !Owned(Order) { + return placeOrder(client, requestBody); + } + pub fn placeorderResult(client: *Client, requestBody: Order) !ApiResult(Order) { + return placeOrderResult(client, requestBody); + } + }; + }; + pub const user = struct { + pub fn create(client: *Client, requestBody: User) !void { + return createUser(client, requestBody); + } + pub fn delete(client: *Client, username: []const u8) !void { + return deleteUser(client, username); + } + pub fn get(client: *Client, username: []const u8) !Owned(User) { + return getUserByName(client, username); + } + pub fn getResult(client: *Client, username: []const u8) !ApiResult(User) { + return getUserByNameResult(client, username); + } + pub fn update(client: *Client, username: []const u8, requestBody: User) !void { + return updateUser(client, username, requestBody); + } + pub const createwitharray = struct { + pub fn create(client: *Client, requestBody: []const std.json.Value) !void { + return createUsersWithArrayInput(client, requestBody); + } + }; + pub const createwithlist = struct { + pub fn create(client: *Client, requestBody: []const std.json.Value) !void { + return createUsersWithListInput(client, requestBody); + } + }; + pub const login = struct { + pub fn loginuser(client: *Client, username: []const u8, password: []const u8) !Owned([]const u8) { + return loginUser(client, username, password); + } + pub fn loginuserResult(client: *Client, username: []const u8, password: []const u8) !ApiResult([]const u8) { + return loginUserResult(client, username, password); + } + }; + pub const logout = struct { + pub fn logoutuser(client: *Client) !void { + return logoutUser(client); + } + }; + }; +}; + +pub const pet = resources.pet; +pub const store = resources.store; +pub const user = resources.user; + diff --git a/generated/generated_v31_yaml.zig b/generated/generated_v31_yaml.zig new file mode 100644 index 0000000..03815c9 --- /dev/null +++ b/generated/generated_v31_yaml.zig @@ -0,0 +1,376 @@ +const std = @import("std"); + +/////////////////////////////////////////// +// Generated Zig structures from OpenAPI +/////////////////////////////////////////// + +pub const Pet = struct { + tag: ?[]const u8 = null, + id: i64, + name: []const u8, +}; + + +/////////////////////////////////////////// +// Generated Zig API client from OpenAPI +/////////////////////////////////////////// + + +pub fn Owned(comptime T: type) type { + return struct { + allocator: std.mem.Allocator, + body: []u8, + parsed: std.json.Parsed(T), + + pub fn deinit(self: *@This()) void { + self.parsed.deinit(); + self.allocator.free(self.body); + } + + pub fn value(self: *@This()) *T { + return &self.parsed.value; + } + }; +} + +pub const RawResponse = struct { + allocator: std.mem.Allocator, + status: std.http.Status, + body: []u8, + + pub fn deinit(self: *@This()) void { + self.allocator.free(self.body); + } +}; + +pub const ParseErrorResponse = struct { + raw: RawResponse, + error_name: []const u8, +}; + +pub fn ApiResult(comptime T: type) type { + return union(enum) { + ok: Owned(T), + api_error: RawResponse, + parse_error: ParseErrorResponse, + + pub fn deinit(self: *@This()) void { + switch (self.*) { + .ok => |*value| value.deinit(), + .api_error => |*value| value.deinit(), + .parse_error => |*value| value.raw.deinit(), + } + } + }; +} + +pub const Client = struct { + allocator: std.mem.Allocator, + io: std.Io, + http: std.http.Client, + api_key: []const u8, + base_url: []const u8 = "", + organization: ?[]const u8 = null, + project: ?[]const u8 = null, + default_headers: []const std.http.Header = &.{}, + + pub fn init(allocator: std.mem.Allocator, io: std.Io, api_key: []const u8) Client { + return .{ + .allocator = allocator, + .io = io, + .http = .{ .allocator = allocator, .io = io }, + .api_key = api_key, + }; + } + + pub fn deinit(self: *Client) void { + self.http.deinit(); + } + + pub fn withBaseUrl(self: *Client, base_url: []const u8) void { + self.base_url = base_url; + } +}; + +fn isQueryChar(c: u8) bool { + return std.ascii.isAlphanumeric(c) or switch (c) { + '-', '.', '_', '~' => true, + else => false, + }; +} + +fn writeQueryComponent(writer: *std.Io.Writer, value: []const u8) !void { + try std.Uri.Component.percentEncode(writer, value, isQueryChar); +} + +fn writeQueryValue(writer: *std.Io.Writer, value: anytype) !void { + const T = @TypeOf(value); + switch (@typeInfo(T)) { + .pointer => |ptr| { + if (ptr.size == .slice and ptr.child == u8) { + try writeQueryComponent(writer, value); + } else { + try std.json.Stringify.value(value, .{}, writer); + } + }, + .int, .comptime_int, .float, .comptime_float, .bool => try writer.print("{}", .{value}), + .@"enum" => try writeQueryComponent(writer, @tagName(value)), + else => try std.json.Stringify.value(value, .{}, writer), + } +} + +fn appendQueryParam(writer: *std.Io.Writer, first_query: *bool, name: []const u8, value: anytype) !void { + if (first_query.*) { + try writer.writeByte('?'); + first_query.* = false; + } else { + try writer.writeByte('&'); + } + try writeQueryComponent(writer, name); + try writer.writeByte('='); + try writeQueryValue(writer, value); +} + +pub fn requestRaw(client: *Client, method: std.http.Method, url: []const u8, payload: ?[]const u8) !RawResponse { + const allocator = client.allocator; + var headers = std.ArrayList(std.http.Header).empty; + defer headers.deinit(allocator); + const auth_header = try appendClientHeaders(allocator, &headers, client, payload != null, "application/json"); + defer if (auth_header) |value| allocator.free(value); + + const uri = try std.Uri.parse(url); + var response_body: std.Io.Writer.Allocating = .init(allocator); + defer response_body.deinit(); + + const result = try client.http.fetch(.{ + .location = .{ .uri = uri }, + .method = method, + .extra_headers = headers.items, + .payload = payload, + .response_writer = &response_body.writer, + }); + + return .{ + .allocator = allocator, + .status = result.status, + .body = try response_body.toOwnedSlice(), + }; +} + +pub fn getRaw(client: *Client, path: []const u8) !RawResponse { + const url = try std.fmt.allocPrint(client.allocator, "{s}{s}", .{ client.base_url, path }); + defer client.allocator.free(url); + return requestRaw(client, .GET, url, null); +} + +pub fn postJsonRaw(client: *Client, path: []const u8, payload: anytype) !RawResponse { + const allocator = client.allocator; + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(payload, .{ .emit_null_optional_fields = false }, &str.writer); + + const url = try std.fmt.allocPrint(allocator, "{s}{s}", .{ client.base_url, path }); + defer allocator.free(url); + return requestRaw(client, .POST, url, str.written()); +} + +pub fn parseRawResponse(comptime T: type, raw: RawResponse) !ApiResult(T) { + if (raw.status.class() != .success) return .{ .api_error = raw }; + const parsed = std.json.parseFromSlice(T, raw.allocator, raw.body, .{ .ignore_unknown_fields = true }) catch |err| { + return .{ .parse_error = .{ .raw = raw, .error_name = @errorName(err) } }; + }; + return .{ .ok = .{ .allocator = raw.allocator, .body = raw.body, .parsed = parsed } }; +} + +pub fn getJsonResult(comptime T: type, client: *Client, path: []const u8) !ApiResult(T) { + return parseRawResponse(T, try getRaw(client, path)); +} + +pub fn postJsonResult(comptime T: type, client: *Client, path: []const u8, payload: anytype) !ApiResult(T) { + return parseRawResponse(T, try postJsonRaw(client, path, payload)); +} + +const max_sse_line_size = 256 * 1024; +const max_sse_event_size = 1024 * 1024; + +pub fn parseSseBytes(allocator: std.mem.Allocator, bytes: []const u8, callback: anytype) !void { + var reader: std.Io.Reader = .fixed(bytes); + try parseSseReader(allocator, &reader, callback); +} + +pub fn parseSseReader(allocator: std.mem.Allocator, reader: *std.Io.Reader, callback: anytype) !void { + var line_buf: std.Io.Writer.Allocating = .init(allocator); + defer line_buf.deinit(); + + var event_data: std.Io.Writer.Allocating = .init(allocator); + defer event_data.deinit(); + + while (true) { + line_buf.clearRetainingCapacity(); + + _ = reader.streamDelimiterLimit(&line_buf.writer, '\n', .limited(max_sse_line_size)) catch |err| switch (err) { + error.StreamTooLong => return error.SseLineTooLong, + error.ReadFailed => return err, + error.WriteFailed => return err, + }; + + const ended_with_delimiter = blk: { + const byte = reader.peekByte() catch |err| switch (err) { + error.EndOfStream => break :blk false, + error.ReadFailed => return err, + }; + if (byte == '\n') { + _ = try reader.takeByte(); + break :blk true; + } + break :blk false; + }; + + if (try processSseLine(&event_data, line_buf.written(), callback)) return; + if (!ended_with_delimiter) break; + } + + _ = try dispatchSseEvent(&event_data, callback); +} + +fn processSseLine(event_data: *std.Io.Writer.Allocating, raw_line: []const u8, callback: anytype) !bool { + const line = std.mem.trimEnd(u8, raw_line, "\r"); + if (line.len == 0) return try dispatchSseEvent(event_data, callback); + if (line[0] == ':') return false; + + const colon = std.mem.indexOfScalar(u8, line, ':') orelse return false; + const field = line[0..colon]; + if (!std.mem.eql(u8, field, "data")) return false; + + var value = line[colon + 1 ..]; + if (value.len > 0 and value[0] == ' ') value = value[1..]; + const separator_len: usize = if (event_data.written().len == 0) 0 else 1; + if (event_data.written().len + separator_len + value.len > max_sse_event_size) return error.SseEventTooLong; + if (separator_len != 0) try event_data.writer.writeByte('\n'); + try event_data.writer.writeAll(value); + return false; +} + +fn dispatchSseEvent(event_data: *std.Io.Writer.Allocating, callback: anytype) !bool { + const data = event_data.written(); + if (data.len == 0) return false; + defer event_data.clearRetainingCapacity(); + + if (std.mem.eql(u8, data, "[DONE]")) return true; + try callback.event(data); + return false; +} + +fn TypedSseCallback(comptime T: type, comptime Callback: type) type { + return struct { + allocator: std.mem.Allocator, + callback: *Callback, + + pub fn event(self: *@This(), data: []const u8) !void { + var parsed = try std.json.parseFromSlice(T, self.allocator, data, .{ .ignore_unknown_fields = true }); + defer parsed.deinit(); + try self.callback.event(&parsed.value); + } + }; +} + +pub fn parseSseBytesTyped(comptime T: type, allocator: std.mem.Allocator, bytes: []const u8, callback: anytype) !void { + const Callback = @TypeOf(callback.*); + var typed_callback: TypedSseCallback(T, Callback) = .{ .allocator = allocator, .callback = callback }; + try parseSseBytes(allocator, bytes, &typed_callback); +} + +pub fn parseSseReaderTyped(comptime T: type, allocator: std.mem.Allocator, reader: *std.Io.Reader, callback: anytype) !void { + const Callback = @TypeOf(callback.*); + var typed_callback: TypedSseCallback(T, Callback) = .{ .allocator = allocator, .callback = callback }; + try parseSseReader(allocator, reader, &typed_callback); +} + +fn stringifyStreamRequest(allocator: std.mem.Allocator, requestBody: anytype) ![]u8 { + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, str.written(), .{ .ignore_unknown_fields = true }); + defer parsed.deinit(); + + if (parsed.value == .object) { + try parsed.value.object.put(parsed.arena.allocator(), "stream", .{ .bool = true }); + } + + var out: std.Io.Writer.Allocating = .init(allocator); + errdefer out.deinit(); + try std.json.Stringify.value(parsed.value, .{ .emit_null_optional_fields = false }, &out.writer); + return try out.toOwnedSlice(); +} + +fn streamJsonTyped(comptime T: type, client: *Client, path: []const u8, requestBody: anytype, callback: anytype) !void { + const Callback = @TypeOf(callback.*); + var typed_callback: TypedSseCallback(T, Callback) = .{ .allocator = client.allocator, .callback = callback }; + try streamJson(client, path, requestBody, &typed_callback); +} + +fn streamJson(client: *Client, path: []const u8, requestBody: anytype, callback: anytype) !void { + const allocator = client.allocator; + const payload = try stringifyStreamRequest(allocator, requestBody); + defer allocator.free(payload); + + var headers = std.ArrayList(std.http.Header).empty; + defer headers.deinit(allocator); + const auth_header = try appendClientHeaders(allocator, &headers, client, true, "text/event-stream"); + defer if (auth_header) |value| allocator.free(value); + + const url = try std.fmt.allocPrint(allocator, "{s}{s}", .{ client.base_url, path }); + defer allocator.free(url); + const uri = try std.Uri.parse(url); + + var req = try client.http.request(.POST, uri, .{ + .redirect_behavior = .unhandled, + .headers = .{ .accept_encoding = .{ .override = "identity" } }, + .extra_headers = headers.items, + }); + defer req.deinit(); + + req.transfer_encoding = .{ .content_length = payload.len }; + var body = try req.sendBodyUnflushed(&.{}); + try body.writer.writeAll(payload); + try body.end(); + try req.connection.?.flush(); + + var response = try req.receiveHead(&.{}); + if (response.head.status.class() != .success) return error.ResponseError; + + var transfer_buffer: [8 * 1024]u8 = undefined; + const reader = response.reader(&transfer_buffer); + parseSseReader(allocator, reader, callback) catch |err| switch (err) { + error.ReadFailed => return response.bodyErr() orelse err, + else => return err, + }; +} + +fn appendClientHeaders(allocator: std.mem.Allocator, headers: *std.ArrayList(std.http.Header), client: *Client, include_content_type: bool, accept: []const u8) !?[]u8 { + if (include_content_type) { + try headers.append(allocator, .{ .name = "Content-Type", .value = "application/json" }); + } + try headers.append(allocator, .{ .name = "Accept", .value = accept }); + + var auth_header: ?[]u8 = null; + if (client.api_key.len > 0) { + auth_header = try std.fmt.allocPrint(allocator, "Bearer {s}", .{client.api_key}); + try headers.append(allocator, .{ .name = "Authorization", .value = auth_header.? }); + } + if (client.organization) |organization| { + try headers.append(allocator, .{ .name = "OpenAI-Organization", .value = organization }); + } + if (client.project) |project| { + try headers.append(allocator, .{ .name = "OpenAI-Project", .value = project }); + } + for (client.default_headers) |header| { + try headers.append(allocator, header); + } + return auth_header; +} + +pub const resources = struct { +}; + diff --git a/generated/generated_v3_yaml.zig b/generated/generated_v3_yaml.zig new file mode 100644 index 0000000..1a25a3d --- /dev/null +++ b/generated/generated_v3_yaml.zig @@ -0,0 +1,1195 @@ +const std = @import("std"); + +/////////////////////////////////////////// +// Generated Zig structures from OpenAPI +/////////////////////////////////////////// + +pub const Category = struct { + id: ?i64 = null, + name: ?[]const u8 = null, +}; + +pub const Address = struct { + city: ?[]const u8 = null, + zip: ?[]const u8 = null, + state: ?[]const u8 = null, + street: ?[]const u8 = null, +}; + +pub const Tag = struct { + id: ?i64 = null, + name: ?[]const u8 = null, +}; + +pub const Order = struct { + status: ?[]const u8 = null, + petId: ?i64 = null, + complete: ?bool = null, + id: ?i64 = null, + quantity: ?i64 = null, + shipDate: ?[]const u8 = null, +}; + +pub const Customer = struct { + address: ?[]const Address = null, + id: ?i64 = null, + username: ?[]const u8 = null, +}; + +pub const Pet = struct { + status: ?[]const u8 = null, + tags: ?[]const Tag = null, + category: ?Category = null, + id: ?i64 = null, + name: []const u8, + photoUrls: []const []const u8, +}; + +pub const User = struct { + password: ?[]const u8 = null, + userStatus: ?i64 = null, + username: ?[]const u8 = null, + email: ?[]const u8 = null, + firstName: ?[]const u8 = null, + id: ?i64 = null, + lastName: ?[]const u8 = null, + phone: ?[]const u8 = null, +}; + +pub const ApiResponse = struct { + @"type": ?[]const u8 = null, + message: ?[]const u8 = null, + code: ?i64 = null, +}; + + +/////////////////////////////////////////// +// Generated Zig API client from OpenAPI +/////////////////////////////////////////// + + +pub fn Owned(comptime T: type) type { + return struct { + allocator: std.mem.Allocator, + body: []u8, + parsed: std.json.Parsed(T), + + pub fn deinit(self: *@This()) void { + self.parsed.deinit(); + self.allocator.free(self.body); + } + + pub fn value(self: *@This()) *T { + return &self.parsed.value; + } + }; +} + +pub const RawResponse = struct { + allocator: std.mem.Allocator, + status: std.http.Status, + body: []u8, + + pub fn deinit(self: *@This()) void { + self.allocator.free(self.body); + } +}; + +pub const ParseErrorResponse = struct { + raw: RawResponse, + error_name: []const u8, +}; + +pub fn ApiResult(comptime T: type) type { + return union(enum) { + ok: Owned(T), + api_error: RawResponse, + parse_error: ParseErrorResponse, + + pub fn deinit(self: *@This()) void { + switch (self.*) { + .ok => |*value| value.deinit(), + .api_error => |*value| value.deinit(), + .parse_error => |*value| value.raw.deinit(), + } + } + }; +} + +pub const Client = struct { + allocator: std.mem.Allocator, + io: std.Io, + http: std.http.Client, + api_key: []const u8, + base_url: []const u8 = "https://petstore3.swagger.io/api/v3", + organization: ?[]const u8 = null, + project: ?[]const u8 = null, + default_headers: []const std.http.Header = &.{}, + + pub fn init(allocator: std.mem.Allocator, io: std.Io, api_key: []const u8) Client { + return .{ + .allocator = allocator, + .io = io, + .http = .{ .allocator = allocator, .io = io }, + .api_key = api_key, + }; + } + + pub fn deinit(self: *Client) void { + self.http.deinit(); + } + + pub fn withBaseUrl(self: *Client, base_url: []const u8) void { + self.base_url = base_url; + } +}; + +fn isQueryChar(c: u8) bool { + return std.ascii.isAlphanumeric(c) or switch (c) { + '-', '.', '_', '~' => true, + else => false, + }; +} + +fn writeQueryComponent(writer: *std.Io.Writer, value: []const u8) !void { + try std.Uri.Component.percentEncode(writer, value, isQueryChar); +} + +fn writeQueryValue(writer: *std.Io.Writer, value: anytype) !void { + const T = @TypeOf(value); + switch (@typeInfo(T)) { + .pointer => |ptr| { + if (ptr.size == .slice and ptr.child == u8) { + try writeQueryComponent(writer, value); + } else { + try std.json.Stringify.value(value, .{}, writer); + } + }, + .int, .comptime_int, .float, .comptime_float, .bool => try writer.print("{}", .{value}), + .@"enum" => try writeQueryComponent(writer, @tagName(value)), + else => try std.json.Stringify.value(value, .{}, writer), + } +} + +fn appendQueryParam(writer: *std.Io.Writer, first_query: *bool, name: []const u8, value: anytype) !void { + if (first_query.*) { + try writer.writeByte('?'); + first_query.* = false; + } else { + try writer.writeByte('&'); + } + try writeQueryComponent(writer, name); + try writer.writeByte('='); + try writeQueryValue(writer, value); +} + +pub fn requestRaw(client: *Client, method: std.http.Method, url: []const u8, payload: ?[]const u8) !RawResponse { + const allocator = client.allocator; + var headers = std.ArrayList(std.http.Header).empty; + defer headers.deinit(allocator); + const auth_header = try appendClientHeaders(allocator, &headers, client, payload != null, "application/json"); + defer if (auth_header) |value| allocator.free(value); + + const uri = try std.Uri.parse(url); + var response_body: std.Io.Writer.Allocating = .init(allocator); + defer response_body.deinit(); + + const result = try client.http.fetch(.{ + .location = .{ .uri = uri }, + .method = method, + .extra_headers = headers.items, + .payload = payload, + .response_writer = &response_body.writer, + }); + + return .{ + .allocator = allocator, + .status = result.status, + .body = try response_body.toOwnedSlice(), + }; +} + +pub fn getRaw(client: *Client, path: []const u8) !RawResponse { + const url = try std.fmt.allocPrint(client.allocator, "{s}{s}", .{ client.base_url, path }); + defer client.allocator.free(url); + return requestRaw(client, .GET, url, null); +} + +pub fn postJsonRaw(client: *Client, path: []const u8, payload: anytype) !RawResponse { + const allocator = client.allocator; + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(payload, .{ .emit_null_optional_fields = false }, &str.writer); + + const url = try std.fmt.allocPrint(allocator, "{s}{s}", .{ client.base_url, path }); + defer allocator.free(url); + return requestRaw(client, .POST, url, str.written()); +} + +pub fn parseRawResponse(comptime T: type, raw: RawResponse) !ApiResult(T) { + if (raw.status.class() != .success) return .{ .api_error = raw }; + const parsed = std.json.parseFromSlice(T, raw.allocator, raw.body, .{ .ignore_unknown_fields = true }) catch |err| { + return .{ .parse_error = .{ .raw = raw, .error_name = @errorName(err) } }; + }; + return .{ .ok = .{ .allocator = raw.allocator, .body = raw.body, .parsed = parsed } }; +} + +pub fn getJsonResult(comptime T: type, client: *Client, path: []const u8) !ApiResult(T) { + return parseRawResponse(T, try getRaw(client, path)); +} + +pub fn postJsonResult(comptime T: type, client: *Client, path: []const u8, payload: anytype) !ApiResult(T) { + return parseRawResponse(T, try postJsonRaw(client, path, payload)); +} + +const max_sse_line_size = 256 * 1024; +const max_sse_event_size = 1024 * 1024; + +pub fn parseSseBytes(allocator: std.mem.Allocator, bytes: []const u8, callback: anytype) !void { + var reader: std.Io.Reader = .fixed(bytes); + try parseSseReader(allocator, &reader, callback); +} + +pub fn parseSseReader(allocator: std.mem.Allocator, reader: *std.Io.Reader, callback: anytype) !void { + var line_buf: std.Io.Writer.Allocating = .init(allocator); + defer line_buf.deinit(); + + var event_data: std.Io.Writer.Allocating = .init(allocator); + defer event_data.deinit(); + + while (true) { + line_buf.clearRetainingCapacity(); + + _ = reader.streamDelimiterLimit(&line_buf.writer, '\n', .limited(max_sse_line_size)) catch |err| switch (err) { + error.StreamTooLong => return error.SseLineTooLong, + error.ReadFailed => return err, + error.WriteFailed => return err, + }; + + const ended_with_delimiter = blk: { + const byte = reader.peekByte() catch |err| switch (err) { + error.EndOfStream => break :blk false, + error.ReadFailed => return err, + }; + if (byte == '\n') { + _ = try reader.takeByte(); + break :blk true; + } + break :blk false; + }; + + if (try processSseLine(&event_data, line_buf.written(), callback)) return; + if (!ended_with_delimiter) break; + } + + _ = try dispatchSseEvent(&event_data, callback); +} + +fn processSseLine(event_data: *std.Io.Writer.Allocating, raw_line: []const u8, callback: anytype) !bool { + const line = std.mem.trimEnd(u8, raw_line, "\r"); + if (line.len == 0) return try dispatchSseEvent(event_data, callback); + if (line[0] == ':') return false; + + const colon = std.mem.indexOfScalar(u8, line, ':') orelse return false; + const field = line[0..colon]; + if (!std.mem.eql(u8, field, "data")) return false; + + var value = line[colon + 1 ..]; + if (value.len > 0 and value[0] == ' ') value = value[1..]; + const separator_len: usize = if (event_data.written().len == 0) 0 else 1; + if (event_data.written().len + separator_len + value.len > max_sse_event_size) return error.SseEventTooLong; + if (separator_len != 0) try event_data.writer.writeByte('\n'); + try event_data.writer.writeAll(value); + return false; +} + +fn dispatchSseEvent(event_data: *std.Io.Writer.Allocating, callback: anytype) !bool { + const data = event_data.written(); + if (data.len == 0) return false; + defer event_data.clearRetainingCapacity(); + + if (std.mem.eql(u8, data, "[DONE]")) return true; + try callback.event(data); + return false; +} + +fn TypedSseCallback(comptime T: type, comptime Callback: type) type { + return struct { + allocator: std.mem.Allocator, + callback: *Callback, + + pub fn event(self: *@This(), data: []const u8) !void { + var parsed = try std.json.parseFromSlice(T, self.allocator, data, .{ .ignore_unknown_fields = true }); + defer parsed.deinit(); + try self.callback.event(&parsed.value); + } + }; +} + +pub fn parseSseBytesTyped(comptime T: type, allocator: std.mem.Allocator, bytes: []const u8, callback: anytype) !void { + const Callback = @TypeOf(callback.*); + var typed_callback: TypedSseCallback(T, Callback) = .{ .allocator = allocator, .callback = callback }; + try parseSseBytes(allocator, bytes, &typed_callback); +} + +pub fn parseSseReaderTyped(comptime T: type, allocator: std.mem.Allocator, reader: *std.Io.Reader, callback: anytype) !void { + const Callback = @TypeOf(callback.*); + var typed_callback: TypedSseCallback(T, Callback) = .{ .allocator = allocator, .callback = callback }; + try parseSseReader(allocator, reader, &typed_callback); +} + +fn stringifyStreamRequest(allocator: std.mem.Allocator, requestBody: anytype) ![]u8 { + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, str.written(), .{ .ignore_unknown_fields = true }); + defer parsed.deinit(); + + if (parsed.value == .object) { + try parsed.value.object.put(parsed.arena.allocator(), "stream", .{ .bool = true }); + } + + var out: std.Io.Writer.Allocating = .init(allocator); + errdefer out.deinit(); + try std.json.Stringify.value(parsed.value, .{ .emit_null_optional_fields = false }, &out.writer); + return try out.toOwnedSlice(); +} + +fn streamJsonTyped(comptime T: type, client: *Client, path: []const u8, requestBody: anytype, callback: anytype) !void { + const Callback = @TypeOf(callback.*); + var typed_callback: TypedSseCallback(T, Callback) = .{ .allocator = client.allocator, .callback = callback }; + try streamJson(client, path, requestBody, &typed_callback); +} + +fn streamJson(client: *Client, path: []const u8, requestBody: anytype, callback: anytype) !void { + const allocator = client.allocator; + const payload = try stringifyStreamRequest(allocator, requestBody); + defer allocator.free(payload); + + var headers = std.ArrayList(std.http.Header).empty; + defer headers.deinit(allocator); + const auth_header = try appendClientHeaders(allocator, &headers, client, true, "text/event-stream"); + defer if (auth_header) |value| allocator.free(value); + + const url = try std.fmt.allocPrint(allocator, "{s}{s}", .{ client.base_url, path }); + defer allocator.free(url); + const uri = try std.Uri.parse(url); + + var req = try client.http.request(.POST, uri, .{ + .redirect_behavior = .unhandled, + .headers = .{ .accept_encoding = .{ .override = "identity" } }, + .extra_headers = headers.items, + }); + defer req.deinit(); + + req.transfer_encoding = .{ .content_length = payload.len }; + var body = try req.sendBodyUnflushed(&.{}); + try body.writer.writeAll(payload); + try body.end(); + try req.connection.?.flush(); + + var response = try req.receiveHead(&.{}); + if (response.head.status.class() != .success) return error.ResponseError; + + var transfer_buffer: [8 * 1024]u8 = undefined; + const reader = response.reader(&transfer_buffer); + parseSseReader(allocator, reader, callback) catch |err| switch (err) { + error.ReadFailed => return response.bodyErr() orelse err, + else => return err, + }; +} + +fn appendClientHeaders(allocator: std.mem.Allocator, headers: *std.ArrayList(std.http.Header), client: *Client, include_content_type: bool, accept: []const u8) !?[]u8 { + if (include_content_type) { + try headers.append(allocator, .{ .name = "Content-Type", .value = "application/json" }); + } + try headers.append(allocator, .{ .name = "Accept", .value = accept }); + + var auth_header: ?[]u8 = null; + if (client.api_key.len > 0) { + auth_header = try std.fmt.allocPrint(allocator, "Bearer {s}", .{client.api_key}); + try headers.append(allocator, .{ .name = "Authorization", .value = auth_header.? }); + } + if (client.organization) |organization| { + try headers.append(allocator, .{ .name = "OpenAI-Organization", .value = organization }); + } + if (client.project) |project| { + try headers.append(allocator, .{ .name = "OpenAI-Project", .value = project }); + } + for (client.default_headers) |header| { + try headers.append(allocator, header); + } + return auth_header; +} + +///////////////// +// Summary: +// Place an order for a pet +// +// Description: +// Place a new order in the store +// +pub fn placeOrder(client: *Client, requestBody: Order) !Owned(Order) { + var result = try placeOrderResult(client, requestBody); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} + +pub fn placeOrderRaw(client: *Client, requestBody: Order) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/store/order", .{client.base_url}); + + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); + const payload: ?[]const u8 = str.written(); + + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); +} + +pub fn placeOrderResult(client: *Client, requestBody: Order) !ApiResult(Order) { + return parseRawResponse(Order, try placeOrderRaw(client, requestBody)); +} + +///////////////// +// Summary: +// uploads an image +// +// Description: +// +// +pub fn uploadFile(client: *Client, petId: i64, additionalMetadata: ?[]const u8, requestBody: []const u8) !Owned(ApiResponse) { + var result = try uploadFileResult(client, petId, additionalMetadata, requestBody); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} + +pub fn uploadFileRaw(client: *Client, petId: i64, additionalMetadata: ?[]const u8, requestBody: []const u8) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet/{d}/uploadImage", .{client.base_url, petId}); + var first_query = true; + if (additionalMetadata) |value| { + try appendQueryParam(&uri_buf.writer, &first_query, "additionalMetadata", value); + } + + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); + const payload: ?[]const u8 = str.written(); + + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); +} + +pub fn uploadFileResult(client: *Client, petId: i64, additionalMetadata: ?[]const u8, requestBody: []const u8) !ApiResult(ApiResponse) { + return parseRawResponse(ApiResponse, try uploadFileRaw(client, petId, additionalMetadata, requestBody)); +} + +///////////////// +// Summary: +// Find pet by ID +// +// Description: +// Returns a single pet +// +pub fn getPetById(client: *Client, petId: i64) !Owned(Pet) { + var result = try getPetByIdResult(client, petId); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} + +pub fn getPetByIdRaw(client: *Client, petId: i64) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet/{d}", .{client.base_url, petId}); + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} + +pub fn getPetByIdResult(client: *Client, petId: i64) !ApiResult(Pet) { + return parseRawResponse(Pet, try getPetByIdRaw(client, petId)); +} + +///////////////// +// Summary: +// Updates a pet in the store with form data +// +// Description: +// +// +pub fn updatePetWithForm(client: *Client, petId: i64, name: ?[]const u8, status: ?[]const u8) !void { + var raw = try updatePetWithFormRaw(client, petId, name, status); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} + +pub fn updatePetWithFormRaw(client: *Client, petId: i64, name: ?[]const u8, status: ?[]const u8) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet/{d}", .{client.base_url, petId}); + var first_query = true; + if (name) |value| { + try appendQueryParam(&uri_buf.writer, &first_query, "name", value); + } + if (status) |value| { + try appendQueryParam(&uri_buf.writer, &first_query, "status", value); + } + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); +} + +///////////////// +// Summary: +// Deletes a pet +// +// Description: +// +// +pub fn deletePet(client: *Client, api_key: []const u8, petId: i64) !void { + var raw = try deletePetRaw(client, api_key, petId); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} + +pub fn deletePetRaw(client: *Client, api_key: []const u8, petId: i64) !RawResponse { + const allocator = client.allocator; + _ = api_key; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet/{d}", .{client.base_url, petId}); + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.DELETE, uri_buf.written(), payload); +} + +///////////////// +// Summary: +// Finds Pets by tags +// +// Description: +// Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. +// +pub fn findPetsByTags(client: *Client, tags: ?[]const u8) !Owned([]const std.json.Value) { + var result = try findPetsByTagsResult(client, tags); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} + +pub fn findPetsByTagsRaw(client: *Client, tags: ?[]const u8) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet/findByTags", .{client.base_url}); + var first_query = true; + if (tags) |value| { + try appendQueryParam(&uri_buf.writer, &first_query, "tags", value); + } + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} + +pub fn findPetsByTagsResult(client: *Client, tags: ?[]const u8) !ApiResult([]const std.json.Value) { + return parseRawResponse([]const std.json.Value, try findPetsByTagsRaw(client, tags)); +} + +///////////////// +// Summary: +// Logs user into the system +// +// Description: +// +// +pub fn loginUser(client: *Client, username: ?[]const u8, password: ?[]const u8) !Owned([]const u8) { + var result = try loginUserResult(client, username, password); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} + +pub fn loginUserRaw(client: *Client, username: ?[]const u8, password: ?[]const u8) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/user/login", .{client.base_url}); + var first_query = true; + if (username) |value| { + try appendQueryParam(&uri_buf.writer, &first_query, "username", value); + } + if (password) |value| { + try appendQueryParam(&uri_buf.writer, &first_query, "password", value); + } + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} + +pub fn loginUserResult(client: *Client, username: ?[]const u8, password: ?[]const u8) !ApiResult([]const u8) { + return parseRawResponse([]const u8, try loginUserRaw(client, username, password)); +} + +///////////////// +// Summary: +// Finds Pets by status +// +// Description: +// Multiple status values can be provided with comma separated strings +// +pub fn findPetsByStatus(client: *Client, status: ?[]const u8) !Owned([]const std.json.Value) { + var result = try findPetsByStatusResult(client, status); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} + +pub fn findPetsByStatusRaw(client: *Client, status: ?[]const u8) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet/findByStatus", .{client.base_url}); + var first_query = true; + if (status) |value| { + try appendQueryParam(&uri_buf.writer, &first_query, "status", value); + } + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} + +pub fn findPetsByStatusResult(client: *Client, status: ?[]const u8) !ApiResult([]const std.json.Value) { + return parseRawResponse([]const std.json.Value, try findPetsByStatusRaw(client, status)); +} + +///////////////// +// Summary: +// Returns pet inventories by status +// +// Description: +// Returns a map of status codes to quantities +// +pub fn getInventory(client: *Client) !Owned(std.json.Value) { + var result = try getInventoryResult(client); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} + +pub fn getInventoryRaw(client: *Client) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/store/inventory", .{client.base_url}); + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} + +pub fn getInventoryResult(client: *Client) !ApiResult(std.json.Value) { + return parseRawResponse(std.json.Value, try getInventoryRaw(client)); +} + +///////////////// +// Summary: +// Get user by user name +// +// Description: +// +// +pub fn getUserByName(client: *Client, username: []const u8) !Owned(User) { + var result = try getUserByNameResult(client, username); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} + +pub fn getUserByNameRaw(client: *Client, username: []const u8) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/user/{s}", .{client.base_url, username}); + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} + +pub fn getUserByNameResult(client: *Client, username: []const u8) !ApiResult(User) { + return parseRawResponse(User, try getUserByNameRaw(client, username)); +} + +///////////////// +// Summary: +// Update user +// +// Description: +// This can only be done by the logged in user. +// +pub fn updateUser(client: *Client, username: []const u8, requestBody: User) !void { + var raw = try updateUserRaw(client, username, requestBody); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} + +pub fn updateUserRaw(client: *Client, username: []const u8, requestBody: User) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/user/{s}", .{client.base_url, username}); + + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); + const payload: ?[]const u8 = str.written(); + + return requestRaw(client, std.http.Method.PUT, uri_buf.written(), payload); +} + +///////////////// +// Summary: +// Delete user +// +// Description: +// This can only be done by the logged in user. +// +pub fn deleteUser(client: *Client, username: []const u8) !void { + var raw = try deleteUserRaw(client, username); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} + +pub fn deleteUserRaw(client: *Client, username: []const u8) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/user/{s}", .{client.base_url, username}); + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.DELETE, uri_buf.written(), payload); +} + +///////////////// +// Summary: +// Create user +// +// Description: +// This can only be done by the logged in user. +// +pub fn createUser(client: *Client, requestBody: User) !void { + var raw = try createUserRaw(client, requestBody); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} + +pub fn createUserRaw(client: *Client, requestBody: User) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/user", .{client.base_url}); + + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); + const payload: ?[]const u8 = str.written(); + + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); +} + +///////////////// +// Summary: +// Creates list of users with given input array +// +// Description: +// Creates list of users with given input array +// +pub fn createUsersWithListInput(client: *Client, requestBody: []const std.json.Value) !Owned(User) { + var result = try createUsersWithListInputResult(client, requestBody); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} + +pub fn createUsersWithListInputRaw(client: *Client, requestBody: []const std.json.Value) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/user/createWithList", .{client.base_url}); + + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); + const payload: ?[]const u8 = str.written(); + + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); +} + +pub fn createUsersWithListInputResult(client: *Client, requestBody: []const std.json.Value) !ApiResult(User) { + return parseRawResponse(User, try createUsersWithListInputRaw(client, requestBody)); +} + +///////////////// +// Summary: +// Add a new pet to the store +// +// Description: +// Add a new pet to the store +// +pub fn addPet(client: *Client, requestBody: Pet) !Owned(Pet) { + var result = try addPetResult(client, requestBody); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} + +pub fn addPetRaw(client: *Client, requestBody: Pet) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet", .{client.base_url}); + + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); + const payload: ?[]const u8 = str.written(); + + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); +} + +pub fn addPetResult(client: *Client, requestBody: Pet) !ApiResult(Pet) { + return parseRawResponse(Pet, try addPetRaw(client, requestBody)); +} + +///////////////// +// Summary: +// Update an existing pet +// +// Description: +// Update an existing pet by Id +// +pub fn updatePet(client: *Client, requestBody: Pet) !Owned(Pet) { + var result = try updatePetResult(client, requestBody); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} + +pub fn updatePetRaw(client: *Client, requestBody: Pet) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet", .{client.base_url}); + + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); + const payload: ?[]const u8 = str.written(); + + return requestRaw(client, std.http.Method.PUT, uri_buf.written(), payload); +} + +pub fn updatePetResult(client: *Client, requestBody: Pet) !ApiResult(Pet) { + return parseRawResponse(Pet, try updatePetRaw(client, requestBody)); +} + +///////////////// +// Summary: +// Find purchase order by ID +// +// Description: +// For valid response try integer IDs with value <= 5 or > 10. Other values will generated exceptions +// +pub fn getOrderById(client: *Client, orderId: i64) !Owned(Order) { + var result = try getOrderByIdResult(client, orderId); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} + +pub fn getOrderByIdRaw(client: *Client, orderId: i64) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/store/order/{d}", .{client.base_url, orderId}); + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} + +pub fn getOrderByIdResult(client: *Client, orderId: i64) !ApiResult(Order) { + return parseRawResponse(Order, try getOrderByIdRaw(client, orderId)); +} + +///////////////// +// Summary: +// Delete purchase order by ID +// +// Description: +// For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors +// +pub fn deleteOrder(client: *Client, orderId: i64) !void { + var raw = try deleteOrderRaw(client, orderId); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} + +pub fn deleteOrderRaw(client: *Client, orderId: i64) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/store/order/{d}", .{client.base_url, orderId}); + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.DELETE, uri_buf.written(), payload); +} + +///////////////// +// Summary: +// Logs out current logged in user session +// +// Description: +// +// +pub fn logoutUser(client: *Client) !void { + var raw = try logoutUserRaw(client); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} + +pub fn logoutUserRaw(client: *Client) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/user/logout", .{client.base_url}); + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} + +pub const resources = struct { + pub const pet = struct { + pub fn addpet(client: *Client, requestBody: Pet) !Owned(Pet) { + return addPet(client, requestBody); + } + pub fn addpetResult(client: *Client, requestBody: Pet) !ApiResult(Pet) { + return addPetResult(client, requestBody); + } + pub fn delete(client: *Client, api_key: []const u8, petId: i64) !void { + return deletePet(client, api_key, petId); + } + pub fn get(client: *Client, petId: i64) !Owned(Pet) { + return getPetById(client, petId); + } + pub fn getResult(client: *Client, petId: i64) !ApiResult(Pet) { + return getPetByIdResult(client, petId); + } + pub fn updatepet_(client: *Client, requestBody: Pet) !Owned(Pet) { + return updatePet(client, requestBody); + } + pub fn updatepet_Result(client: *Client, requestBody: Pet) !ApiResult(Pet) { + return updatePetResult(client, requestBody); + } + pub fn updatepetwithform_(client: *Client, petId: i64, name: ?[]const u8, status: ?[]const u8) !void { + return updatePetWithForm(client, petId, name, status); + } + pub const findbystatus = struct { + pub fn findpetsbystatus(client: *Client, status: ?[]const u8) !Owned([]const std.json.Value) { + return findPetsByStatus(client, status); + } + pub fn findpetsbystatusResult(client: *Client, status: ?[]const u8) !ApiResult([]const std.json.Value) { + return findPetsByStatusResult(client, status); + } + }; + pub const findbytags = struct { + pub fn findpetsbytags(client: *Client, tags: ?[]const u8) !Owned([]const std.json.Value) { + return findPetsByTags(client, tags); + } + pub fn findpetsbytagsResult(client: *Client, tags: ?[]const u8) !ApiResult([]const std.json.Value) { + return findPetsByTagsResult(client, tags); + } + }; + pub const uploadimage = struct { + pub fn uploadfile(client: *Client, petId: i64, additionalMetadata: ?[]const u8, requestBody: []const u8) !Owned(ApiResponse) { + return uploadFile(client, petId, additionalMetadata, requestBody); + } + pub fn uploadfileResult(client: *Client, petId: i64, additionalMetadata: ?[]const u8, requestBody: []const u8) !ApiResult(ApiResponse) { + return uploadFileResult(client, petId, additionalMetadata, requestBody); + } + }; + }; + pub const store = struct { + pub const inventory = struct { + pub fn get(client: *Client) !Owned(std.json.Value) { + return getInventory(client); + } + pub fn getResult(client: *Client) !ApiResult(std.json.Value) { + return getInventoryResult(client); + } + }; + pub const order = struct { + pub fn delete(client: *Client, orderId: i64) !void { + return deleteOrder(client, orderId); + } + pub fn get(client: *Client, orderId: i64) !Owned(Order) { + return getOrderById(client, orderId); + } + pub fn getResult(client: *Client, orderId: i64) !ApiResult(Order) { + return getOrderByIdResult(client, orderId); + } + pub fn placeorder(client: *Client, requestBody: Order) !Owned(Order) { + return placeOrder(client, requestBody); + } + pub fn placeorderResult(client: *Client, requestBody: Order) !ApiResult(Order) { + return placeOrderResult(client, requestBody); + } + }; + }; + pub const user = struct { + pub fn create(client: *Client, requestBody: User) !void { + return createUser(client, requestBody); + } + pub fn delete(client: *Client, username: []const u8) !void { + return deleteUser(client, username); + } + pub fn get(client: *Client, username: []const u8) !Owned(User) { + return getUserByName(client, username); + } + pub fn getResult(client: *Client, username: []const u8) !ApiResult(User) { + return getUserByNameResult(client, username); + } + pub fn update(client: *Client, username: []const u8, requestBody: User) !void { + return updateUser(client, username, requestBody); + } + pub const createwithlist = struct { + pub fn create(client: *Client, requestBody: []const std.json.Value) !Owned(User) { + return createUsersWithListInput(client, requestBody); + } + pub fn createResult(client: *Client, requestBody: []const std.json.Value) !ApiResult(User) { + return createUsersWithListInputResult(client, requestBody); + } + }; + pub const login = struct { + pub fn loginuser(client: *Client, username: ?[]const u8, password: ?[]const u8) !Owned([]const u8) { + return loginUser(client, username, password); + } + pub fn loginuserResult(client: *Client, username: ?[]const u8, password: ?[]const u8) !ApiResult([]const u8) { + return loginUserResult(client, username, password); + } + }; + pub const logout = struct { + pub fn logoutuser(client: *Client) !void { + return logoutUser(client); + } + }; + }; +}; + +pub const pet = resources.pet; +pub const store = resources.store; +pub const user = resources.user; + diff --git a/generated/main.zig b/generated/main.zig index 2106ddf..bd189e4 100644 --- a/generated/main.zig +++ b/generated/main.zig @@ -1,6 +1,9 @@ const std = @import("std"); const v2 = @import("generated_v2.zig"); +const v2_yaml = @import("generated_v2_yaml.zig"); const v3 = @import("generated_v3.zig"); +const v3_yaml = @import("generated_v3_yaml.zig"); +const v31_yaml = @import("generated_v31_yaml.zig"); pub fn main(init: std.process.Init) !void { const allocator = init.gpa; @@ -10,8 +13,18 @@ pub fn main(init: std.process.Init) !void { defer v3_client.deinit(); var v2_client = v2.Client.init(allocator, io, ""); defer v2_client.deinit(); + var v3_yaml_client = v3_yaml.Client.init(allocator, io, ""); + defer v3_yaml_client.deinit(); + var v2_yaml_client = v2_yaml.Client.init(allocator, io, ""); + defer v2_yaml_client.deinit(); + var v31_yaml_client = v31_yaml.Client.init(allocator, io, ""); + defer v31_yaml_client.deinit(); + _ = &v3_yaml_client; + _ = &v2_yaml_client; + _ = &v31_yaml_client; std.debug.print("Generated models build and run !!\n", .{}); + std.debug.print("YAML-generated client modules initialize too.\n", .{}); std.debug.print("Testing memory management in generated functions...\n", .{}); var pet3 = v3.getPetById(&v3_client, 1) catch |err| { diff --git a/src/generator.zig b/src/generator.zig index 808c5f1..ebc9db2 100644 --- a/src/generator.zig +++ b/src/generator.zig @@ -18,7 +18,10 @@ const Extension = enum { JSON, }; -const GeneratorErrors = error{UnsupportedExtension}; +const GeneratorErrors = error{ + UnsupportedExtension, + UnsupportedOpenAPIVersion, +}; pub fn validateExtension(input_file_path: []const u8) !Extension { var buf: [std.fs.max_path_bytes]u8 = undefined; @@ -100,7 +103,7 @@ fn generateCodeFromJsonContents(allocator: std.mem.Allocator, io: std.Io, json_c }, else => { std.debug.print("Unsupported OpenAPI version: {s}\n", .{detector.getOpenApiVersionString(version)}); - return GeneratorErrors.UnsupportedExtension; + return GeneratorErrors.UnsupportedOpenAPIVersion; }, } } @@ -157,3 +160,28 @@ fn generateCodeFromOpenApi32Document(allocator: std.mem.Allocator, io: std.Io, o defer unified_doc.deinit(allocator); try generateCodeFromUnifiedDocument(allocator, io, unified_doc, args); } + +test "unsupported OpenAPI versions return a distinct generator error" { + const test_utils = @import("tests/test_utils.zig"); + + var gpa = test_utils.createTestAllocator(); + const allocator = gpa.allocator(); + + const json_contents = + \\{ + \\ "openapi": "9.9.9", + \\ "info": { + \\ "title": "Unsupported", + \\ "version": "1.0.0" + \\ }, + \\ "paths": {} + \\} + ; + + try std.testing.expectError( + GeneratorErrors.UnsupportedOpenAPIVersion, + generateCodeFromJsonContents(allocator, std.testing.io, json_contents, .{ + .input_path = "unsupported.json", + }), + ); +} diff --git a/src/tests.zig b/src/tests.zig index c1c3283..9f718c8 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -8,6 +8,7 @@ const test_input_loader = @import("tests/test_input_loader.zig"); const yaml_loader_tests = @import("tests/yaml_loader_tests.zig"); const resource_wrapper_tests = @import("tests/resource_wrapper_tests.zig"); const model_typing_tests = @import("tests/model_typing_tests.zig"); +const generator = @import("generator.zig"); comptime { _ = openapi_v3_tests; _ = openapi_v31_tests; @@ -19,4 +20,5 @@ comptime { _ = yaml_loader_tests; _ = resource_wrapper_tests; _ = model_typing_tests; + _ = generator; } diff --git a/src/tests/yaml_loader_tests.zig b/src/tests/yaml_loader_tests.zig index 0dd1e53..300758e 100644 --- a/src/tests/yaml_loader_tests.zig +++ b/src/tests/yaml_loader_tests.zig @@ -143,3 +143,82 @@ test "convert YAML specification to unified document" { try std.testing.expectEqualStrings("Unified YAML API", unified.info.title); try std.testing.expect(unified.paths.count() == 1); } + +test "preserve quoted YAML scalars as JSON strings" { + var gpa = test_utils.createTestAllocator(); + const allocator = gpa.allocator(); + + const yaml_content = + \\openapi: 3.0.3 + \\info: + \\ title: "false" + \\ version: "null" + \\paths: + \\ /pets: + \\ get: + \\ operationId: listPets + \\ responses: + \\ '200': + \\ description: OK + \\components: + \\ schemas: + \\ Sample: + \\ type: object + \\ minimum: "10" + \\ maxLength: "8" + \\ properties: + \\ flag: + \\ type: string + \\ default: "false" + ; + + const json_content = try yaml_loader.yamlToJson(allocator, yaml_content); + defer allocator.free(json_content); + + const parsed = try std.json.parseFromSlice(std.json.Value, allocator, json_content, .{}); + defer parsed.deinit(); + + const root = parsed.value.object; + const info = root.get("info").?.object; + const schemas = root.get("components").?.object.get("schemas").?.object; + const sample = schemas.get("Sample").?.object; + const properties = sample.get("properties").?.object; + const flag = properties.get("flag").?.object; + + try std.testing.expectEqualStrings("false", info.get("title").?.string); + try std.testing.expectEqualStrings("null", info.get("version").?.string); + try std.testing.expectEqualStrings("10", sample.get("minimum").?.string); + try std.testing.expectEqualStrings("8", sample.get("maxLength").?.string); + try std.testing.expectEqualStrings("false", flag.get("default").?.string); +} + +test "preserve folded YAML paragraph breaks and backslashes" { + var gpa = test_utils.createTestAllocator(); + const allocator = gpa.allocator(); + + const yaml_content = + \\openapi: 3.0.3 + \\info: + \\ title: YAML Petstore + \\ description: > + \\ First paragraph + \\ + \\ C:\temp + \\ version: 1.0.0 + \\paths: + \\ /pets: + \\ get: + \\ operationId: listPets + \\ responses: + \\ '200': + \\ description: OK + ; + + const json_content = try yaml_loader.yamlToJson(allocator, yaml_content); + defer allocator.free(json_content); + + var parsed = try models.OpenApiDocument.parseFromJson(allocator, json_content); + defer parsed.deinit(allocator); + + try std.testing.expectEqualStrings("First paragraph\n\nC:\\temp", parsed.info.description.?); +} diff --git a/src/yaml_loader.zig b/src/yaml_loader.zig index 00586a0..1ae3032 100644 --- a/src/yaml_loader.zig +++ b/src/yaml_loader.zig @@ -4,6 +4,7 @@ const yaml = @import("yaml"); const YamlValue = yaml.Yaml.Value; const lbrace_placeholder = "__openapi2zig_lbrace__"; const rbrace_placeholder = "__openapi2zig_rbrace__"; +const unquoted_scalar_placeholder = "__openapi2zig_unquoted_scalar__"; pub const YamlToJsonError = error{ EmptyYamlDocument, @@ -21,7 +22,10 @@ pub fn yamlToJson(allocator: std.mem.Allocator, yaml_content: []const u8) ![]con const normalized_yaml = try normalizeQuotedMapKeys(allocator, folded_yaml); defer allocator.free(normalized_yaml); - var parsed: yaml.Yaml = .{ .source = normalized_yaml }; + const marked_yaml = try markOriginallyUnquotedScalars(allocator, normalized_yaml); + defer allocator.free(marked_yaml); + + var parsed: yaml.Yaml = .{ .source = marked_yaml }; defer parsed.deinit(allocator); try parsed.load(allocator); @@ -62,6 +66,7 @@ fn foldBlockScalars(allocator: std.mem.Allocator, yaml_content: []const u8) ![]c line_index += 1; var block_indent: ?usize = null; var first_block_line = true; + var folded_paragraph_break = false; while (line_index < source_lines.items.len) { const block_line = source_lines.items[line_index]; const trimmed_block_line = std.mem.trim(u8, block_line, " \t\r"); @@ -69,7 +74,13 @@ fn foldBlockScalars(allocator: std.mem.Allocator, yaml_content: []const u8) ![]c if (trimmed_block_line.len != 0 and indent <= header.parent_indent) break; if (trimmed_block_line.len == 0) { - if (!first_block_line and header.style == '|') try appendYamlDoubleQuotedChar(allocator, &out, '\n'); + if (!first_block_line and header.style == '|') { + try appendYamlDoubleQuotedChar(allocator, &out, '\n'); + } else if (!first_block_line and header.style == '>') { + try appendYamlDoubleQuotedChar(allocator, &out, '\n'); + try appendYamlDoubleQuotedChar(allocator, &out, '\n'); + folded_paragraph_break = true; + } line_index += 1; continue; } @@ -80,12 +91,13 @@ fn foldBlockScalars(allocator: std.mem.Allocator, yaml_content: []const u8) ![]c if (!first_block_line) { if (header.style == '|') { try appendYamlDoubleQuotedChar(allocator, &out, '\n'); - } else { + } else if (!folded_paragraph_break) { try out.append(allocator, ' '); } } try appendYamlDoubleQuotedContent(allocator, &out, content); first_block_line = false; + folded_paragraph_break = false; line_index += 1; } @@ -136,6 +148,7 @@ fn appendYamlDoubleQuotedChar(allocator: std.mem.Allocator, out: *std.ArrayList( switch (char) { '\n' => try out.appendSlice(allocator, "\\n"), '\t' => try out.appendSlice(allocator, "\\t"), + '\\' => try out.appendSlice(allocator, "\\\\"), '"' => try out.appendSlice(allocator, "\\\""), else => try out.append(allocator, char), } @@ -282,6 +295,68 @@ fn appendKeyWithEscapedBraces(allocator: std.mem.Allocator, out: *std.ArrayList( } } +fn markOriginallyUnquotedScalars(allocator: std.mem.Allocator, yaml_content: []const u8) ![]const u8 { + var out = std.ArrayList(u8).empty; + errdefer out.deinit(allocator); + + var lines = std.mem.splitScalar(u8, yaml_content, '\n'); + var first_output_line = true; + while (lines.next()) |line| { + if (!first_output_line) try out.append(allocator, '\n'); + first_output_line = false; + try appendMarkedLine(allocator, &out, line); + } + + return try out.toOwnedSlice(allocator); +} + +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 findInlineCommentStart(value: []const u8) ?usize { + for (value, 0..) |char, index| { + if (char == '#' and (index == 0 or value[index - 1] == ' ' or value[index - 1] == '\t')) { + return index; + } + } + return null; +} + +fn shouldMarkOriginallyUnquotedScalar(value: []const u8) bool { + if (value.len == 0) return false; + + switch (value[0]) { + '"', '\'', '|', '>', '[', '{', '&', '*', '!' => return false, + else => {}, + } + + return isNullScalar(value) or parseBoolScalar(value) != null or isJsonNumber(value); +} + fn writeJsonValue(allocator: std.mem.Allocator, writer: *std.Io.Writer, value: YamlValue, current_key: ?[]const u8) !void { switch (value) { .empty => try writer.writeAll("null"), @@ -313,31 +388,48 @@ fn writeJsonValue(allocator: std.mem.Allocator, writer: *std.Io.Writer, value: Y } fn writeJsonScalar(writer: *std.Io.Writer, scalar: []const u8, current_key: ?[]const u8) !void { - const trimmed = std.mem.trim(u8, scalar, " \t\r\n"); - - if (isNullScalar(trimmed)) { - try writer.writeAll("null"); - return; - } + const scalar_info = stripOriginallyUnquotedScalarMarker(scalar); + const trimmed = std.mem.trim(u8, scalar_info.value, " \t\r\n"); - if (parseBoolScalar(trimmed)) |boolean| { - try writer.writeAll(if (boolean) "true" else "false"); - return; - } - - if (current_key) |key| { - if (isFloatSchemaKeyword(key) and isJsonNumber(trimmed)) { - try writer.writeAll(trimmed); - if (!hasFractionOrExponent(trimmed)) try writer.writeAll(".0"); + if (scalar_info.originally_unquoted) { + if (isNullScalar(trimmed)) { + try writer.writeAll("null"); return; } - if (isIntegerSchemaKeyword(key) and isJsonNumber(trimmed)) { - try writer.writeAll(trimmed); + + if (parseBoolScalar(trimmed)) |boolean| { + try writer.writeAll(if (boolean) "true" else "false"); return; } + + if (current_key) |key| { + if (isFloatSchemaKeyword(key) and isJsonNumber(trimmed)) { + try writer.writeAll(trimmed); + if (!hasFractionOrExponent(trimmed)) try writer.writeAll(".0"); + return; + } + if (isIntegerSchemaKeyword(key) and isJsonNumber(trimmed)) { + try writer.writeAll(trimmed); + return; + } + } } - try std.json.Stringify.value(scalar, .{}, writer); + try std.json.Stringify.value(scalar_info.value, .{}, writer); +} + +fn stripOriginallyUnquotedScalarMarker(value: []const u8) struct { value: []const u8, originally_unquoted: bool } { + if (std.mem.startsWith(u8, value, unquoted_scalar_placeholder)) { + return .{ + .value = value[unquoted_scalar_placeholder.len..], + .originally_unquoted = true, + }; + } + + return .{ + .value = value, + .originally_unquoted = false, + }; } fn normalizeYamlKey(allocator: std.mem.Allocator, raw_key: []const u8) ![]const u8 { diff --git a/test/smoke-tests.ps1 b/test/smoke-tests.ps1 index fd9b313..5838b85 100644 --- a/test/smoke-tests.ps1 +++ b/test/smoke-tests.ps1 @@ -4,11 +4,11 @@ Smoke tests for openapi2zig. .DESCRIPTION - Discovers JSON OpenAPI/Swagger specs under openapi/v2.0, openapi/v3.0, - openapi/v3.1, and openapi/v3.2, then for each (spec, resource-wrapper mode) - pair runs `openapi2zig generate` and compile-checks the generated file with - `zig test`. Continues after individual failures and exits non-zero if any - non-denylisted case fails. + Discovers JSON and YAML OpenAPI/Swagger specs under openapi/v2.0, + openapi/v3.0, openapi/v3.1, and openapi/v3.2, then for each + (spec, resource-wrapper mode) pair runs `openapi2zig generate` and + compile-checks the generated file with `zig test`. Continues after + individual failures and exits non-zero if any non-denylisted case fails. Cross-platform: works on PowerShell 7+ (Windows, Linux, macOS). @@ -61,8 +61,8 @@ $OutputDir = Join-Path $RepoRoot "test/output" # Reason - short justification, included in skip output # # Keep this list small and explicit. Each entry is a generator gap to fix. -# This list intentionally starts empty; populate only when CI signal demands -# it and a tracking issue exists. +# Populate only from observed smoke-test failures, and remove entries once the +# underlying parser/generator gap is closed. # --------------------------------------------------------------------------- $Denylist = @( # openapi/v3.0/ingram-micro.json fails the compile phase in every @@ -71,6 +71,12 @@ $Denylist = @( # several shared schemas. Denylisted across all modes via Mode="*" until # the unified model generator dedupes shared/nested type emissions. @{ Spec = "openapi/v3.0/ingram-micro.json"; Mode = "*"; Reason = "duplicate `pub const` emissions from unified model generator (all wrapper modes)" } + @{ Spec = "openapi/v2.0/petstore-expanded.yaml"; Mode = "*"; Reason = "YAML normalization gap causes ParseFailure before generation (all wrapper modes)" } + @{ Spec = "openapi/v2.0/uber.yaml"; Mode = "*"; Reason = "YAML normalization gap causes ParseFailure before generation (all wrapper modes)" } + @{ Spec = "openapi/v3.0/api-with-examples.yaml"; Mode = "*"; Reason = "YAML normalization gap causes ParseFailure before generation (all wrapper modes)" } + @{ Spec = "openapi/v3.0/bot.paths.yaml"; Mode = "*"; Reason = "YAML normalization gap causes ParseFailure before generation (all wrapper modes)" } + @{ Spec = "openapi/v3.0/petstore-expanded.yaml"; Mode = "*"; Reason = "YAML normalization gap causes ParseFailure before generation (all wrapper modes)" } + @{ Spec = "openapi/v3.0/uspto.yaml"; Mode = "*"; Reason = "YAML normalization gap causes ParseFailure before generation (all wrapper modes)" } ) function Test-Denylisted { @@ -134,7 +140,10 @@ New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null # --------------------------------------------------------------------------- $IncludedDirs = @("openapi/v2.0", "openapi/v3.0", "openapi/v3.1", "openapi/v3.2") $Specs = New-Object System.Collections.Generic.List[object] -$SkippedYaml = 0 +$SpecCounts = @{ + Json = 0 + Yaml = 0 +} foreach ($rel in $IncludedDirs) { $abs = Join-Path $RepoRoot $rel @@ -142,17 +151,23 @@ foreach ($rel in $IncludedDirs) { $files = Get-ChildItem -Path $abs -Recurse -File foreach ($f in $files) { $ext = $f.Extension.ToLowerInvariant() - if ($ext -eq ".yaml" -or $ext -eq ".yml") { $SkippedYaml++; continue } - if ($ext -ne ".json") { continue } + if ($ext -notin @(".json", ".yaml", ".yml")) { continue } $relPath = (Resolve-Path -Relative $f.FullName) -replace '\\', '/' $relPath = $relPath -replace '^\./', '' # Pattern match against the slash-normalized relative path. if ($relPath -notlike "*$Filter*" -and ($Filter -ne "*")) { continue } + if ($ext -eq ".json") { + $SpecCounts.Json++ + } + else { + $SpecCounts.Yaml++ + } $Specs.Add([pscustomobject]@{ - Abs = $f.FullName - Rel = $relPath - Version = (Split-Path $rel -Leaf) - Base = [System.IO.Path]::GetFileNameWithoutExtension($f.Name) + Abs = $f.FullName + Rel = $relPath + Version = (Split-Path $rel -Leaf) + Base = [System.IO.Path]::GetFileNameWithoutExtension($f.Name) + Extension = $ext.TrimStart('.').ToLowerInvariant() }) } } @@ -161,10 +176,11 @@ $Specs = $Specs | Sort-Object Rel $totalCases = $Specs.Count * $Modes.Count Write-Host "" -Write-Host "Discovered $($Specs.Count) JSON specs across $($IncludedDirs.Count) directories" +Write-Host "Discovered $($Specs.Count) specs across $($IncludedDirs.Count) directories" +Write-Host "JSON specs: $($SpecCounts.Json)" +Write-Host "YAML specs: $($SpecCounts.Yaml)" Write-Host "Resource-wrapper modes: $($Modes -join ', ')" Write-Host "Total cases to run: $totalCases (sequential)" -Write-Host "Skipped YAML files: $SkippedYaml (unsupported by generator)" Write-Host "Output directory: $OutputDir" Write-Host "" @@ -190,7 +206,7 @@ foreach ($spec in $Specs) { Write-Host "---- $caseLabel" -ForegroundColor Cyan - $outFile = Join-Path $OutputDir (Join-Path $spec.Version ("{0}__{1}.zig" -f $spec.Base, $mode)) + $outFile = Join-Path $OutputDir (Join-Path $spec.Version ("{0}__{1}__{2}.zig" -f $spec.Base, $spec.Extension, $mode)) $outParent = Split-Path -Parent $outFile if (-not (Test-Path $outParent)) { New-Item -ItemType Directory -Path $outParent -Force | Out-Null } diff --git a/vendor/zig-yaml/README.md b/vendor/zig-yaml/README.md index fdeb2a5..4fbd967 100644 --- a/vendor/zig-yaml/README.md +++ b/vendor/zig-yaml/README.md @@ -15,17 +15,17 @@ issues. ## Basic installation The library can be installed using the Zig tools. First, you need to fetch the required release of the library into your project. -``` +```sh zig fetch --save https://github.com/kubkon/zig-yaml/archive/refs/tags/[RELEASE_VERSION].tar.gz ``` It's more convenient to save the library with a desired name, for example, like this (assuming you are targeting latest release of Zig): -``` +```sh zig fetch --save=yaml https://github.com/kubkon/zig-yaml/archive/refs/tags/0.1.1.tar.gz ``` And then add those lines to your project's `build.zig` file: -``` +```zig // add that code after "b.installArtifact(exe)" line const yaml = b.dependency("yaml", .{ .target = target, @@ -76,11 +76,11 @@ defer yaml.deinit(gpa); 1. For untyped, raw representation of YAML, use `Yaml.load`: ```zig -try yaml.load(gpa, source); +try yaml.load(gpa); -try std.testing.expectEqual(untyped.docs.items.len, 1); +try std.testing.expectEqual(yaml.docs.items.len, 1); -const map = untyped.docs.items[0].map; +const map = yaml.docs.items[0].map; try std.testing.expect(map.contains("names")); try std.testing.expectEqual(map.get("names").?.list.len, 3); ``` diff --git a/vendor/zig-yaml/src/Parser.zig b/vendor/zig-yaml/src/Parser.zig index 82cda7b..a4d2bc0 100644 --- a/vendor/zig-yaml/src/Parser.zig +++ b/vendor/zig-yaml/src/Parser.zig @@ -706,6 +706,11 @@ fn parseDoubleQuoted(self: *Parser, gpa: Allocator, raw: []const u8) ParseError! self.string_bytes.appendAssumeCapacity('\t'); string.len += 1; }, + '\\' => { + state = .start; + self.string_bytes.appendAssumeCapacity('\\'); + string.len += 1; + }, '"' => { state = .start; self.string_bytes.appendAssumeCapacity('"'); diff --git a/vendor/zig-yaml/test/test.zig b/vendor/zig-yaml/test/test.zig index 766da9f..683d9b2 100644 --- a/vendor/zig-yaml/test/test.zig +++ b/vendor/zig-yaml/test/test.zig @@ -56,6 +56,9 @@ test "simple" { if (!mem.eql(u8, self.nested.some, other.nested.some)) return false; if (!mem.eql(u8, self.nested.wick, other.nested.wick)) return false; + if (self.nested.ok != other.nested.ok) return false; + if (self.isyaml != other.isyaml) return false; + if (self.hasBoolean != other.hasBoolean) return false; return true; } @@ -111,11 +114,18 @@ const LibTbd = struct { pub fn eql(self: LibTbd, other: LibTbd) bool { if (self.tbd_version != other.tbd_version) return false; if (self.targets.len != other.targets.len) return false; + if (self.uuids.len != other.uuids.len) return false; for (self.targets, 0..) |target, i| { if (!mem.eql(u8, target, other.targets[i])) return false; } + for (self.uuids, 0..) |uuid, i| { + const o_uuid = other.uuids[i]; + if (!mem.eql(u8, uuid.target, o_uuid.target)) return false; + if (!mem.eql(u8, uuid.value, o_uuid.value)) return false; + } + if (!mem.eql(u8, self.install_name, other.install_name)) return false; switch (self.current_version) { @@ -149,6 +159,8 @@ const LibTbd = struct { if (!mem.eql(u8, library, o_library)) return false; } } + } else if (other.reexported_libraries != null) { + return false; } if (self.parent_umbrella) |parent_umbrella| { @@ -167,6 +179,8 @@ const LibTbd = struct { if (!mem.eql(u8, pumbrella.umbrella, o_pumbrella.umbrella)) return false; } + } else if (other.parent_umbrella != null) { + return false; } if (self.exports.len != other.exports.len) return false; @@ -326,6 +340,7 @@ test "multi lib tbd" { }, }; + try testing.expectEqual(expected.len, result.len); for (result, 0..) |lib, i| { try testing.expect(lib.eql(expected[i])); }