+++ title = "Add build.rs — convert api/openapi.yaml to JSON at compile time for Workers embed" priority = 8 status = "todo" ticket_type = "task" dependencies = [] +++ Resolved from TRIAGE ticket 2ec8b1. The `GET /api/` endpoint must serve the OpenAPI spec as JSON. The three strategies were: 1. Compile-time embed (chosen) 2. Runtime load from filesystem — impossible on Cloudflare Workers (no filesystem) 3. utoipa programmatic generation — significant complexity; spec already exists and is complete The chosen approach: a `build.rs` script reads `api/openapi.yaml`, parses it to a `serde_json::Value`, serialises it as compact JSON, and writes the result to `$OUT_DIR/openapi.json`. The `GET /api/` handler then serves this via: ```rust const OPENAPI_JSON: &str = include_str!(concat!(env!("OUT_DIR"), "/openapi.json")); ``` This means: - `serde_yaml` ships only as a `[build-dependencies]` entry — it never enters the Workers binary. - The handler is a zero-overhead static string response with no runtime parsing. - `cargo:rerun-if-changed=api/openapi.yaml` ensures the conversion re-runs whenever the spec is edited — no manual JSON regeneration step needed. - `api/openapi.yaml` remains the single source of truth; the JSON output is ephemeral (in `$OUT_DIR`, not committed to the repository). 1. Create `build.rs` at the `quotesdb/` project root containing: ```rust use std::{env, fs, path::Path}; fn main() { // Re-run this script whenever the OpenAPI spec changes. println!("cargo:rerun-if-changed=api/openapi.yaml"); let yaml = fs::read_to_string("api/openapi.yaml").expect("api/openapi.yaml not found"); // Parse YAML to a generic JSON value, then re-serialise as compact JSON. // serde_yaml is a build-only dependency — it does not appear in the final binary. let value: serde_json::Value = serde_yaml::from_str(&yaml).expect("api/openapi.yaml is invalid YAML"); let json = serde_json::to_string(&value).expect("JSON serialisation failed"); let out_dir = env::var("OUT_DIR").expect("OUT_DIR not set"); let out_path = Path::new(&out_dir).join("openapi.json"); fs::write(&out_path, json).expect("failed to write openapi.json"); } ``` 2. Add the following to `Cargo.toml` (ticket 1f5bb5 should also include this): ```toml [build-dependencies] serde_json = "1" serde_yaml = "0.9" ``` 3. Verify the build succeeds and `$OUT_DIR/openapi.json` is produced: ```sh cargo check # $OUT_DIR is typically target/debug/build/quotesdb-*/out/openapi.json ``` - `serde_yaml` must be a `[build-dependencies]` entry only — NOT in `[dependencies]`. Adding it to `[dependencies]` would bloat the Workers WASM binary. - Do NOT commit `$OUT_DIR/openapi.json` — it is generated automatically at build time. - The `build.rs` file lives at the crate root (same level as `Cargo.toml`), not in `src/`. - `api/openapi.yaml` is the source of truth; do not create or commit an `api/openapi.json`. Ticket 28e7d9 (GET /api/ handler) depends on this ticket. The handler uses `include_str!(concat!(env!("OUT_DIR"), "/openapi.json"))` to serve the spec — see 28e7d9 for the Axum handler implementation. ```sh cargo fmt cargo check cargo clippy cargo test ``` `chore(quotesdb): add build.rs to convert api/openapi.yaml to JSON at compile time` quotesdb/api