diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 583fb4d..8b94494 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -140,6 +140,31 @@ jobs: manifest-path: ${{ matrix.manifest }} command: check advisories bans licenses sources + wasm: + name: wasm32 build + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Install wasm32 target + run: rustup target add wasm32-unknown-unknown + + - name: Check library on wasm32 + run: cargo check --locked --target wasm32-unknown-unknown + + semver: + name: SemVer compatibility + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Check public API against the latest published release + uses: obi1kenobi/cargo-semver-checks-action@v2 + msrv: name: Rust 1.88 MSRV runs-on: ubuntu-latest diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..48e3862 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,15 @@ +# CLAUDE.md + +Guidance for AI assistants working in this repository. + +## Attribution + +Never attribute work to Claude or any AI assistant, anywhere in this +repository: + +- No `Co-Authored-By` trailers naming an AI in commit messages. +- No claude.ai or session links in commit messages, PR titles or bodies, + issues, or comments. +- Commit author and committer identity must be the repository owner's git + identity (`jskoiz `), never an AI or bot identity. +- Do not mention AI assistance in code comments, docs, or changelogs. diff --git a/Cargo.toml b/Cargo.toml index 1bf1198..6920fb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,8 @@ include = [ "/docs/README.md", "/docs/SUMMARY.md", "/docs/getting-started.md", + "/docs/conformance.md", + "/docs/ROADMAP.md", "/docs/cookbook.md", "/docs/schema-modes.md", "/docs/diagnostics.md", diff --git a/README.md b/README.md index 95288dc..ad0bace 100644 --- a/README.md +++ b/README.md @@ -81,13 +81,20 @@ hand. saneyaml is serde-first **and** YAML 1.2-correct. anchors, ordering, and untouched bytes. - **Benchmarked** — real-world config corpus runs are tracked against `serde_yaml`, `yaml-rust2`, and `saphyr`; see [BENCHMARKS.md](docs/BENCHMARKS.md). +- **Conformance-tracked** — all 402 upstream yaml-test-suite cases run against + this crate and its alternatives, published as a + [live conformance matrix](https://jskoiz.github.io/saneyaml/conformance/index.html); + see [docs/conformance.md](docs/conformance.md). ## Status Pre-1.0 (`0.3.0`), MSRV Rust 1.88, and actively maintained. The public API is a preview surface but is treated as SemVer-visible: breaking changes and MSRV -bumps are explicit, documented release decisions. The road to 1.0 is about -locking the surface down, not expanding it — stability is the goal. +bumps are explicit, documented release decisions, and every PR is gated by +`cargo semver-checks` against the published release. The road to 1.0 is about +locking the surface down, not expanding it — see the +[stability gates](docs/ROADMAP.md). The library also builds for +`wasm32-unknown-unknown` (kept green in CI). ## Documentation @@ -101,7 +108,8 @@ everything else. [Diagnostics](docs/diagnostics.md) · [Untrusted input](docs/untrusted-input.md) · [Editing files](docs/editing.md) · [Streaming](docs/streaming.md) - **Migrating** — [from `serde_yaml`](docs/MIGRATION.md) -- **Reference** — [Compatibility](docs/COMPATIBILITY.md) · +- **Reference** — [Conformance](docs/conformance.md) · + [Compatibility](docs/COMPATIBILITY.md) · [Architecture](docs/ARCHITECTURE.md) · [Benchmarks](docs/BENCHMARKS.md) · [API docs](https://docs.rs/saneyaml) - **Project** — [Security](SECURITY.md) · [Contributing](CONTRIBUTING.md) · diff --git a/contrib/oss-fuzz/Dockerfile b/contrib/oss-fuzz/Dockerfile new file mode 100644 index 0000000..44fc32a --- /dev/null +++ b/contrib/oss-fuzz/Dockerfile @@ -0,0 +1,20 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +################################################################################ + +FROM gcr.io/oss-fuzz-base/base-builder-rust +RUN git clone --depth 1 https://github.com/jskoiz/saneyaml "$SRC/saneyaml" +WORKDIR $SRC/saneyaml +COPY build.sh $SRC/ diff --git a/contrib/oss-fuzz/README.md b/contrib/oss-fuzz/README.md new file mode 100644 index 0000000..393564e --- /dev/null +++ b/contrib/oss-fuzz/README.md @@ -0,0 +1,43 @@ +# OSS-Fuzz integration (staging copy) + +This directory is a ready-to-submit [OSS-Fuzz](https://github.com/google/oss-fuzz) +project definition for saneyaml. The canonical copy lives in the +`google/oss-fuzz` repository once accepted; this staging copy is kept in-tree +so changes to the fuzz harness and the OSS-Fuzz config can be reviewed +together. + +It builds all 11 libFuzzer targets under [`fuzz/`](../../fuzz) with debug +assertions enabled (the targets assert parser invariants, not just +crash-freedom) and ships each target's checked-in corpus as its seed corpus. + +## Submitting + +1. Fork and clone `https://github.com/google/oss-fuzz`. +2. Copy this directory to `projects/saneyaml/` in the fork. +3. Verify locally (requires Docker): + + ```sh + python infra/helper.py build_image saneyaml + python infra/helper.py build_fuzzers --sanitizer address saneyaml + python infra/helper.py check_build saneyaml + python infra/helper.py run_fuzzer saneyaml parse_bytes -- -max_total_time=60 + ``` + +4. Open a PR titled `[saneyaml] Initial integration`. In the description, + cover what OSS-Fuzz acceptance reviewers ask about: what the project is, who + uses it (YAML parsing of untrusted input; serde_yaml replacement path), and + that the primary contact is the maintainer. + +## Notes + +- `primary_contact` in `project.yaml` must be an email associated with a + Google account to receive ClusterFuzz crash reports and access + https://oss-fuzz.com. Additional maintainers can be added later via + `auto_ccs`. +- The Dockerfile clones `main`, so OSS-Fuzz always fuzzes the current default + branch; no release coupling. +- New fuzz targets added under `fuzz/fuzz_targets/` are picked up + automatically by `cargo fuzz list` in `build.sh`. +- OSS-Fuzz requires the Apache-2.0 header on `Dockerfile` and `build.sh` + (Google copyright line included per their contribution guidelines); it does + not affect saneyaml's MIT licensing. diff --git a/contrib/oss-fuzz/build.sh b/contrib/oss-fuzz/build.sh new file mode 100755 index 0000000..523c567 --- /dev/null +++ b/contrib/oss-fuzz/build.sh @@ -0,0 +1,30 @@ +#!/bin/bash -eu +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +################################################################################ + +cd "$SRC/saneyaml/fuzz" + +# The fuzz targets assert parser invariants (round-trip stability, span +# bounds, limit enforcement), so debug assertions stay enabled in the +# optimized build. +cargo fuzz build -O --debug-assertions + +for target in $(cargo fuzz list); do + cp "target/x86_64-unknown-linux-gnu/release/$target" "$OUT/" + if [ -d "corpus/$target" ]; then + zip -j -q "$OUT/${target}_seed_corpus.zip" "corpus/$target"/* + fi +done diff --git a/contrib/oss-fuzz/project.yaml b/contrib/oss-fuzz/project.yaml new file mode 100644 index 0000000..df95659 --- /dev/null +++ b/contrib/oss-fuzz/project.yaml @@ -0,0 +1,8 @@ +homepage: "https://github.com/jskoiz/saneyaml" +main_repo: "https://github.com/jskoiz/saneyaml" +language: rust +primary_contact: "brinakdorser@gmail.com" +fuzzing_engines: + - libfuzzer +sanitizers: + - address diff --git a/docs/README.md b/docs/README.md index 5943d1c..f350d47 100644 --- a/docs/README.md +++ b/docs/README.md @@ -27,6 +27,11 @@ in the repo and open a topic page when you hit it. ### Reference +- **[Conformance](conformance.md)** — all 402 yaml-test-suite cases, with an + [interactive matrix](https://jskoiz.github.io/saneyaml/conformance/index.html) + against other crates. +- **[Road to 1.0](ROADMAP.md)** — stability gates, MSRV and deprecation + policy, and what 1.0 will and won't promise. - **[Compatibility](COMPATIBILITY.md)** — scalar resolution table, divergences, threat model. - **[Architecture](ARCHITECTURE.md)** — crate layout and design decisions. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 0000000..cfbbbaf --- /dev/null +++ b/docs/ROADMAP.md @@ -0,0 +1,46 @@ +# Road to 1.0 + +saneyaml is pre-1.0 (`0.3.x`). The public API is treated as SemVer-visible +today: breaking changes ship only in `0.minor` bumps and are called out in the +[changelog](../CHANGELOG.md). The road to 1.0 is about locking the surface +down, not expanding it. + +## What 1.0 means here + +Releasing 1.0 is a commitment, not a milestone badge: + +- **No breaking API changes** outside a 2.0, including diagnostics text + guarded by tests and the documented `serde_yaml` rename-compatibility + surface in [COMPATIBILITY.md](COMPATIBILITY.md). +- **MSRV policy**: MSRV bumps are minor-version events, documented in the + changelog, and never exceed the oldest Rust release of the prior six + months. +- **Deprecation policy**: anything removed in a future 2.0 is marked + `#[deprecated]` for at least one minor release first. + +## Gates + +Each gate is a verifiable artifact, not an intention: + +| gate | status | +|---|---| +| Full yaml-test-suite imported and classified (402/402) | done — [conformance](conformance.md) | +| Public conformance matrix vs other crates | done — [matrix](https://jskoiz.github.io/saneyaml/conformance/index.html) | +| `cargo semver-checks` gating every PR against the published release | done — CI `semver` job | +| Public API snapshot tracked in-repo | done — [PUBLIC_API.txt](PUBLIC_API.txt) | +| Continuous fuzzing of all targets | staged — [contrib/oss-fuzz](https://github.com/jskoiz/saneyaml/tree/main/contrib/oss-fuzz), pending upstream acceptance | +| API review pass over every `pub` item (naming, sealedness, `#[non_exhaustive]`) | open | +| Real-world migration validation (downstream crates' suites run against saneyaml) | in progress — five crates verified clean | +| `wasm32-unknown-unknown` library build kept green | done — CI `wasm` job | + +When the open gates close, the then-current `0.x` is re-tagged 1.0-rc with a +soak window for migration feedback, then released as 1.0 unchanged unless an +rc bug forces a change. + +## What 1.0 does not promise + +- Byte-identical emitter output across versions outside the documented + `byte_compatible()` corpus. +- Stability of `#[doc(hidden)]` items, including `__unstable_event_serde`. +- Wall-clock or resident-memory guarantees from the resource limits (see + [Untrusted input](untrusted-input.md)). diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 84e0075..6211bd5 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -18,6 +18,8 @@ # Reference +- [Conformance](conformance.md) +- [Road to 1.0](ROADMAP.md) - [Compatibility](COMPATIBILITY.md) - [Architecture](ARCHITECTURE.md) - [Benchmarks](BENCHMARKS.md) diff --git a/docs/conformance.md b/docs/conformance.md new file mode 100644 index 0000000..2788942 --- /dev/null +++ b/docs/conformance.md @@ -0,0 +1,52 @@ +# Conformance + +saneyaml imports **all 402 cases** of the upstream +[yaml-test-suite](https://github.com/yaml/yaml-test-suite) (no skipped or +unselected cases) and classifies every one in +`tests/fixtures/yaml-test-suite/manifest.toml`. The +**[interactive conformance matrix](https://jskoiz.github.io/saneyaml/conformance/index.html)** +(generated into `docs/conformance/index.html`) shows the per-case results side +by side with `serde_yaml`, `yaml-rust2`, and `saphyr`. + +## Headline scores + +The neutral axis is the 400 spec-derived accept / syntax-error cases: a +library passes a case when a full tree/`Value` load accepts a valid document +or rejects an invalid one. The 2 remaining *tree-error* cases are valid YAML +event streams that saneyaml's duplicate-key policy intentionally rejects at +tree loading; they are scored separately so policy differences don't distort +the spec axis. + +| library | spec accept/reject | too strict | too lax | duplicate-key policy | +|---|---:|---:|---:|---:| +| `saneyaml` | **400/400 (100%)** | 0 | 0 | 2/2 | +| `serde_yaml` 0.9.34 | 333/400 (83.25%) | 17 | 50 | 2/2 | +| `yaml-rust2` 0.11.0 | 400/400 (100%) | 0 | 0 | 2/2 | +| `saphyr` 0.0.6 | 400/400 (100%) | 0 | 0 | 0/2 | + +`yaml-rust2` and `saphyr` are strong YAML 1.2 parsers on this axis — the +difference is that they hand you a node tree, while saneyaml pairs the same +acceptance behavior with Serde-first loading, diagnostics, and resource +limits. `serde_yaml`'s gap is the libyaml-era YAML 1.1 acceptance behavior +described in [Compatibility](COMPATIBILITY.md). + +## Reproducing + +Both the matrix page and the numbers above are generated by running the full +corpus, not transcribed by hand: + +```sh +# Regenerate docs/conformance/index.html and print the summary table +cargo run --locked --example conformance_matrix_html + +# Terminal-only head-to-head with per-case mismatch listings +cargo run --locked --example conformance_compare + +# Manifest/coverage invariants (case counts, parity ledgers, divergences) +cargo test --locked --test conformance_dashboard -- --nocapture +``` + +The selection manifest, upstream pin, per-case policies, and divergence +records all live under `tests/fixtures/yaml-test-suite/` and +`tests/fixtures/divergences/`; `tests/conformance_dashboard.rs` fails CI if +the documented counts drift from the fixtures. diff --git a/docs/conformance/index.html b/docs/conformance/index.html new file mode 100644 index 0000000..3960c64 --- /dev/null +++ b/docs/conformance/index.html @@ -0,0 +1,523 @@ + + + + + +saneyaml — YAML test-suite conformance matrix + + + +
+

YAML test-suite conformance matrix

+

Tree/Value loading of all 402 curated +yaml-test-suite cases (pinned at +6ad3d2c62885), scored against the manifest's expected outcome. +A ✓ means the library accepted the input; green/red shows whether that +matches the expectation. Generated by +cargo run --locked --example conformance_matrix_html in the +saneyaml repository.

+
+

saneyaml 0.3.0

400/400 100.0%

0 too strict · 0 too lax · tree policy 2/2

+

serde_yaml 0.9.34+deprecated

333/400 83.2%

51 too strict · 16 too lax · tree policy 2/2

+

yaml-rust2 0.11.0

400/400 100.0%

0 too strict · 0 too lax · tree policy 2/2

+

saphyr 0.0.6

400/400 100.0%

0 too strict · 0 too lax · tree policy 0/2

+
+

Spec score covers the 400 neutral accept / +syntax-error cases. The 2 tree-error cases (amber) are +valid YAML event streams that saneyaml's stricter duplicate-key policy rejects +at tree loading; they are scored separately so no library is penalised on the +neutral axis for a policy difference.

+
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CaseNameExpectedsaneyamlserde_yamlyaml-rust2saphyr
229QSpec Example 2.4. Sequence of Mappingsspec sequence mappingaccept
236Binvalid value after mapping is rejectederror block mapping scalarsyntax-error
26DVWhitespace around colon in mappingsmappingaccept
27NAYAML directive indicatordirective document scalar specaccept
2AUYTags in Block Sequencesequence tagaccept
2CMSover-indented multiline plain scalar mapping value is rejectederror block mapping scalar indentation multilinesyntax-error
2EBWallowed characters in keysblock mapping scalar plain punctuationaccept
2G84/00zero block scalar indentation indicator is rejectederror block scalar indentationsyntax-error
2G84/01two-digit block scalar indentation indicator is rejectederror block scalar indentationsyntax-error
2G84/02Literal modifersliteralaccept
2G84/03Literal modifersliteralaccept
2JQSduplicate missing block mapping keys are rejected by tree loadingblock mapping null duplicate-keytree-error
2LFXreserved directive with warningdirective document scalar comment specaccept
2SXEanchors with colon in nameanchor alias mapping key scalaraccept
2XXWSpec Example 2.25. Unordered Setsspec comment sequence tagaccept
33X3Three explicit integers in a block sequencesequence tagaccept
35KPTags for Root Objectsstream sequence mapping tagaccept
36F6multiline plain scalar with empty lineblock mapping scalar multilineaccept
3ALJBlock Sequence in Block Sequenceblock sequenceaccept
3GZXalias nodesanchor alias mapping scalaraccept
3HFZcontent after document end marker is rejectederror suite-invalidsyntax-error
3MYTPlain Scalar looking like key, comment, anchor and tagscalar plain tag anchor commentaccept
3R3Psingle block sequence with anchoranchor block sequence scalaraccept
3RLN-001escaped tab after folded double-quoted line breakdouble-quoted scalar tab foldaccept
3RLN-002tab as double-quoted continuation indentationdouble-quoted scalar tab foldaccept
3RLN/00leading tab content in double-quoted scalardouble-quoted scalar tab foldaccept
3RLN/03leading tab with following spaces in double-quoted scalardouble-quoted scalar tab foldaccept
3RLN/04escaped leading tab with following spaces in double-quoted scalardouble-quoted scalar tab foldaccept
3RLN/05leading tab folded away in double-quoted scalardouble-quoted scalar tab foldaccept
3UYSEscaped slash in double quotessuite-validaccept
4ABKFlow Mapping Separate Valuesflow mappingaccept
4CQQmulti-line flow scalarsblock mapping scalar quoted multilineaccept
4EJStab indentation is rejectederror indentationsyntax-error
4FJ6Nested implicit complex keyssuite-validaccept
4GC6Spec Example 7.7. Single Quoted Charactersspec single-quotedaccept
4H7Kextra flow sequence closing bracket is rejectederror suite-invalidsyntax-error
4HVUwrong-indented sequence is rejectederror suite-invalidsyntax-error
4JVGduplicate anchor property on one node is rejectederror anchor mappingsyntax-error
4MUZ/00Flow mapping colon on line after keyflow mappingaccept
4MUZ/01Flow mapping colon on line after keyflow mappingaccept
4MUZ/02Flow mapping colon on line after keyflow mappingaccept
4Q9Ffolded block scalar with empty lines and explicit startdocument scalar folded block chompingaccept
4QFQSpec Example 8.2. Block Indentation Indicator [1.3]spec block indentationaccept
4RWCTrailing spaces after flow collectionflowaccept
4UYUColon in Double Quoted Stringdouble-quotedaccept
4V8UPlain scalar with backslashesscalar plainaccept
4WA9Literal scalarsscalar literalaccept
4ZYMSpec Example 6.4. Line Prefixesspecaccept
52DLExplicit Non-Specific Tag [1.3]sequence tagaccept
54T7Flow Mappingflow mappingaccept
55WFinvalid double-quoted escape is rejectederror suite-invalidsyntax-error
565NConstruct Binarymapping tagaccept
57H4block collection nodesblock mapping sequence tagaccept
58MPflow mapping adjacent colon-prefixed valueflow mapping scalar plainaccept
5BVJSpec Example 5.7. Block Scalar Indicatorsspec block scalaraccept
5C5MSpec Example 7.15. Flow Mappingsspec flow mappingaccept
5GBFempty lines in flow and block scalarsblock mapping scalar quoted literal whitespace multilineaccept
5KJESpec Example 7.13. Flow Sequencespec flow sequenceaccept
5LLUwrongly indented block scalar content after spaces-only lines is rejectederror block scalar folded indentation whitespacesyntax-error
5MUDcolon and adjacent value on next lineflow mapping scalar quoted multilineaccept
5NYZSpec Example 6.9. Separated Commentspec commentaccept
5T43colon at beginning of adjacent flow scalarflow sequence mapping scalar quoted plainaccept
5TRBinvalid document-start marker in double-quoted scalar is rejectederror suite-invalidsyntax-error
5TYMSpec Example 6.21. Local Tag Prefixspec stream directive comment sequence tagaccept
5U3Asequence on same line as mapping key is rejectederror mapping sequencesyntax-error
5WE3explicit block mapping entriesblock mapping explicit scalar sequenceaccept
62EZflow mapping followed by adjacent plain content is rejectederror flow mapping scalarsyntax-error
652ZQuestion mark at start of flow keyflow mappingaccept
65WHSingle Entry Block Sequenceblock sequenceaccept
6BCTtabs may separate tokensblock sequence mapping tab separationaccept
6BFJmapping, key and flow sequence item anchorsanchor complex-key flow mapping sequenceaccept
6CA3tab-indented top-level flow sequenceflow sequence tab separationaccept
6CK3tag shorthand suffix escapesdirective tag sequence scalaraccept
6FWRliteral keep block scalar preserves spaces-only content linedocument scalar literal block chompingaccept
6H3VBackslashes in singlequotessuite-validaccept
6HB6Spec Example 6.1. Indentation Spacesspec indentationaccept
6JQWSpec Example 2.13. In literals, newlines are preservedspec literalaccept
6JTTunclosed nested flow sequence is rejectederror flow sequencesyntax-error
6JWBTags for Block Objectssequence mapping tagaccept
6KGNanchor for empty nodeanchor alias mapping nullaccept
6LVFreserved directives are ignoreddirective document scalaraccept
6M2Faliases in explicit block mappingevent anchor alias mapping explicitaccept
6PBEzero-indented sequences in explicit mapping keysblock mapping explicit sequence complex-keyaccept
6S55invalid scalar after sequence is rejectederror suite-invalidsyntax-error
6SLAAllowed characters in quoted mapping keymappingaccept
6VJKfolded block scalar keeps paragraph and more-indented breaksscalar folded block indentaccept
6WLZSpec Example 6.18. Primary Tag Handle [1.3]spec stream directive comment sequence mapping tagaccept
6WPFsame-indent root double-quoted scalar continuationdouble-quoted scalar foldaccept
6XDYTwo document start markersdocumentaccept
6ZKBdocument stream with mid-stream YAML directivedirective stream document mapping null specaccept
735YSpec Example 8.20. Block Node Typesspec comment sequence mapping tagaccept
74H7Tags in Implicit Mappingmapping tagaccept
753EBlock Scalar Strip [1.3]block scalaraccept
7A4ESpec Example 7.6. Double Quoted Linesspec double-quotedaccept
7BMTnode and mapping key anchorsanchor mapping key scalar commentaccept
7BUBspec example node referenced by aliasanchor alias mapping sequence scalar commentaccept
7FWLSpec Example 6.24. Verbatim Tagsspec mapping tagaccept
7LBHmultiline double-quoted mapping key indentation is rejectederror block mapping scalar double-quoted multilinesyntax-error
7MNFmissing mapping value after top-level key is rejectederror block mapping scalarsyntax-error
7T8Xfolded block scalar with leading blank and indented list-like linesscalar folded block indentaccept
7TMGcomment in multiline flow sequenceflow sequence comment multilineaccept
7W2Pmissing values resolve to nullblock mapping nullaccept
7Z25Bare document after document end markerstream sequence mappingaccept
7ZZ5Empty flow collectionsflow emptyaccept
82ANThree dashes and content without spacesuite-validaccept
87E4Spec Example 7.8. Single Quoted Implicit Keysspec single-quotedaccept
8CWCPlain mapping key ending with colonmapping plainaccept
8G76Spec Example 6.10. Comment Linesspec stream commentaccept
8KB6multiline plain flow mapping key without valueflow mapping scalar plain multilineaccept
8MK2Explicit Non-Specific Tagtagaccept
8QBEBlock Sequence in Block Mappingblock sequence mappingaccept
8UDBSpec Example 7.14. Flow Sequence Entriesspec flow sequenceaccept
8XDJcomment inside multiline plain mapping value is rejectederror comment mapping scalar multilinesyntax-error
8XYNAnchor with unicode charactersequence anchoraccept
93JHBlock Mappings in Block Sequenceblock sequence mappingaccept
93WFstrip folded block scalar with spaces-only blank lines and explicit startdocument scalar folded block chomping indentaccept
96L6Spec Example 2.14. In the folded scalars, newlines become spacesspec scalar foldedaccept
96NN/00leading tab content in literal scalarblock mapping scalar literal tabaccept
96NN/01leading tab content in literal scalarblock mapping scalar literal tabaccept
98YDSpec Example 5.5. Comment Indicatorspec stream commentaccept
9BXHMultiline doublequoted flow mapping key without valueflow mappingaccept
9C9Nwrong indented flow sequence is rejectederror flow indent sequencesyntax-error
9CWYmissing mapping value after block sequence is rejectederror block mapping sequencesyntax-error
9DXLmapping document stream with mid-stream YAML directivedirective stream document mapping null specaccept
9FMGMulti-level Mapping Indentmapping indentationaccept
9HCYTAG directive after implicit document content is rejectederror directive tag documentsyntax-error
9J7ASimple Mapping Indentmapping indentationaccept
9JBAcomment must be separated from flow sequence enderror comment flow sequencesyntax-error
9KAXvarious combinations of tags and anchorstag anchor document mapping scalaraccept
9KBCblock mapping on document start line is rejectederror document mappingsyntax-error
9MAGleading comma in flow sequence is rejectederror suite-invalidsyntax-error
9MMAbare YAML directive without document start is rejectederror directive documentsyntax-error
9MMWsingle pair implicit entriesflow sequence mapping implicit complex-keyaccept
9MQT/00Scalar doc with '...' in contentscalaraccept
9MQT/01document end marker inside scalar document is rejectederror suite-invalidsyntax-error
9SA2multiline double quoted flow mapping keyflow mapping scalar quoted multilineaccept
9SHHSpec Example 5.8. Quoted Scalar Indicatorsspec scalaraccept
9TFXSpec Example 7.6. Double Quoted Lines [1.3]spec double-quotedaccept
9U5KSpec Example 2.12. Compact Nested Mappingspec mappingaccept
9WXWSpec Example 6.18. Primary Tag Handlespec stream directive comment sequence mapping tagaccept
9YRDMultiline Scalar at Top Levelscalaraccept
A2M4Spec Example 6.2. Indentation Indicatorsspec sequence mappingaccept
A6F9Spec Example 8.4. Chomping Final Line Breakspecaccept
A984multiline scalar in mappingblock mapping scalar multilineaccept
AB8Usequence entry looking continuationblock sequence scalar plain multiline indentationaccept
AVM7Empty Streamstream emptyaccept
AZ63Sequence With Same Indentation as Parent Mappingsequence mapping indentationaccept
AZW3Lookahead test casessuite-validaccept
B3HGSpec Example 8.9. Folded Scalar [1.3]spec scalar foldedaccept
B63PYAML directive before document end marker is rejectederror directive documentsyntax-error
BD7Linvalid mapping after sequence is rejectederror suite-invalidsyntax-error
BEC7YAML directive with unsupported version warningdirective document scalar specaccept
BF9Htrailing comment in multiline plain scalar is rejectederror comment mapping scalar multilinesyntax-error
BS4Kcomment between top-level plain scalar lines is rejectederror comment scalar multilinesyntax-error
BU8Lnode anchor and tag on separate linesevent anchor tag mappingaccept
C2DTflow mapping adjacent valuesflow mapping scalar quoted null specaccept
C2SPmultiline flow collection key is rejectederror flow mapping multilinesyntax-error
C4HZSpec Example 2.24. Global Tagsspec directive comment flow sequence mapping tag anchor aliasaccept
CC74Spec Example 6.20. Tag Handlesspec directive sequence mapping tagaccept
CFD4empty implicit key in single pair flow sequencesflow sequence mapping empty-keyaccept
CML9missing comma in flow sequence is rejectederror flow sequence commentsyntax-error
CN3Ranchors at various flow-sequence locationsanchor flow sequence mapping scalaraccept
CPZ3double-quoted scalar starting with a tabdouble-quoted mapping scalar tabaccept
CQ3Wunclosed double-quoted string is rejectederror suite-invalidsyntax-error
CT4Qmultiline explicit key single pair flow mapping in flow sequenceflow sequence mapping explicit multilineaccept
CTN5extra comma in flow sequence is rejectederror flow sequencesyntax-error
CUP7node property indicatorsanchor alias tag mapping scalaraccept
CVW2comment-looking flow sequence entry requires separationerror comment flow sequencesyntax-error
CXX2anchored block mapping on document start line is rejectederror document mapping anchorsyntax-error
D49Qmultiline single-quoted mapping key indentation is rejectederror block mapping scalar single-quoted multilinesyntax-error
D83LBlock scalar indicator orderblock scalaraccept
D88JFlow Sequence in Block Mappingflow block sequence mappingaccept
D9TUSingle Pair Block Mappingblock mappingaccept
DBG4Spec Example 7.10. Plain Charactersspec comment flow sequence mapping tagaccept
DC7Xtrailing tabs around mapping and sequence contentblock mapping sequence scalar tab trailingaccept
DE56/00escaped trailing tab folds in double-quoted scalardouble-quoted scalar tab foldaccept
DE56/01escaped trailing tab with spaces folds in double-quoted scalardouble-quoted scalar tab foldaccept
DE56/02escaped line-end tab folds in double-quoted scalardouble-quoted scalar tab foldaccept
DE56/03escaped line-end tab with spaces folds in double-quoted scalardouble-quoted scalar tab foldaccept
DE56/04literal trailing tab folds in double-quoted scalardouble-quoted scalar tab foldaccept
DE56/05literal trailing tab with spaces folds in double-quoted scalardouble-quoted scalar tab foldaccept
DFF7Spec Example 7.16. Flow Mapping Entriesspec flow mappingaccept
DHP8flow sequenceflow sequence scalaraccept
DK3Jzero-indented folded block scalar with comment-looking contentdocument scalar folded commentaccept
DK4Himplicit flow sequence key followed by newline is rejectederror flow mapping sequencesyntax-error
DK95/00space-tab content after mapping key is acceptedblock mapping scalar indentation tabaccept
DK95/01tab-looking indentation in multiline quoted scalar is rejectederror double-quoted mapping scalar tabsyntax-error
DK95/02space-tab double-quoted continuation indentation is accepteddouble-quoted mapping scalar indentation tabaccept
DK95/03space-tab blank line before mapping is acceptedblank-line block mapping scalar tabaccept
DK95/04tab-only blank line between mapping entries is acceptedblank-line block mapping scalar tabaccept
DK95/05space-tab blank line between mapping entries is acceptedblank-line block mapping scalar tabaccept
DK95/06tab-looking indentation under nested mapping is rejectederror block mapping indentation tabsyntax-error
DK95/07tab-only line before explicit document start is acceptedblank-line document tabaccept
DK95/08tabs in double-quoted folded scalar content are accepteddouble-quoted mapping scalar fold tabaccept
DMG6wrong-indented mapping is rejectederror suite-invalidsyntax-error
DWX9Spec Example 8.8. Literal Contentspec literalaccept
E76Zaliases in implicit block mappinganchor alias mapping key scalaraccept
EB22YAML directive after scalar document content is rejectederror directive document scalarsyntax-error
EHF6Tags for Flow Objectsflow mapping tagaccept
EW3Vwrong-indented mapping entry is rejectederror suite-invalidsyntax-error
EX5HMultiline Scalar at Top Level [1.3]scalaraccept
EXG3Three dashes and content without space [1.3]suite-validaccept
F2C7Anchors and Tagssequence tag anchoraccept
F3CPNested flow collections on one lineflowaccept
F6MCmore indented folded block scalar linesblock scalar folded indentationaccept
F8F9chomping trailing linesblock scalar chomping commentaccept
FBC9allowed characters in plain scalarsblock mapping scalar plain punctuation multilineaccept
FH7JTags on Empty Scalarssequence mapping tagaccept
FP8Rzero-indented folded block scalar after document startdocument scalar folded indentaccept
FQ7FSpec Example 2.1. Sequence of Scalarsspec sequence scalaraccept
FRK4Spec Example 7.3. Completely Empty Flow Nodesspec flow mappingaccept
FTA2root sequence anchor after document startevent document anchor sequenceaccept
FUP4Flow Sequence in Flow Sequenceflow sequenceaccept
G4RSSpec Example 2.17. Quoted Scalarsspec scalaraccept
G5U8plain dash entries in flow sequence are rejectederror suite-invalidsyntax-error
G7JEmultiline plain mapping key indentation is rejectederror block mapping scalar indentation multilinesyntax-error
G992Spec Example 8.9. Folded Scalarspec scalar foldedaccept
G9HCinvalid anchor in zero-indented sequence is rejectederror anchor sequencesyntax-error
GDY7comment-looking mapping key is rejectederror suite-invalidsyntax-error
GH63Mixed Block Mapping (explicit to implicit)block mappingaccept
GT5Mnode anchor in sequence is rejectederror anchor sequencesyntax-error
H2RWBlank linessuite-validaccept
H3Z8Literal unicodeliteralaccept
H7J7unindented node anchor is rejectederror suite-invalidsyntax-error
H7TQextra words on YAML directive are rejectederror suite-invalidsyntax-error
HM87/00Scalars in flow start with syntax charflow mappingaccept
HM87/01Scalars in flow start with syntax charflowaccept
HMK4Spec Example 2.16. Indentation determines scopespec indentationaccept
HMQ5Spec Example 6.23. Node Propertiesspec mapping tag anchor aliasaccept
HRE5invalid double-quoted single-quote escape is rejectederror suite-invalidsyntax-error
HS5TSpec Example 7.12. Plain Linesspec plainaccept
HU3Pmapping value inserted into multiline plain scalar is rejectederror block mapping scalar multilinesyntax-error
HWV9Document-end markerstreamaccept
J3BTSpec Example 5.12. Tabs and Spacesspec tabaccept
J5UCMultiple Pair Block Mappingblock mappingaccept
J7PZSpec Example 2.26. Ordered Mappingsspec comment sequence mapping tagaccept
J7VCEmpty Lines Between Mapping Elementsmapping emptyaccept
J9HZSpec Example 2.9. Single Document with Two Commentsspec document commentaccept
JEF9/00Trailing whitespace in streamssuite-validaccept
JEF9/01Trailing whitespace in streamssuite-validaccept
JEF9/02Trailing whitespace in streamssequenceaccept
JHB9two documents in streamstream documentaccept
JKF3unindented multiline double-quoted block key is rejectederror suite-invalidsyntax-error
JQ4RSpec Example 8.14. Block Sequencespec block sequenceaccept
JR7VQuestion marks in scalarsflow sequence mappingaccept
JS2JSpec Example 6.29. Node Anchorsspec anchoraccept
JTV5Block Mapping with Multiline Scalarsblock mapping scalaraccept
JY7Ztrailing mapping-looking text after double-quoted scalar is rejectederror double-quoted mapping scalarsyntax-error
K3WXColon and adjacent value after comment on next linecomment flow sequence mappingaccept
K4SUMultiple Entry Block Sequenceblock sequenceaccept
K527strip folded block scalar with spaces-only blank linesscalar folded block chomping indentaccept
K54UTab after document headersequenceaccept
K858empty block scalar chompingmapping scalar folded literal block chompingaccept
KH5V-001escaped inline tab in double-quoted scalardouble-quoted scalar tab escapeaccept
KH5V/00inline tab in double-quoted scalardouble-quoted scalar tabaccept
KH5V/02inline escaped tab in double-quoted scalardouble-quoted scalar tab escapeaccept
KK5PVarious combinations of explicit block mappingsblock mappingaccept
KMK3Block Submappingblock mappingaccept
KS4Uextra item after flow sequence end is rejectederror suite-invalidsyntax-error
KSS4same-indent double-quoted scalar before next documentdouble-quoted scalar stream anchoraccept
L24T/00Trailing line of spacessuite-validaccept
L24T/01Trailing line of spacesmappingaccept
L383Two scalar docs with trailing commentsscalar commentaccept
L94MTags in Explicit Mappingmapping tagaccept
L9U5Spec Example 7.11. Plain Implicit Keysspec plainaccept
LE5ASpec Example 7.24. Flow Nodesspec sequence tag anchor aliasaccept
LHL4invalid tag is rejectederror suite-invalidsyntax-error
LP6EWhitespace After Scalars in Flowflow scalaraccept
LQZ7Spec Example 7.4. Double Quoted Implicit Keysspec double-quotedaccept
LX3PImplicit Flow Mapping Key on one lineflow mappingaccept
M29MLiteral Block Scalarblock scalar literalaccept
M2N8/00question mark edge case with empty compact mapping keyblock sequence mapping explicit empty-key compactaccept
M2N8/01Question mark edge casessuite-validaccept
M5C3block scalar nodes with separated tagblock scalar tag indentationaccept
M5DYSpec Example 2.11. Mapping between Sequencesspec sequence mappingaccept
M6YHBlock sequence indentationblock sequence indentationaccept
M7A3bare documentsstream document scalar plain literalaccept
M7NXNested flow collectionsflowaccept
M9B4Spec Example 8.7. Literal Scalarspec scalar literalaccept
MJS9Spec Example 6.7. Block Foldingspec blockaccept
MUS6/00YAML 1.1 directive without required separationdirective document errorsyntax-error
MUS6/01YAML directive after document startdirective document errorsyntax-error
MUS6/02YAML 1.1 directive with extra separationdirective document nullaccept
MUS6/03YAML 1.1 directive with tab separationdirective document null tabaccept
MUS6/04YAML 1.1 directive with trailing commentdirective document null commentaccept
MUS6/05reserved directive with short YAML spellingdirective document nullaccept
MUS6/06reserved directive with long YAML spellingdirective document nullaccept
MXS3Flow Mapping in Block Sequenceflow block sequence mappingaccept
MYW6Block Scalar Stripblock scalaraccept
MZX3scalar style fidelityevent scalar quoted foldedaccept
N4JPbad mapping indentation is rejectederror suite-invalidsyntax-error
N782document markers inside flow style are rejectederror suite-invalidsyntax-error
NAT4Various empty or newline only quoted stringsemptyaccept
NB6Ztabs on empty lines in multiline plain scalarblock mapping scalar plain multiline blank-line tabaccept
NHX8Empty Lines at End of Documentmappingaccept
NJ66Multiline plain flow mapping keyflow sequence mappingaccept
NKF9Empty keys in block and flow mappingstream comment flow sequence mappingaccept
NP9HSpec Example 7.5. Double Quoted Line Breaksspec double-quotedaccept
P2ADblock scalar headerblock scalar indentation chompingaccept
P2EQsame-line sequence item is rejectederror suite-invalidsyntax-error
P76LSpec Example 6.19. Secondary Tag Handlespec directive comment sequence mapping tagaccept
P94KSpec Example 6.11. Multi-Line Commentsspec commentaccept
PBJ2Spec Example 2.3. Mapping Scalars to Sequencesspec sequence mapping scalaraccept
PRH3Spec Example 7.9. Single Quoted Linesspec single-quotedaccept
PUW8Document start on last linedocumentaccept
PW8Xanchors on empty scalarsanchor mapping sequence null explicitaccept
Q4CLtrailing text after double-quoted scalar is rejectederror double-quoted mapping scalarsyntax-error
Q5MGtab before root flow mappingflow mapping tab separationaccept
Q88ASpec Example 7.23. Flow Contentspec flowaccept
Q8ADSpec Example 7.5. Double Quoted Line Breaks [1.3]spec double-quotedaccept
Q9WFSpec Example 6.12. Separation Spacesspec comment flow mappingaccept
QB6Ewrong-indented multiline quoted scalar is rejectederror double-quoted mapping scalar indentationsyntax-error
QF4Ymultiline single pair flow mapping in flow sequenceflow sequence mapping implicit multilineaccept
QLJ7TAG handle used after document scope is rejectederror suite-invalidsyntax-error
QT73Comment and document-end markerstream commentaccept
R4YGblock scalar detected indentationsequence scalar folded literal block indent tabaccept
R52LNested flow mapping sequence and mappingsflow sequence mappingaccept
RHX7YAML directive after mapping document content is rejectederror directive document mappingsyntax-error
RLU9Sequence Indentsequence indentationaccept
RR7FMixed Block Mapping (implicit to explicit)block mappingaccept
RTP8Spec Example 9.2. Document Markersspec directive comment sequenceaccept
RXY3document end marker in single-quoted scalar is rejectederror suite-invalidsyntax-error
RZP5various trailing comments with same-line anchorcomment mapping sequence explicit anchor double-quoted plain foldedaccept
RZT7Spec Example 2.28. Log Filespecaccept
S3PDimplicit block mapping entriesblock mapping sequence empty-keyaccept
S4GJtrailing text after block scalar indicator is rejectederror suite-invalidsyntax-error
S4JQexplicit non-specific tagevent tag scalaraccept
S4T7Document with footerdocumentaccept
S7BGColon followed by commasuite-validaccept
S98Zover-indented first block scalar content line is rejectederror suite-invalidsyntax-error
S9E8Spec Example 5.3. Block Structure Indicatorsspec blockaccept
SBG9Flow Sequence in Flow Mappingflow sequence mappingaccept
SF5Vduplicate YAML directive is rejectederror suite-invalidsyntax-error
SKE5anchor before zero-indented sequenceanchor indent sequenceaccept
SM9W/00Single character streamssuite-validaccept
SM9W/01Single character streamsmappingaccept
SR86anchor plus alias is rejectederror anchor aliassyntax-error
SSW6Spec Example 7.7. Single Quoted Characters [1.3]spec single-quotedaccept
SU5Zadjacent comment after double-quoted scalar is rejectederror suite-invalidsyntax-error
SU74anchor and alias mapping key is rejectederror suite-invalidsyntax-error
SY6Vanchor before sequence entry on same line is rejectederror anchor sequencesyntax-error
SYW4Spec Example 2.2. Mapping Scalars to Scalarsspec mapping scalaraccept
T26HSpec Example 8.8. Literal Content [1.3]spec literalaccept
T4YYSpec Example 7.9. Single Quoted Lines [1.3]spec single-quotedaccept
T5N4Spec Example 8.7. Literal Scalar [1.3]spec scalar literalaccept
T833missing comma in flow mapping is rejectederror flow mappingsyntax-error
TD5Ninvalid scalar after sequence is rejectederror suite-invalidsyntax-error
TE2ASpec Example 8.16. Block Mappingsspec block mappingaccept
TL85Spec Example 6.8. Flow Foldingspec flowaccept
TS54folded block scalar with empty linesscalar folded block chompingaccept
U3C3TAG directive resolves tag handlesdirective tag scalar quotedaccept
U3XVnode and mapping key anchorsanchor mapping key scalaraccept
U44Rbad mapping indentation variant is rejectederror suite-invalidsyntax-error
U99Rinvalid comma in tag is rejectederror suite-invalidsyntax-error
U9NSSpec Example 2.8. Play by Play Feed from a Gamespecaccept
UDM2Plain URL in flow mappingflow sequence mappingaccept
UDR7Spec Example 5.4. Flow Collection Indicatorsspec flowaccept
UGM3Spec Example 2.27. Invoicespec comment sequence mapping tag anchor aliasaccept
UKK6/00colon-only compact sequence mappingblock sequence mapping empty-key compactaccept
UKK6/01Syntax character edge casessuite-validaccept
UKK6/02bare explicit non-specific tagtag scalar nullaccept
UT92explicit documents with directive-looking flow keyflow mapping stream document directive comment specaccept
UV7Qlegal tab after indentationblock sequence scalar indentation tabaccept
V55RAliases in Block Sequenceblock sequence aliasaccept
V9D5compact block mappingsblock mapping explicit sequence compactaccept
VJP3/00wrong-indented multiline flow collection is rejectederror suite-invalidsyntax-error
VJP3/01Flow collections over many linesflow mappingaccept
W42USpec Example 8.15. Block Sequence Entry Typesspec block sequenceaccept
W4TNYAML directive and explicit document boundariesdirective stream document scalar literalaccept
W5VHAllowed characters in aliasmapping anchor aliasaccept
W9L4over-indented literal scalar first line is rejectederror suite-invalidsyntax-error
WZ62Spec Example 7.2. Empty Contentspec flow mapping tagaccept
X38Waliases in flow objectsanchor alias complex-key flow mapping sequence duplicate-keytree-error
X4QWadjacent comment after block scalar indicator is rejectederror suite-invalidsyntax-error
X8DWExplicit key and value seperated by commentcommentaccept
XLQ9plain scalar line that looks like a YAML directivedocument scalar plain directive multilineaccept
XV9VSpec Example 6.5. Empty Lines [1.3]spec emptyaccept
XW4Dvarious trailing commentscomment mapping sequence explicit anchor double-quoted plain foldedaccept
Y2GNanchor with colon in the middleanchor mapping scalaraccept
Y79Ytab-only block scalar content line is rejectederror block scalar indentation tabsyntax-error
Y79Y/001space then tab block scalar content line is acceptedblock scalar indentation tabaccept
Y79Y/002tab-only flow sequence separation line is acceptedflow sequence tab separationaccept
Y79Y/003tab in flow sequence indentation is rejectederror flow sequence indentation tabsyntax-error
Y79Y/004tab after block sequence indicator is rejectederror block sequence indentation tabsyntax-error
Y79Y/005tab after block sequence indicator separation is rejectederror block sequence indentation tabsyntax-error
Y79Y/006tab after explicit key indicator is rejectederror block mapping explicit indentation tabsyntax-error
Y79Y/007tab after explicit mapping value indicator is rejectederror block mapping indentation tabsyntax-error
Y79Y/008tab after explicit key indicator before mapping key is rejectederror block mapping explicit indentation tabsyntax-error
Y79Y/009tab after explicit mapping value indicator before nested key is rejectederror block mapping indentation tabsyntax-error
Y79Y/010tab before negative scalar is acceptedblock sequence scalar tab separationaccept
YD5XSpec Example 2.5. Sequence of Sequencesspec sequenceaccept
YJV2dash in flow sequence is rejectederror flow sequence scalarsyntax-error
Z67PSpec Example 8.21. Block Scalar Nodes [1.3]spec block scalaraccept
Z9M4Spec Example 6.22. Global Tag Prefixspec directive sequence mapping tagaccept
ZCZ6nested mapping in plain single-line value is rejectederror mapping scalarsyntax-error
ZF4XSpec Example 2.6. Mapping of Mappingsspec mappingaccept
ZH7CAnchors in Mappingmapping anchoraccept
ZK9HNested top level flow mappingflow mappingaccept
ZL4Zinvalid nested mapping is rejectederror suite-invalidsyntax-error
ZVH3wrong-indented sequence item is rejectederror suite-invalidsyntax-error
ZWK4key with anchor after missing explicit mapping valueanchor mapping key null explicit scalaraccept
ZXT5implicit flow sequence key followed by newline and adjacent value is rejectederror flow mapping sequencesyntax-error
+
+

Methodology: acceptance is whether a full tree/Value load of the +fixture succeeds (saneyaml via from_documents_str, serde_yaml via +its document iterator, yaml-rust2 via YamlLoader, saphyr via +Yaml::load_from_str). Case ids link to the pinned upstream +fixture. The selection manifest, per-case policies, and divergence registry +live under tests/fixtures/yaml-test-suite/; the counts here are +cross-checked by tests/conformance_dashboard.rs.

+
+
+ + + diff --git a/docs/editing.md b/docs/editing.md index 38dc8b2..bd4ace4 100644 --- a/docs/editing.md +++ b/docs/editing.md @@ -7,6 +7,11 @@ load-then-re-emit round trip can't do. > Snippets elide the enclosing function; assume a function returning > `saneyaml::Result<()>`. +For a runnable end-to-end demo, `examples/syq.rs` is a minimal `yq`-style +command-line editor built on this API: `cargo run --example syq -- set +/services/web/image nginx:1.27 stack.yaml` rewrites one value and leaves +every comment and anchor in place. + ## Edit by path `saneyaml::edit` opens a `ConfigEditor`. Address values by path, then `finish`: diff --git a/examples/conformance_matrix_html.rs b/examples/conformance_matrix_html.rs new file mode 100644 index 0000000..08b5a09 --- /dev/null +++ b/examples/conformance_matrix_html.rs @@ -0,0 +1,470 @@ +//! Generates the static conformance-matrix web page from the selected YAML +//! test-suite corpus. Every curated case is fed to `saneyaml`, `serde_yaml`, +//! `yaml-rust2`, and `saphyr`, and each library is scored against the +//! manifest's expected outcome — the same methodology as +//! `examples/conformance_compare.rs`, rendered as a self-contained HTML page. +//! +//! Run with: `cargo run --locked --example conformance_matrix_html` +//! +//! The page is written to `docs/conformance/index.html` (override with the +//! first CLI argument) and is committed so the published guide can serve it +//! without building dev-dependencies. + +use serde::Deserialize; +use std::fmt::Write as _; +use std::panic::{self, AssertUnwindSafe}; +use std::{fs, path::PathBuf}; + +/// A library's tree/Value acceptance predicate for one input. +type AcceptFn = fn(&str) -> bool; + +#[derive(Debug, Deserialize)] +struct SuiteManifest { + case: Vec, +} + +#[derive(Debug, Deserialize)] +struct SuiteCase { + id: String, + name: String, + expected: ExpectedOutcome, + #[serde(default)] + features: Vec, +} + +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +enum ExpectedOutcome { + Accept, + SyntaxError, + TreeError, +} + +impl ExpectedOutcome { + fn label(self) -> &'static str { + match self { + ExpectedOutcome::Accept => "accept", + ExpectedOutcome::SyntaxError => "syntax-error", + ExpectedOutcome::TreeError => "tree-error", + } + } +} + +#[derive(Debug, Deserialize)] +struct SuiteCoverage { + local_case_alias: Vec, +} + +#[derive(Debug, Deserialize)] +struct LocalCaseAlias { + manifest_id: String, + upstream_id: String, +} + +#[derive(Debug, Deserialize)] +struct SuiteSource { + upstream: String, + data_branch_commit: String, +} + +impl SuiteCase { + fn fixture_path(&self) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/yaml-test-suite/data") + .join(self.id.replace('/', "-")) + .join("in.yaml") + } +} + +/// Whether a tree/Value load of `input` succeeds, for each library. +fn saneyaml_accepts(input: &str) -> bool { + saneyaml::from_documents_str::(input).is_ok() +} + +fn serde_yaml_accepts(input: &str) -> bool { + // serde_yaml has no multi-document loader on a single call; the suite's + // multi-doc cases are exercised via its document iterator so we don't + // penalise it for a single-doc-only `from_str`. + for doc in serde_yaml::Deserializer::from_str(input) { + if serde_yaml::Value::deserialize(doc).is_err() { + return false; + } + } + // An empty stream is a valid (null) document for both libraries. + true +} + +fn yaml_rust2_accepts(input: &str) -> bool { + yaml_rust2::YamlLoader::load_from_str(input).is_ok() +} + +fn saphyr_accepts(input: &str) -> bool { + use saphyr::LoadableYamlNode; + saphyr::Yaml::load_from_str(input).is_ok() +} + +/// Reference parsers are third-party code; a panic on hostile suite input +/// counts as a rejection rather than aborting the whole matrix run. +fn accepts_guarded(accepts: AcceptFn, input: &str) -> bool { + panic::catch_unwind(AssertUnwindSafe(|| accepts(input))).unwrap_or(false) +} + +struct Library { + name: &'static str, + version: String, + accepts: AcceptFn, +} + +#[derive(Default)] +struct Score { + spec_pass: usize, + too_strict: usize, + too_lax: usize, + tree_pass: usize, + tree_total: usize, +} + +impl Score { + fn spec_total(&self) -> usize { + self.spec_pass + self.too_strict + self.too_lax + } + + fn spec_rate(&self) -> f64 { + (self.spec_pass as f64) * 100.0 / (self.spec_total() as f64) + } +} + +fn locked_version(lock: &str, name: &str) -> String { + let needle = format!("name = \"{name}\""); + let mut lines = lock.lines(); + while let Some(line) = lines.next() { + if line.trim() == needle + && let Some(version) = lines + .next() + .and_then(|next| next.trim().strip_prefix("version = \"")) + { + return version.trim_end_matches('"').to_string(); + } + } + "unknown".to_string() +} + +fn escape_html(text: &str) -> String { + text.replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) +} + +fn main() { + let manifest: SuiteManifest = toml::from_str(include_str!( + "../tests/fixtures/yaml-test-suite/manifest.toml" + )) + .expect("manifest is valid TOML"); + let coverage: SuiteCoverage = toml::from_str(include_str!( + "../tests/fixtures/yaml-test-suite/coverage.toml" + )) + .expect("coverage is valid TOML"); + let source: SuiteSource = toml::from_str(include_str!( + "../tests/fixtures/yaml-test-suite/SOURCE.toml" + )) + .expect("SOURCE is valid TOML"); + + let lock = include_str!("../Cargo.lock"); + let libraries = [ + Library { + name: "saneyaml", + version: env!("CARGO_PKG_VERSION").to_string(), + accepts: saneyaml_accepts, + }, + Library { + name: "serde_yaml", + version: locked_version(lock, "serde_yaml"), + accepts: serde_yaml_accepts, + }, + Library { + name: "yaml-rust2", + version: locked_version(lock, "yaml-rust2"), + accepts: yaml_rust2_accepts, + }, + Library { + name: "saphyr", + version: locked_version(lock, "saphyr"), + accepts: saphyr_accepts, + }, + ]; + + let upstream_ids: std::collections::BTreeMap<&str, &str> = coverage + .local_case_alias + .iter() + .map(|alias| (alias.manifest_id.as_str(), alias.upstream_id.as_str())) + .collect(); + + let mut cases = manifest.case; + cases.sort_by(|a, b| a.id.cmp(&b.id)); + + // Silence panic backtraces from guarded third-party parser calls. + let default_hook = panic::take_hook(); + panic::set_hook(Box::new(|_| {})); + + let mut scores: Vec = libraries.iter().map(|_| Score::default()).collect(); + let mut rows = String::new(); + for case in &cases { + let input = fs::read_to_string(case.fixture_path()) + .unwrap_or_else(|e| panic!("read fixture {}: {e}", case.id)); + + let mut cells = String::new(); + let mut row_mismatch = false; + for (library, score) in libraries.iter().zip(scores.iter_mut()) { + let accepted = accepts_guarded(library.accepts, &input); + let (class, mark) = match case.expected { + ExpectedOutcome::Accept => { + if accepted { + score.spec_pass += 1; + ("ok", "✓") + } else { + score.too_strict += 1; + row_mismatch = true; + ("bad", "✗") + } + } + ExpectedOutcome::SyntaxError => { + if accepted { + score.too_lax += 1; + row_mismatch = true; + ("bad", "✓") + } else { + score.spec_pass += 1; + ("ok", "✗") + } + } + ExpectedOutcome::TreeError => { + score.tree_total += 1; + if accepted { + row_mismatch = true; + ("policy", "✓") + } else { + score.tree_pass += 1; + ("ok", "✗") + } + } + }; + let _ = write!(cells, "{mark}"); + } + + let upstream_id = upstream_ids + .get(case.id.as_str()) + .copied() + .unwrap_or(case.id.as_str()); + let case_url = format!( + "{}/tree/{}/{}", + source.upstream, source.data_branch_commit, upstream_id + ); + let features = case.features.join(" "); + let _ = writeln!( + rows, + "\ + {id}\ + {name}{tags}\ + {expected}\ + {cells}", + expected = case.expected.label(), + mismatch = row_mismatch, + id_lower = escape_html(&case.id.to_lowercase()), + name_lower = escape_html(&case.name.to_lowercase()), + features_lower = escape_html(&features.to_lowercase()), + url = escape_html(&case_url), + id = escape_html(&case.id), + name = escape_html(&case.name), + tags = escape_html(&features), + ); + } + panic::set_hook(default_hook); + + let total = cases.len(); + let spec_total = scores[0].spec_total(); + let tree_total = scores[0].tree_total; + let mut cards = String::new(); + for (library, score) in libraries.iter().zip(scores.iter()) { + let _ = writeln!( + cards, + "

{name} {version}

\ +

{pass}/{spec_total} {rate:.1}%

\ +

{strict} too strict · {lax} too lax · \ + tree policy {tree_pass}/{tree_total}

", + name = escape_html(library.name), + version = escape_html(&library.version), + pass = score.spec_pass, + rate = score.spec_rate(), + strict = score.too_strict, + lax = score.too_lax, + tree_pass = score.tree_pass, + ); + } + + let mut header_cells = String::new(); + for library in &libraries { + let _ = write!(header_cells, "{}", escape_html(library.name)); + } + + let html = format!( + r#" + + + + +saneyaml — YAML test-suite conformance matrix + + + +
+

YAML test-suite conformance matrix

+

Tree/Value loading of all {total} curated +yaml-test-suite cases (pinned at +{commit_short}), scored against the manifest's expected outcome. +A ✓ means the library accepted the input; green/red shows whether that +matches the expectation. Generated by +cargo run --locked --example conformance_matrix_html in the +saneyaml repository.

+
+{cards}
+

Spec score covers the {spec_total} neutral accept / +syntax-error cases. The {tree_total} tree-error cases (amber) are +valid YAML event streams that saneyaml's stricter duplicate-key policy rejects +at tree loading; they are scored separately so no library is penalised on the +neutral axis for a policy difference.

+
+ + + + +
+ +{header_cells} + +{rows} +
CaseNameExpected
+
+

Methodology: acceptance is whether a full tree/Value load of the +fixture succeeds (saneyaml via from_documents_str, serde_yaml via +its document iterator, yaml-rust2 via YamlLoader, saphyr via +Yaml::load_from_str). Case ids link to the pinned upstream +fixture. The selection manifest, per-case policies, and divergence registry +live under tests/fixtures/yaml-test-suite/; the counts here are +cross-checked by tests/conformance_dashboard.rs.

+
+
+ + + +"#, + total = total, + upstream = escape_html(&source.upstream), + commit_short = escape_html( + source + .data_branch_commit + .get(..12) + .unwrap_or(&source.data_branch_commit) + ), + cards = cards, + spec_total = spec_total, + tree_total = tree_total, + header_cells = header_cells, + rows = rows, + ); + + let output = std::env::args().nth(1).map_or_else( + || { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("docs/conformance") + .join("index.html") + }, + PathBuf::from, + ); + if let Some(parent) = output.parent() { + fs::create_dir_all(parent).expect("create output directory"); + } + fs::write(&output, html).expect("write conformance matrix page"); + + println!("wrote {}", output.display()); + for (library, score) in libraries.iter().zip(scores.iter()) { + println!( + " {:<12} spec {}/{} ({:.2}%), tree policy {}/{}", + library.name, + score.spec_pass, + score.spec_total(), + score.spec_rate(), + score.tree_pass, + score.tree_total, + ); + } +} diff --git a/examples/syq.rs b/examples/syq.rs new file mode 100644 index 0000000..7738364 --- /dev/null +++ b/examples/syq.rs @@ -0,0 +1,196 @@ +//! `syq` — a minimal `yq`-style YAML editor demonstrating saneyaml's +//! comment-preserving lossless edits. +//! +//! Unlike load-and-re-emit tools, edits rewrite only the addressed value: +//! comments, anchors, key ordering, quoting, and untouched bytes all survive. +//! +//! ```sh +//! cargo run --example syq -- get /server/port config.yaml +//! cargo run --example syq -- set /server/port 9090 config.yaml +//! cargo run --example syq -- push /server/hosts web-3 config.yaml +//! cargo run --example syq -- rename /server old-server config.yaml +//! cargo run --example syq -- rm /server/debug config.yaml +//! ``` +//! +//! Paths are RFC 6901 JSON Pointers (`~1` escapes `/`, `~0` escapes `~`). +//! Values are parsed as YAML, so `9090`, `true`, `[a, b]`, and `{k: v}` all +//! work. Reads from stdin when no file is given; edits print the rewritten +//! document to stdout, or rewrite the file in place with `-i`. + +use std::io::Read as _; +use std::process::ExitCode; + +struct Cli { + command: String, + path: String, + value: Option, + file: Option, + in_place: bool, +} + +const USAGE: &str = "\ +usage: syq [value] [file] [-i] + +commands: + get print the value at the path as YAML + set replace the value at the path + push append to the sequence at the path + rename rename the mapping key at the path + rm remove the mapping entry or sequence item + +The pointer is an RFC 6901 JSON Pointer, e.g. /services/web/image or +/jobs/test/steps/0/uses. Values are parsed as YAML. Input comes from the +file argument or stdin. Edits print to stdout unless -i rewrites the file."; + +fn parse_args() -> Result { + let mut positional = Vec::new(); + let mut in_place = false; + for arg in std::env::args().skip(1) { + match arg.as_str() { + "-i" | "--in-place" => in_place = true, + "-h" | "--help" => return Err(String::new()), + _ => positional.push(arg), + } + } + let mut positional = positional.into_iter(); + let command = positional.next().ok_or("missing command")?; + let path = positional.next().ok_or("missing pointer path")?; + let takes_value = matches!(command.as_str(), "set" | "push" | "rename"); + let value = if takes_value { + Some(positional.next().ok_or("missing value argument")?) + } else { + None + }; + let file = positional.next(); + if let Some(extra) = positional.next() { + return Err(format!("unexpected argument {extra:?}")); + } + if in_place && file.is_none() { + return Err("-i requires a file argument".to_string()); + } + Ok(Cli { + command, + path, + value, + file, + in_place, + }) +} + +fn read_input(file: Option<&str>) -> std::io::Result { + match file { + Some(path) => std::fs::read_to_string(path), + None => { + let mut buffer = String::new(); + std::io::stdin().read_to_string(&mut buffer)?; + Ok(buffer) + } + } +} + +/// Decodes RFC 6901 tokens for the read-only `get` traversal; edits delegate +/// decoding to `ConfigPath::json_pointer`. +fn pointer_tokens(pointer: &str) -> Result, String> { + if pointer.is_empty() { + return Ok(Vec::new()); + } + let rest = pointer + .strip_prefix('/') + .ok_or("pointer must be empty or start with '/'")?; + rest.split('/') + .map(|token| { + let mut decoded = String::with_capacity(token.len()); + let mut chars = token.chars(); + while let Some(c) = chars.next() { + if c != '~' { + decoded.push(c); + continue; + } + match chars.next() { + Some('0') => decoded.push('~'), + Some('1') => decoded.push('/'), + _ => return Err(format!("invalid escape in token {token:?}")), + } + } + Ok(decoded) + }) + .collect() +} + +fn lookup<'a>(mut node: &'a saneyaml::Value, tokens: &[String]) -> Option<&'a saneyaml::Value> { + for token in tokens { + node = match node { + saneyaml::Value::Mapping(_) => node.get(token.as_str())?, + saneyaml::Value::Sequence(_) => node.get(token.parse::().ok()?)?, + _ => return None, + }; + } + Some(node) +} + +fn run(cli: &Cli) -> Result<(), String> { + let input = read_input(cli.file.as_deref()).map_err(|e| format!("read input: {e}"))?; + + if cli.command == "get" { + let tokens = pointer_tokens(&cli.path)?; + let root: saneyaml::Value = + saneyaml::from_str(&input).map_err(|e| format!("parse input: {e}"))?; + let found = lookup(&root, &tokens).ok_or_else(|| format!("no value at {:?}", cli.path))?; + let rendered = saneyaml::to_string(found).map_err(|e| format!("render value: {e}"))?; + print!("{rendered}"); + return Ok(()); + } + + let path = saneyaml::ConfigPath::json_pointer(&cli.path).map_err(|e| e.to_string())?; + let mut editor = saneyaml::edit(input).map_err(|e| format!("parse input: {e}"))?; + match cli.command.as_str() { + "set" | "push" => { + let raw = cli.value.as_deref().expect("value argument was parsed"); + let value: saneyaml::Value = + saneyaml::from_str(raw).map_err(|e| format!("parse value as YAML: {e}"))?; + if cli.command == "set" { + editor.set(path, value).map_err(|e| e.to_string())?; + } else { + editor.push(path, value).map_err(|e| e.to_string())?; + } + } + "rename" => { + let new_key = cli.value.as_deref().expect("value argument was parsed"); + editor.rename(path, new_key).map_err(|e| e.to_string())?; + } + "rm" => { + editor.remove(path).map_err(|e| e.to_string())?; + } + other => return Err(format!("unknown command {other:?}")), + } + + let output = editor.finish().map_err(|e| e.to_string())?; + if cli.in_place { + let file = cli.file.as_deref().expect("-i requires a file"); + std::fs::write(file, output).map_err(|e| format!("write {file}: {e}"))?; + } else { + print!("{output}"); + } + Ok(()) +} + +fn main() -> ExitCode { + let cli = match parse_args() { + Ok(cli) => cli, + Err(message) => { + if message.is_empty() { + println!("{USAGE}"); + return ExitCode::SUCCESS; + } + eprintln!("error: {message}\n\n{USAGE}"); + return ExitCode::FAILURE; + } + }; + match run(&cli) { + Ok(()) => ExitCode::SUCCESS, + Err(message) => { + eprintln!("error: {message}"); + ExitCode::FAILURE + } + } +}