Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ADR-0004 — Pack format: YAML skeleton + embedded raw Hurl blocks

Status: Accepted · Date: 2026-07-28

Context

Requirement (stated): macro packs must be human-readable — “is there a better alternative than macro YAML?” Analysis (architecture review §8) evaluated YAML+schema, KDL, TOML, Pkl/CUE/Dhall, RON/JSON5, Rhai/Lua scripting, a custom DSL, and Karate-style Gherkin-native macros against readability/writability, comments, multiline bodies, templating interplay, editor tooling, serde support, and team familiarity (existing packs are YAML with schemars-driven autocomplete). Key insight: the unreadable part of packs was never YAML itself — it was HTTP-as-YAML-trees, while hurl’s own plaintext format is the human-readable HTTP DSL, and the backend team already reads/writes it fluently.

Decision

Packs stay YAML (serde_norway; schemars JSON Schema; comments; block scalars) but only as the thin binding skeleton: macro name, match: pattern, params, defaults, tags, description, composition (use:/with:), step modifiers (optional:, when:, retry:, saveAs:). The HTTP payload of a hurl step is a raw Hurl block:

steps:
  - name: Resolve the record name to its id
    hurl: |
      GET ${url:base}/api/v1/admin/search/records
      Authorization: Bearer ${secret:apiToken}
      [Query]
      q: ${name}
      HTTP 200
      [Captures]
      recordId: jsonpath "$[0].id"

Blocks are validated at pack load by parse_hurl_file after ${…} lowering — real hurl syntax errors with real spans. Structured step trees are reserved for a future non-hurl engine, which would have no native text DSL. Assert-only macros use expect: (merged into the previous request entry — the Then-step rule).

Consequences

Pack bodies are literally hurl: copy-paste flows both ways with the backend corpus; no bespoke assert/capture schema to maintain for the API engine; the emitter for hurl steps approaches the identity function. Costs: autocomplete inside the block is plain-text (mitigated: load-time parse errors are immediate; editors have hurl highlighting; a proef fmt pass can normalize blocks); one lowering pass must run before parse (already required for ${…}).

Alternatives considered

KDL — pleasant syntax but no schema/LSP story comparable to YAML, zero team familiarity; recorded as the fallback if YAML friction materializes. TOML — wrong shape for nested step lists (kept for proef.toml config). Pkl/CUE/Dhall — second language + toolchain, over-architecture at this size. Rhai/Lua — packs become programs; kills static validation and --dry-run guarantees. Custom DSL — a parser/LSP/formatter to own forever. Karate-style callable feature files — collapses the macro/test distinction; a typed params/defaults/validation model is strictly stronger.

Amendment (2026-07-30): the top-level key is macros:

The pack root key was renamed templates: → macros: to end a three-way naming split (the YAML key said templates, the docs and internal model said macro, the file/dir said pack). The entry is now uniformly a macro; a pack is a file of macros. Pure rename — format, schema, and semantics are unchanged (error-corpus snapshots regenerated, diff verified as templates:→macros: only). No templates: alias is kept — one canonical spelling (golden rule: one way to do one thing).