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

Editing files

Change values in an existing YAML file while keeping every comment, anchor, blank line, and untouched byte exactly where it was. This is the part a load-then-re-emit round trip can’t do.

Snippets elide the enclosing function; assume a function returning saneyaml::Result<()>.

Edit by path

saneyaml::edit opens a ConfigEditor. Address values by path, then finish:

#![allow(unused)]
fn main() {
let source = "\
service stack
services:
  web:
    image: nginx:1.25
    ports:
      - \"80:80\"
";

let mut editor = saneyaml::edit(source)?;
editor
    .set(saneyaml::ConfigPath::keys(["services", "web", "image"]), "nginx:1.27")?
    .push(saneyaml::ConfigPath::keys(["services", "web", "ports"]), "8080:80")?;

let edited = editor.finish()?;
assert!(edited.contains("# service stack"));   // comment preserved
assert!(edited.contains("image: nginx:1.27")); // value updated
assert!(edited.contains("- 8080:80"));         // item appended
}

Operations: set, insert, remove, rename, push (append to a sequence), and insert_item (insert at an index). Each returns &mut Self, so chain them; the editor reparses between operations so later paths see current source. Generated block lines use the surrounding line ending, so edits in CRLF files do not introduce mixed LF/CRLF output.

Addressing paths

#![allow(unused)]
fn main() {
use saneyaml::{ConfigPath, PathSegment};

// string keys (most common)
ConfigPath::keys(["metadata", "labels", "app"]);

// mixed keys and sequence indices
ConfigPath::new([
    PathSegment::from("jobs"),
    PathSegment::from("test"),
    PathSegment::from("steps"),
    PathSegment::from(0usize),
    PathSegment::from("uses"),
]);

// JSON Pointer — handles keys containing "/" or "~"
ConfigPath::json_pointer("/metadata/labels/app.kubernetes.io~1name")?;
}

Read and write files directly

#![allow(unused)]
fn main() {
let mut editor = saneyaml::edit_file("compose.yaml")?;
editor.set(saneyaml::ConfigPath::keys(["version"]), "3.9")?;
editor.finish_to_file()?; // writes back to compose.yaml
}

finish_to_file writes a same-directory temporary file and commits it with an atomic replacement, so readers see either the old complete contents or the new complete contents. If the path passed to edit_file is a symlink, the editor updates the target resolved when it was opened; retargeting the symlink later does not redirect the commit. The resolved target must still be a regular file at save time; a deleted target is reported as an I/O error rather than silently recreated.

On Unix, ordinary mode bits are preserved and ownership preservation is best-effort. Timestamps, ACLs, extended attributes, and platform security labels are not preserved; non-Unix platforms may also assign replacement-file permissions and ownership using their defaults. The atomic visibility guarantee does not promise stronger power-loss durability than the host filesystem.

Inspect without editing

Drop to LosslessStream when you need to read source-level detail — comments, exact scalar spelling, anchor/alias graph identity — that the semantic Value tree discards:

#![allow(unused)]
fn main() {
let stream = saneyaml::parse_lossless(source)?;

for comment in stream.comments() {
    println!("{}", comment.text());
}
}

LosslessStream also exposes effective_mapping_entries(node) — the merged view of a mapping with << provenance kept — and source_fragment(span) to recover the original bytes for any node. It’s the surface for tools that must preserve or analyze source, not just values.

A runnable end-to-end example (Docker Compose, Kubernetes, GitHub Actions) lives in examples/config_refactor.rs.