fyaml 0.5.0

Safe Rust bindings for libfyaml YAML parser with DOM navigation, path queries, and serde-compatible Value type
Documentation

fyaml

A safe Rust wrapper around the libfyaml C library for parsing and manipulating YAML documents.

Overview

fyaml provides an idiomatic Rust interface to the high-performance libfyaml YAML parsing library. It supports DOM-style navigation, zero-copy scalar access, node type introspection, multi-document parsing, document mutation, and a serde-compatible Value type.

Status

Early development - This library is functional but has not yet been widely used or audited. The API may change, and edge cases may exist. If you need a mature, battle-tested YAML library, consider serde_yml or serde-yaml-ng instead.

Why libfyaml?

fyaml is built on libfyaml, a modern C library that offers several advantages over the traditional libyaml:

  • Full YAML 1.2 compliance with YAML 1.3 preparation
  • Zero-copy architecture for efficient large document handling
  • No artificial limits (libyaml has a 1024-char implicit key limit)
  • Up to 24x faster on large files in streaming mode (vs document mode)
  • Rich manipulation APIs including YPATH expressions for path queries
  • MIT licensed (as of v0.9.1)

This makes fyaml suitable for use cases requiring DOM manipulation, YAML transformation tools, or configuration inspection utilities where path-based queries are convenient.

Features

  • Parse YAML strings into document objects
  • Zero-copy scalar access via lifetime-bound NodeRef and ValueRef
  • Navigate nodes using path-based queries (e.g., /foo/bar, /list/0)
  • Support for all YAML node types: scalars, sequences, and mappings
  • Iterate over mapping key-value pairs and sequence items
  • Convert nodes back to YAML strings
  • Multi-document stream parsing via FyParser
  • Read YAML from stdin (single document or stream)
  • Document mutation via Editor with compile-time safety
  • Style and comment preservation during edits: comments, quote styles, block/flow structure
  • ValueRef type: Zero-copy typed access with YAML type interpretation
    • as_str(), as_bool(), as_i64(), as_f64(), is_null()
    • Non-plain scalars (quoted, literal, folded) preserved as strings
  • Value type: Pure Rust enum with serde support
    • Serialize/deserialize with any serde-compatible format (JSON, TOML, etc.)
    • Emit YAML using libfyaml for standards-compliant output
    • Convenient indexing: value["key"], value[0]

Error Handling

Parse errors include detailed location information (line and column numbers), making it easy to report errors to users or integrate with IDEs and linters.

use fyaml::Document;

let result = Document::parse_str("[unclosed bracket");
if let Err(e) = result {
    // Access structured error info
    if let fyaml::Error::ParseError(parse_err) = &e {
        println!("Error: {}", parse_err.message());
        if let Some((line, col)) = parse_err.location() {
            println!("At line {}, column {}", line, col);
        }
    }
    // Or just display it nicely
    println!("{}", e);  // "Parse error at 2:1: flow sequence without a closing bracket"
}

The ParseError type provides:

  • message() - The error message from libfyaml
  • line() - Line number (1-based), if available
  • column() - Column number (1-based), if available
  • location() - Tuple of (line, column) if both available

All parsing methods (Document::parse_str, Document::from_string, Document::from_bytes, Editor::build_from_yaml) capture errors silently without printing to stderr, making the library suitable for use in GUI applications and test suites.

Zero-Copy Architecture

fyaml leverages libfyaml's zero-copy design for efficient memory usage. When you access scalar values through NodeRef or ValueRef, you get references directly into the parsed document buffer - no string copying or allocation occurs.

use fyaml::Document;

let doc = Document::parse_str("message: Hello, World!").unwrap();
let root = doc.root().unwrap();
let node = root.at_path("/message").unwrap();

// Zero-copy: this &str points directly into the document's memory
let s: &str = node.scalar_str().unwrap();
assert_eq!(s, "Hello, World!");

// The reference is tied to the document's lifetime -
// this prevents use-after-free at compile time

This is particularly beneficial for:

  • Large documents: Read gigabytes of YAML without doubling memory usage
  • Config parsing: Extract only the values you need without copying everything
  • High-throughput processing: Minimize allocations in hot paths

Style and Comment Preservation

When modifying documents with Editor, fyaml preserves formatting and comments. This is essential for configuration files where maintaining the original style improves readability and diff-friendliness.

What IS preserved:

  • Comments: Top-level, inline, and end-of-line comments
  • Quote styles: Single-quoted ('value'), double-quoted ("value"), and plain scalars
  • Block scalar styles: Literal (|) and folded (>) blocks
  • Collection styles: Flow ([a, b], {a: 1}) vs block (indented) sequences/mappings
use fyaml::Document;

// Comments and quote styles are preserved through edits
let yaml = "# Database configuration
database:
  host: 'localhost'  # local dev server
  port: 5432
";

let mut doc = Document::parse_str(yaml).unwrap();
{
    let mut ed = doc.edit();
    ed.set_yaml_at("/database/port", "5433").unwrap();
}

let output = doc.emit().unwrap();
// Comments preserved
assert!(output.contains("# Database configuration"));
assert!(output.contains("# local dev server"));
// Quote style preserved
assert!(output.contains("'localhost'"));

Block scalars are also preserved:

use fyaml::Document;

let yaml = "script: |
  echo hello
  echo world
name: test
";

let mut doc = Document::parse_str(yaml).unwrap();
{
    let mut ed = doc.edit();
    ed.set_yaml_at("/name", "modified").unwrap();
}

let output = doc.emit().unwrap();
// Literal block style (|) is preserved for the script
assert!(output.contains("script: |"));

Formatting notes:

  • Flow collections ([a, b], {a: 1}) are preserved as flow style but may be reformatted across multiple lines by libfyaml's emitter

Path Syntax

Paths use / as the separator (YPATH/JSON Pointer style):

  • /key - access a mapping key
  • /0 - access a sequence index
  • /parent/child/0 - nested access
use fyaml::Document;

let yaml = "
database:
  host: localhost
  ports:
    - 5432
    - 5433
";

let doc = Document::parse_str(yaml).unwrap();
let root = doc.root().unwrap();

// Access mapping key
let db = root.at_path("/database").unwrap();
assert!(db.is_mapping());

// Nested access
let host = root.at_path("/database/host").unwrap();
assert_eq!(host.scalar_str().unwrap(), "localhost");

// Sequence index
let first_port = root.at_path("/database/ports/0").unwrap();
assert_eq!(first_port.scalar_str().unwrap(), "5432");

Usage

Working with Value (high-level, owned)

The Value type provides a convenient, serde-compatible way to work with YAML:

use fyaml::Value;

// Parse YAML
let value: Value = "name: Alice\nage: 30".parse().unwrap();

// Access values with indexing
assert_eq!(value["name"].as_str(), Some("Alice"));
assert_eq!(value["age"].as_i64(), Some(30));

// Emit back to YAML
let yaml = value.to_yaml_string().unwrap();

Zero-copy with Document and NodeRef

For more control and zero-copy scalar access, use the Document API:

use fyaml::Document;

let doc = Document::parse_str("database:\n  host: localhost\n  port: 5432").unwrap();
let root = doc.root().unwrap();

// Zero-copy: returns &str pointing into document memory
let host = root.at_path("/database/host").unwrap();
assert_eq!(host.scalar_str().unwrap(), "localhost");

// Navigation by path
let port = root.at_path("/database/port").unwrap();
assert_eq!(port.scalar_str().unwrap(), "5432");

Zero-copy typed access with ValueRef

ValueRef wraps NodeRef and provides typed accessors that interpret YAML scalars on demand without allocation:

use fyaml::Document;

let doc = Document::parse_str("name: Alice\nage: 30\nactive: yes").unwrap();
let root = doc.root_value().unwrap();

// Zero-copy typed access
assert_eq!(root.get("name").unwrap().as_str(), Some("Alice"));
assert_eq!(root.get("age").unwrap().as_i64(), Some(30));
assert_eq!(root.get("active").unwrap().as_bool(), Some(true));  // yes -> true

Non-plain scalars (quoted, literal |, folded >) are NOT type-interpreted:

use fyaml::Document;

let doc = Document::parse_str("quoted: 'true'\nunquoted: true").unwrap();
let root = doc.root_value().unwrap();

// Quoted: string, not bool
assert_eq!(root.get("quoted").unwrap().as_bool(), None);
assert_eq!(root.get("quoted").unwrap().as_str(), Some("true"));

// Unquoted: interpreted as bool
assert_eq!(root.get("unquoted").unwrap().as_bool(), Some(true));

Mutation with Editor

Use Document::edit() to get an exclusive Editor for modifications:

use fyaml::Document;

let mut doc = Document::parse_str("name: Alice").unwrap();

// Mutation phase - NodeRef cannot exist during this
{
    let mut ed = doc.edit();
    ed.set_yaml_at("/name", "'Bob'").unwrap();   // Preserve quotes
    ed.set_yaml_at("/age", "30").unwrap();       // Add new key
    ed.delete_at("/name").unwrap();              // Delete key
    ed.set_yaml_at("/name", "\"Charlie\"").unwrap();  // Re-add
}

// Read phase - safe to access nodes again
let root = doc.root().unwrap();
assert_eq!(root.at_path("/name").unwrap().scalar_str().unwrap(), "Charlie");
assert_eq!(root.at_path("/age").unwrap().scalar_str().unwrap(), "30");

Building complex structures:

use fyaml::Document;

let mut doc = Document::new().unwrap();
{
    let mut ed = doc.edit();
    let root = ed.build_from_yaml("users:\n  - name: Alice\n  - name: Bob").unwrap();
    ed.set_root(root).unwrap();
}
assert!(doc.root().is_some());

Modifying sequence elements:

use fyaml::Document;

let mut doc = Document::parse_str("items:\n  - a\n  - b\n  - c").unwrap();
{
    let mut ed = doc.edit();
    // Replace by positive index
    ed.set_yaml_at("/items/0", "first").unwrap();
    // Replace by negative index (Python-style: -1 = last element)
    ed.set_yaml_at("/items/-1", "last").unwrap();
}
assert_eq!(doc.at_path("/items/0").unwrap().scalar_str().unwrap(), "first");
assert_eq!(doc.at_path("/items/1").unwrap().scalar_str().unwrap(), "b");
assert_eq!(doc.at_path("/items/2").unwrap().scalar_str().unwrap(), "last");

Building structures programmatically with handle-level operations:

use fyaml::Document;

let mut doc = Document::new().unwrap();
{
    let mut ed = doc.edit();

    // Build a sequence of servers
    let mut servers = ed.build_sequence().unwrap();
    let s1 = ed.build_scalar("web1").unwrap();
    let s2 = ed.build_scalar("web2").unwrap();
    ed.seq_append(&mut servers, s1).unwrap();
    ed.seq_append(&mut servers, s2).unwrap();

    // Build the root mapping
    let mut root = ed.build_mapping().unwrap();
    let k1 = ed.build_scalar("host").unwrap();
    let v1 = ed.build_scalar("localhost").unwrap();
    ed.map_insert(&mut root, k1, v1).unwrap();
    let k2 = ed.build_scalar("port").unwrap();
    let v2 = ed.build_null().unwrap();
    ed.map_insert(&mut root, k2, v2).unwrap();
    let k3 = ed.build_scalar("servers").unwrap();
    ed.map_insert(&mut root, k3, servers).unwrap();

    // Tag the root
    ed.set_tag(&mut root, "!config").unwrap();
    ed.set_root(root).unwrap();
}

let root = doc.root().unwrap();
assert_eq!(root.at_path("/host").unwrap().scalar_str().unwrap(), "localhost");
assert_eq!(root.tag_str().unwrap().unwrap(), "!config");
assert_eq!(root.at_path("/servers/0").unwrap().scalar_str().unwrap(), "web1");

Multi-document parsing with FyParser

Use FyParser for parsing YAML streams with multiple documents:

use fyaml::FyParser;

let yaml = "---\ndoc1: value1\n---\ndoc2: value2";
let parser = FyParser::from_string(yaml).unwrap();

let docs: Vec<_> = parser.doc_iter().filter_map(|r| r.ok()).collect();
assert_eq!(docs.len(), 2);

// Each document is independent
assert_eq!(docs[0].at_path("/doc1").unwrap().scalar_str().unwrap(), "value1");
assert_eq!(docs[1].at_path("/doc2").unwrap().scalar_str().unwrap(), "value2");

Reading from stdin

For CLI tools that read YAML from stdin:

use fyaml::Document;

// Single document from stdin
let doc = Document::from_stdin().unwrap();
println!("{}", doc.emit().unwrap());

For multi-document streams:

use fyaml::FyParser;

// Default: line-buffered mode for interactive/streaming use
let parser = FyParser::from_stdin().unwrap();

for doc_result in parser.doc_iter() {
    let doc = doc_result.unwrap();
    println!("{}", doc.emit().unwrap());
}

For batch processing where efficiency matters more than interactivity:

use fyaml::FyParser;

// Block-buffered mode: more efficient for large inputs
let parser = FyParser::from_stdin_with_line_buffer(false).unwrap();

for doc_result in parser.doc_iter() {
    // Process each document
}

Serde integration

Value works with any serde-compatible format:

use fyaml::Value;

let value: Value = "key: value".parse().unwrap();

// Convert to JSON
let json = serde_json::to_string(&value).unwrap();
assert_eq!(json, r#"{"key":"value"}"#);

// Parse from JSON
let from_json: Value = serde_json::from_str(&json).unwrap();

Iterating over mappings

use fyaml::Document;

let doc = Document::parse_str("a: 1\nb: 2\nc: 3").unwrap();
let root = doc.root().unwrap();

for (key, value) in root.map_iter() {
    println!("{}: {}", key.scalar_str().unwrap(), value.scalar_str().unwrap());
}

Iterating over sequences

use fyaml::Document;

let doc = Document::parse_str("- apple\n- banana\n- cherry").unwrap();
let root = doc.root().unwrap();

for item in root.seq_iter() {
    println!("{}", item.scalar_str().unwrap());
}

Checking node types

use fyaml::{Document, NodeType};

let doc = Document::parse_str("key: value").unwrap();
let root = doc.root().unwrap();

assert!(root.is_mapping());
assert_eq!(root.kind(), NodeType::Mapping);

let value = root.at_path("/key").unwrap();
assert!(value.is_scalar());

API Reference

Main Types

Enums

Document Methods

NodeRef Methods (zero-copy)

ValueRef Methods (zero-copy typed access)

Editor Methods

FyParser Methods

Value Methods

Iterators

Dependencies

  • libc - C library bindings
  • fyaml-sys - FFI bindings to libfyaml
  • log - Logging framework
  • serde - Serialization framework
  • indexmap - Order-preserving map for YAML mappings

Test Coverage

Other Rust YAML Libraries

Choosing a Library

  • For serde integration: fyaml provides a serde-compatible Value type with libfyaml-powered parsing and emission. Alternatives include serde_yml or serde-yaml-ng (based on unsafe-libyaml).

  • For pure Rust: Use saphyr or yaml-rust2 (no C dependencies, easier to audit).

  • For DOM manipulation and path queries: fyaml provides convenient path-based navigation (/foo/0/bar) via libfyaml's YPATH support, plus a Value type for programmatic manipulation.

  • For maximum performance on large files: fyaml benefits from libfyaml's zero-copy architecture and streaming optimizations.

License

MIT License (c) 2024-2026 Valentin Lab. The LICENSE file is available with the source.

Changelog

0.5.0 (2026-02-02)

New

  • Add set_style method to Editor [Valentin Lab]

    Allows setting the YAML style (plain, single-quoted, double-quoted, literal, folded, flow, block) on detached node handles. libfyaml validates the requested style against node content and may keep the current style if the request is invalid; the actually-applied style is returned.

  • Add handle-level Editor methods for programmatic node assembly. [Valentin Lab]

    Add seq_append(), map_insert(), set_tag(), and build_null() to Editor for building YAML structures without parsing YAML snippets. These complement the existing path-based API (set_yaml_at, seq_append_at) with lower-level handle operations.

    Update README.org with API table entries and a usage example.

Fix

  • Quote ambiguous strings during Value YAML emission. [Valentin Lab]

    Strings like "true", "null", "42" were emitted as plain scalars, causing them to be reinterpreted as bool/null/number on re-parse. Now needs_quoting() detects such values and the emitter wraps them in single quotes so they roundtrip correctly as Value::String.

  • build_null() now produces a proper YAML null node. [Valentin Lab]

    fy_node_create_scalar_copy(doc, NULL, 0) creates an empty scalar without libfyaml's internal is_null flag, making it indistinguishable from build_scalar(""). Switch to build_from_yaml("null") which goes through the parser and sets is_null = true.

    Tighten null emission tests: Value::Null must emit exactly "null", and must differ from Value::String("").

  • Value::to_yaml_string() no longer adds trailing newline. [Valentin Lab]

    to_yaml_string() was using document-level emission (fy_emit_document_to_string) which appends \n. Rewrite to use node-level emission via Editor API, matching NodeRef::emit() behavior.

    Tighten existing test assertions that masked this with .trim().

0.4.0 (2026-01-24)

New

  • Add sequence element support to set_yaml_at [Valentin Lab]

    • Support positive and negative (Python-style) indices for sequences
    • Validate index bounds and return error for out-of-bounds access
    • Update documentation with sequence examples and supported parent types
    • Add comprehensive tests for sequence manipulation edge cases

0.3.0 (2026-01-22)

New

  • Add rich parse errors with line/column location info. [Valentin Lab]

    • Add ParseError type with line(), column(), location() accessors
    • Add diag module to capture libfyaml errors via fy_diag callbacks
    • Enable FYPCF_QUIET on all parse configs to suppress stderr output
    • Update Document::parse_str, from_string, from_bytes to return Error::ParseError with location info
    • Update Editor::build_from_yaml with RAII DiagGuard for diag restoration
    • Update FyParser stream iterator to capture errors with location

    This makes the library suitable for GUI applications and IDEs that need structured error information without stderr pollution.

  • Refactor to enforce zero-copy aspects wherever possible. [Valentin Lab]

0.2.0 (2026-01-20)

New

  • Add from_stdin_with_line_buffer() for configurable stdin buffering. [Valentin Lab]

    Allows callers to control whether stdin uses line-buffered or block-buffered mode. Line-buffered is useful for streaming/interactive use (process documents as lines arrive), while block-buffered is more efficient for batch processing.

    The existing from_stdin() method now delegates to this with line_buffered=true to preserve backward compatibility.