Skip to content
/ fyaml Public

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

License

Notifications You must be signed in to change notification settings

0k/fyaml

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

15 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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

TypeDescription
DocumentParsed 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
FyParserMulti-document stream parser
ValueOwned serde-compatible YAML value
NumberNumeric value: Int(i64), UInt(u64), Float(f64)
TaggedValueValue with an associated YAML tag
ParseErrorRich parse error with line/column location

Enums

TypeVariants
NodeTypeScalar, Sequence, Mapping
NodeStylePlain, SingleQuoted, DoubleQuoted, Literal, Folded, etc.

Document Methods

MethodDescription
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)

MethodDescription
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)

MethodDescription
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

MethodDescription
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

MethodDescription
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

MethodDescription
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

TypeYieldsDescription
SeqIter<'doc>NodeRef<'doc>Sequence items
MapIter<'doc>(NodeRef<'doc>, NodeRef<'doc>)Mapping key-value pairs
DocumentIteratorResult<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

MetricCoverage
Lines88.44%
Functions91.29%
Regions90.44%

Other Rust YAML Libraries

LibraryEngineSerdeStatus
serde_yamlunsafe-libyaml (libyaml transpiled to Rust)YesDeprecated (2024-03)
serde_ymlunsafe-libyamlYesMaintained (fork of serde_yaml)
serde-yaml-ngunsafe-libyamlYesActive (migrating to libyaml-safer)
saphyrPure Rust (fork of yaml-rust)SoonActive
yaml-rust2Pure Rust (fork of yaml-rust)NoActive (high MSRV)
yaml-rustPure RustNoUnmaintained
fyamllibfyaml (C library via FFI)YesDevelopment

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.

About

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

Resources

License

Stars

Watchers

Forks

Packages

No packages published