fyaml 0.3.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");

Installation

Add to your Cargo.toml:

[dependencies]
fyaml = "0.3"

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());

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.3.0 (2026-01-22)

New

- Add comprehensive integration tests for coverage. [Valentin Lab]

  - Add ``tests/editor_edge_cases.rs`` for editor boundary conditions
  - Add ``tests/emit_roundtrip.rs`` for YAML emit and reparse
  - Add ``tests/error_coverage.rs`` for error type formatting
  - Add ``tests/memory_safety.rs`` for large inputs and deep nesting
  - Add ``tests/noderef_coverage.rs`` for ``NodeRef`` methods
  - Add ``tests/parser_edge_cases.rs`` for multi-document parsing
  - Add ``tests/scalar_parsing_edge_cases.rs`` for YAML scalar types
  - Add ``tests/serde_coverage.rs`` for serde integration
  - Add ``tests/value_mutability.rs`` for ``Value`` mutations
  - Add ``tests/valueref_coverage.rs`` for ``ValueRef`` accessors
  - Update ``README.org`` with coverage metrics table (88.44% lines)

  Improves test coverage from ~75% to 88%+ by exercising edge cases,
  error paths, and lesser-used API methods.
- 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.

Changes

- Add source links for libfyaml claims. [Valentin Lab]

  - Link to libyaml source for 1024-char implicit key limit
  - Link to libfyaml changelog for 24x performance claim
  - Clarify streaming mode comparison context
- Update license and documentation for release. [Valentin Lab]

  - Update LICENSE copyright year to 2024-2026
  - Clarify license text in README (explicitly state MIT)
  - Add generated/build files to ``.gitignore`` (``/README.md``, ``/.pkg``, ``/Cargo.lock``)


0.1.1 (2026-01-19)
------------------

Fix
~~~
- Use crates.io syntax in installation instructions. [Valentin Lab]


0.1.0 (2026-01-19)
------------------

Fix
~~~
- Suppress libfyaml stderr noise in empty document test. [Valentin Lab]