fyaml

Crates.iofyaml
lib.rsfyaml
version0.4.0
created_at2026-01-19 07:44:46.421132+00
updated_at2026-01-24 04:01:41.316579+00
descriptionSafe Rust bindings for libfyaml YAML parser with DOM navigation, path queries, and serde-compatible Value type
homepage
repositoryhttps://github.com/0k/fyaml
max_upload_size
id2053962
size436,192
Valentin Lab (vaab)

documentation

https://docs.rs/fyaml

README

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

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

Type Description
Document Parsed YAML document (owns the data)
NodeRef<'doc> Zero-copy reference to a node (borrows document)
ValueRef<'doc> Zero-copy typed access (wraps NodeRef)
Editor<'doc> Exclusive mutable access to document
FyParser Multi-document stream parser
Value Owned serde-compatible YAML value
Number Numeric value: Int(i64), UInt(u64), Float(f64)
TaggedValue Value with an associated YAML tag
ParseError Rich parse error with line/column location

Enums

Type Variants
NodeType Scalar, Sequence, Mapping
NodeStyle Plain, SingleQuoted, DoubleQuoted, Literal, Folded, etc.

Document Methods

Method Description
Document::parse_str(yaml) Parse YAML string into Document
Document::new() Create empty document
Document::from_stdin() Parse single document from stdin
doc.root() Get root node as Option<NodeRef>
doc.root_value() Get root node as Option<ValueRef>
doc.at_path(path) Navigate to node by path
doc.edit() Get exclusive Editor for mutations
doc.emit() Emit document as YAML string

NodeRef Methods (zero-copy)

Method Description
node.kind() Get node type (NodeType)
node.style() Get node style (NodeStyle)
node.is_scalar() Check if node is a scalar
node.is_mapping() Check if node is a mapping
node.is_sequence() Check if node is a sequence
node.is_quoted() Check if scalar is quoted
node.is_non_plain() Check if scalar has non-plain style
node.scalar_str() Get scalar as &str (zero-copy)
node.scalar_bytes() Get scalar as &[u8] (zero-copy)
node.at_path(path) Navigate to child by path
node.seq_iter() Iterate over sequence items
node.map_iter() Iterate over mapping key-value pairs
node.seq_len() Get sequence length
node.map_len() Get mapping length
node.seq_get(i) Get sequence item by index
node.map_get(key) Get mapping value by string key
node.tag_str() Get YAML tag (zero-copy)
node.emit() Emit node as YAML string

ValueRef Methods (zero-copy typed access)

Method Description
value.as_str() Get string (zero-copy) Option<&str>
value.as_bool() Interpret as boolean (yes/no/on/off/true/false)
value.as_i64() Interpret as signed integer (hex/octal/binary)
value.as_u64() Interpret as unsigned integer
value.as_f64() Interpret as float (.inf, .nan support)
value.is_null() Check for null/~/empty
value.is_scalar() Check if scalar
value.is_sequence() Check if sequence
value.is_mapping() Check if mapping
value.get(key) Get mapping value by key
value.index(i) Get sequence item by index
value.at_path(path) Navigate by path
value.seq_iter() Iterate over sequence as ValueRef
value.map_iter() Iterate over mapping as (ValueRef, ValueRef)
value.tag() Get YAML tag (zero-copy)

Editor Methods

Method Description
ed.set_yaml_at(path, yaml) Set/replace value at path (mappings and sequences)
ed.delete_at(path) Delete value at path, returns bool
ed.build_from_yaml(yaml) Build detached node from YAML
ed.build_scalar(value) Build plain scalar node
ed.build_sequence() Build empty sequence node
ed.build_mapping() Build empty mapping node
ed.set_root(handle) Set document root
ed.copy_node(node) Copy node from any document
ed.seq_append_at(path, item) Append item to sequence at path
ed.root() Read root during edit session
ed.at_path(path) Navigate during edit session

FyParser Methods

Method Description
FyParser::from_string(yaml) Create parser from YAML string
FyParser::from_stdin() Create parser from stdin (line-buffered)
FyParser::from_stdin_with_line_buffer(b) Configurable buffering
parser.doc_iter() Iterate over documents (yields Result<Document>)

Value Methods

Method Description
parse() Parse YAML string into Value
to_yaml_string() Emit as YAML string via libfyaml
is_null() Check if value is null
is_bool() Check if value is boolean
is_number() Check if value is numeric
is_string() Check if value is a string
is_sequence() Check if value is a sequence
is_mapping() Check if value is a mapping
is_tagged() Check if value has a tag
as_str() Get as &str if string
as_i64() Get as i64 if numeric
as_u64() Get as u64 if numeric
as_f64() Get as f64 if numeric
as_bool() Get as bool if boolean
as_sequence() Get as &[Value] if sequence
as_mapping() Get as &IndexMap if mapping
as_tagged() Get as &TaggedValue if tagged
get(key) Get value by key from mapping
[key] / [idx] Index into mapping or sequence

Iterators

Type Yields Description
SeqIter<'doc> NodeRef<'doc> Sequence items
MapIter<'doc> (NodeRef<'doc>, NodeRef<'doc>) Mapping key-value pairs
DocumentIterator Result<Document> Documents in stream

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

Metric Coverage
Lines 88.44%
Functions 91.29%
Regions 90.44%

Other Rust YAML Libraries

Library Engine Serde Status
serde_yaml unsafe-libyaml (libyaml transpiled to Rust) Yes Deprecated (2024-03)
serde_yml unsafe-libyaml Yes Maintained (fork of serde_yaml)
serde-yaml-ng unsafe-libyaml Yes Active (migrating to libyaml-safer)
saphyr Pure Rust (fork of yaml-rust) Soon Active
yaml-rust2 Pure Rust (fork of yaml-rust) No Active (high MSRV)
yaml-rust Pure Rust No Unmaintained
fyaml libfyaml (C library via FFI) Yes Development

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.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

Changes

  • Remove installation section from README.org [Valentin Lab]

    The installation instructions are redundant since crates.io displays this information automatically and keeps it current with published versions.

Fix

  • Improve crates.io README rendering. [Valentin Lab]

    • Add #+OPTIONS: ^:nil to prevent underscore subscript interpretation
    • Add .gitchangelog.rc with markdown output format for proper headings

    Fixes serde_yaml/serde_yml displaying as subscript and changelog using reStructuredText headings instead of markdown.

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]

Commit count: 15

cargo fmt