# 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](https://github.com/pantoniou/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](https://github.com/yaml/libyaml/blob/master/src/scanner.c#L1065-L1074))
- **[Up to 24x
faster](https://github.com/pantoniou/libfyaml/blob/master/CHANGELOG.md#091---2025-12-28)**
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.
``` rust
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.
``` rust
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
``` rust
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:
``` rust
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();
```
### 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
``` rust
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:
``` rust
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:
``` rust
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:
``` rust
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:
``` rust
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:
``` rust
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:
``` rust
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:
``` rust
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:
``` rust
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:
``` rust
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:
```rust,no_run
use fyaml::Document;
// Single document from stdin
let doc = Document::from_stdin().unwrap();
println!("{}", doc.emit().unwrap());
```
For multi-document streams:
```rust,no_run
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:
```rust,no_run
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:
``` rust
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
``` rust
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
``` rust
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
``` rust
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
<table>
<thead>
<tr class="header">
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>Document</code></td>
<td>Parsed YAML document (owns the data)</td>
</tr>
<tr class="even">
<td><code>NodeRef<'doc></code></td>
<td>Zero-copy reference to a node (borrows document)</td>
</tr>
<tr class="odd">
<td><code>ValueRef<'doc></code></td>
<td>Zero-copy typed access (wraps NodeRef)</td>
</tr>
<tr class="even">
<td><code>Editor<'doc></code></td>
<td>Exclusive mutable access to document</td>
</tr>
<tr class="odd">
<td><code>FyParser</code></td>
<td>Multi-document stream parser</td>
</tr>
<tr class="even">
<td><code>Value</code></td>
<td>Owned serde-compatible YAML value</td>
</tr>
<tr class="odd">
<td><code>Number</code></td>
<td>Numeric value: <code>Int(i64)</code>, <code>UInt(u64)</code>,
<code>Float(f64)</code></td>
</tr>
<tr class="even">
<td><code>TaggedValue</code></td>
<td>Value with an associated YAML tag</td>
</tr>
<tr class="odd">
<td><code>ParseError</code></td>
<td>Rich parse error with line/column location</td>
</tr>
</tbody>
</table>
### Enums
<table>
<thead>
<tr class="header">
<th>Type</th>
<th>Variants</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>NodeType</code></td>
<td><code>Scalar</code>, <code>Sequence</code>,
<code>Mapping</code></td>
</tr>
<tr class="even">
<td><code>NodeStyle</code></td>
<td><code>Plain</code>, <code>SingleQuoted</code>,
<code>DoubleQuoted</code>, <code>Literal</code>, <code>Folded</code>,
etc.</td>
</tr>
</tbody>
</table>
### Document Methods
<table>
<thead>
<tr class="header">
<th>Method</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>Document::parse_str(yaml)</code></td>
<td>Parse YAML string into Document</td>
</tr>
<tr class="even">
<td><code>Document::new()</code></td>
<td>Create empty document</td>
</tr>
<tr class="odd">
<td><code>Document::from_stdin()</code></td>
<td>Parse single document from stdin</td>
</tr>
<tr class="even">
<td><code>doc.root()</code></td>
<td>Get root node as <code>Option<NodeRef></code></td>
</tr>
<tr class="odd">
<td><code>doc.root_value()</code></td>
<td>Get root node as <code>Option<ValueRef></code></td>
</tr>
<tr class="even">
<td><code>doc.at_path(path)</code></td>
<td>Navigate to node by path</td>
</tr>
<tr class="odd">
<td><code>doc.edit()</code></td>
<td>Get exclusive <code>Editor</code> for mutations</td>
</tr>
<tr class="even">
<td><code>doc.emit()</code></td>
<td>Emit document as YAML string</td>
</tr>
</tbody>
</table>
### NodeRef Methods (zero-copy)
<table>
<thead>
<tr class="header">
<th>Method</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>node.kind()</code></td>
<td>Get node type (<code>NodeType</code>)</td>
</tr>
<tr class="even">
<td><code>node.style()</code></td>
<td>Get node style (<code>NodeStyle</code>)</td>
</tr>
<tr class="odd">
<td><code>node.is_scalar()</code></td>
<td>Check if node is a scalar</td>
</tr>
<tr class="even">
<td><code>node.is_mapping()</code></td>
<td>Check if node is a mapping</td>
</tr>
<tr class="odd">
<td><code>node.is_sequence()</code></td>
<td>Check if node is a sequence</td>
</tr>
<tr class="even">
<td><code>node.is_quoted()</code></td>
<td>Check if scalar is quoted</td>
</tr>
<tr class="odd">
<td><code>node.is_non_plain()</code></td>
<td>Check if scalar has non-plain style</td>
</tr>
<tr class="even">
<td><code>node.scalar_str()</code></td>
<td>Get scalar as <code>&str</code> (zero-copy)</td>
</tr>
<tr class="odd">
<td><code>node.scalar_bytes()</code></td>
<td>Get scalar as <code>&[u8]</code> (zero-copy)</td>
</tr>
<tr class="even">
<td><code>node.at_path(path)</code></td>
<td>Navigate to child by path</td>
</tr>
<tr class="odd">
<td><code>node.seq_iter()</code></td>
<td>Iterate over sequence items</td>
</tr>
<tr class="even">
<td><code>node.map_iter()</code></td>
<td>Iterate over mapping key-value pairs</td>
</tr>
<tr class="odd">
<td><code>node.seq_len()</code></td>
<td>Get sequence length</td>
</tr>
<tr class="even">
<td><code>node.map_len()</code></td>
<td>Get mapping length</td>
</tr>
<tr class="odd">
<td><code>node.seq_get(i)</code></td>
<td>Get sequence item by index</td>
</tr>
<tr class="even">
<td><code>node.map_get(key)</code></td>
<td>Get mapping value by string key</td>
</tr>
<tr class="odd">
<td><code>node.tag_str()</code></td>
<td>Get YAML tag (zero-copy)</td>
</tr>
<tr class="even">
<td><code>node.emit()</code></td>
<td>Emit node as YAML string</td>
</tr>
</tbody>
</table>
### ValueRef Methods (zero-copy typed access)
<table>
<thead>
<tr class="header">
<th>Method</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>value.as_str()</code></td>
<td>Get string (zero-copy) <code>Option<&str></code></td>
</tr>
<tr class="even">
<td><code>value.as_bool()</code></td>
<td>Interpret as boolean (yes/no/on/off/true/false)</td>
</tr>
<tr class="odd">
<td><code>value.as_i64()</code></td>
<td>Interpret as signed integer (hex/octal/binary)</td>
</tr>
<tr class="even">
<td><code>value.as_u64()</code></td>
<td>Interpret as unsigned integer</td>
</tr>
<tr class="odd">
<td><code>value.as_f64()</code></td>
<td>Interpret as float (.inf, .nan support)</td>
</tr>
<tr class="even">
<td><code>value.is_null()</code></td>
<td>Check for null/~/empty</td>
</tr>
<tr class="odd">
<td><code>value.is_scalar()</code></td>
<td>Check if scalar</td>
</tr>
<tr class="even">
<td><code>value.is_sequence()</code></td>
<td>Check if sequence</td>
</tr>
<tr class="odd">
<td><code>value.is_mapping()</code></td>
<td>Check if mapping</td>
</tr>
<tr class="even">
<td><code>value.get(key)</code></td>
<td>Get mapping value by key</td>
</tr>
<tr class="odd">
<td><code>value.index(i)</code></td>
<td>Get sequence item by index</td>
</tr>
<tr class="even">
<td><code>value.at_path(path)</code></td>
<td>Navigate by path</td>
</tr>
<tr class="odd">
<td><code>value.seq_iter()</code></td>
<td>Iterate over sequence as <code>ValueRef</code></td>
</tr>
<tr class="even">
<td><code>value.map_iter()</code></td>
<td>Iterate over mapping as <code>(ValueRef, ValueRef)</code></td>
</tr>
<tr class="odd">
<td><code>value.tag()</code></td>
<td>Get YAML tag (zero-copy)</td>
</tr>
</tbody>
</table>
### Editor Methods
<table>
<thead>
<tr class="header">
<th>Method</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>ed.set_yaml_at(path, yaml)</code></td>
<td>Set/replace value at path (mappings and sequences)</td>
</tr>
<tr class="even">
<td><code>ed.delete_at(path)</code></td>
<td>Delete value at path, returns <code>bool</code></td>
</tr>
<tr class="odd">
<td><code>ed.build_from_yaml(yaml)</code></td>
<td>Build detached node from YAML</td>
</tr>
<tr class="even">
<td><code>ed.build_scalar(value)</code></td>
<td>Build plain scalar node</td>
</tr>
<tr class="odd">
<td><code>ed.build_sequence()</code></td>
<td>Build empty sequence node</td>
</tr>
<tr class="even">
<td><code>ed.build_mapping()</code></td>
<td>Build empty mapping node</td>
</tr>
<tr class="odd">
<td><code>ed.build_null()</code></td>
<td>Build null scalar node</td>
</tr>
<tr class="even">
<td><code>ed.set_root(handle)</code></td>
<td>Set document root</td>
</tr>
<tr class="odd">
<td><code>ed.copy_node(node)</code></td>
<td>Copy node from any document</td>
</tr>
<tr class="even">
<td><code>ed.seq_append(seq, item)</code></td>
<td>Append item to detached sequence handle</td>
</tr>
<tr class="odd">
<td><code>ed.map_insert(map, key, val)</code></td>
<td>Insert key-value pair into detached mapping</td>
</tr>
<tr class="even">
<td><code>ed.set_tag(node, tag)</code></td>
<td>Set YAML tag on detached node</td>
</tr>
<tr class="odd">
<td><code>ed.set_style(node, style)</code></td>
<td>Set YAML style on detached node, returns actual style</td>
</tr>
<tr class="even">
<td><code>ed.seq_append_at(path, item)</code></td>
<td>Append item to sequence at path</td>
</tr>
<tr class="odd">
<td><code>ed.root()</code></td>
<td>Read root during edit session</td>
</tr>
<tr class="even">
<td><code>ed.at_path(path)</code></td>
<td>Navigate during edit session</td>
</tr>
</tbody>
</table>
### FyParser Methods
<table>
<thead>
<tr class="header">
<th>Method</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>FyParser::from_string(yaml)</code></td>
<td>Create parser from YAML string</td>
</tr>
<tr class="even">
<td><code>FyParser::from_stdin()</code></td>
<td>Create parser from stdin (line-buffered)</td>
</tr>
<tr class="odd">
<td><code>FyParser::from_stdin_with_line_buffer(b)</code></td>
<td>Configurable buffering</td>
</tr>
<tr class="even">
<td><code>parser.doc_iter()</code></td>
<td>Iterate over documents (yields
<code>Result<Document></code>)</td>
</tr>
</tbody>
</table>
### Value Methods
<table>
<thead>
<tr class="header">
<th>Method</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>parse()</code></td>
<td>Parse YAML string into Value</td>
</tr>
<tr class="even">
<td><code>to_yaml_string()</code></td>
<td>Emit as YAML string via libfyaml</td>
</tr>
<tr class="odd">
<td><code>is_null()</code></td>
<td>Check if value is null</td>
</tr>
<tr class="even">
<td><code>is_bool()</code></td>
<td>Check if value is boolean</td>
</tr>
<tr class="odd">
<td><code>is_number()</code></td>
<td>Check if value is numeric</td>
</tr>
<tr class="even">
<td><code>is_string()</code></td>
<td>Check if value is a string</td>
</tr>
<tr class="odd">
<td><code>is_sequence()</code></td>
<td>Check if value is a sequence</td>
</tr>
<tr class="even">
<td><code>is_mapping()</code></td>
<td>Check if value is a mapping</td>
</tr>
<tr class="odd">
<td><code>is_tagged()</code></td>
<td>Check if value has a tag</td>
</tr>
<tr class="even">
<td><code>as_str()</code></td>
<td>Get as <code>&str</code> if string</td>
</tr>
<tr class="odd">
<td><code>as_i64()</code></td>
<td>Get as <code>i64</code> if numeric</td>
</tr>
<tr class="even">
<td><code>as_u64()</code></td>
<td>Get as <code>u64</code> if numeric</td>
</tr>
<tr class="odd">
<td><code>as_f64()</code></td>
<td>Get as <code>f64</code> if numeric</td>
</tr>
<tr class="even">
<td><code>as_bool()</code></td>
<td>Get as <code>bool</code> if boolean</td>
</tr>
<tr class="odd">
<td><code>as_sequence()</code></td>
<td>Get as <code>&[Value]</code> if sequence</td>
</tr>
<tr class="even">
<td><code>as_mapping()</code></td>
<td>Get as <code>&IndexMap</code> if mapping</td>
</tr>
<tr class="odd">
<td><code>as_tagged()</code></td>
<td>Get as <code>&TaggedValue</code> if tagged</td>
</tr>
<tr class="even">
<td><code>get(key)</code></td>
<td>Get value by key from mapping</td>
</tr>
<tr class="odd">
<td><code>[key]</code> / <code>[idx]</code></td>
<td>Index into mapping or sequence</td>
</tr>
</tbody>
</table>
### Iterators
<table>
<thead>
<tr class="header">
<th>Type</th>
<th>Yields</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>SeqIter<'doc></code></td>
<td><code>NodeRef<'doc></code></td>
<td>Sequence items</td>
</tr>
<tr class="even">
<td><code>MapIter<'doc></code></td>
<td><code>(NodeRef<'doc>, NodeRef<'doc>)</code></td>
<td>Mapping key-value pairs</td>
</tr>
<tr class="odd">
<td><code>DocumentIterator</code></td>
<td><code>Result<Document></code></td>
<td>Documents in stream</td>
</tr>
</tbody>
</table>
## 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
<table>
<thead>
<tr class="header">
<th>Metric</th>
<th>Coverage</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Lines</td>
<td>89.05%</td>
</tr>
<tr class="even">
<td>Functions</td>
<td>91.83%</td>
</tr>
<tr class="odd">
<td>Regions</td>
<td>91.22%</td>
</tr>
</tbody>
</table>
## Other Rust YAML Libraries
<table>
<thead>
<tr class="header">
<th>Library</th>
<th>Engine</th>
<th>Serde</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><a href="https://github.com/dtolnay/serde-yaml">serde_yaml</a></td>
<td><a
href="https://github.com/dtolnay/unsafe-libyaml">unsafe-libyaml</a>
(libyaml transpiled to Rust)</td>
<td>Yes</td>
<td>Deprecated (2024-03)</td>
</tr>
<tr class="even">
<td><a
href="https://github.com/sebastienrousseau/serde_yml">serde_yml</a></td>
<td><a
href="https://github.com/dtolnay/unsafe-libyaml">unsafe-libyaml</a></td>
<td>Yes</td>
<td>Maintained (fork of serde_yaml)</td>
</tr>
<tr class="odd">
<td><a
href="https://github.com/acatton/serde-yaml-ng">serde-yaml-ng</a></td>
<td><a
href="https://github.com/dtolnay/unsafe-libyaml">unsafe-libyaml</a></td>
<td>Yes</td>
<td>Active (migrating to libyaml-safer)</td>
</tr>
<tr class="even">
<td><a href="https://github.com/saphyr-rs/saphyr">saphyr</a></td>
<td>Pure Rust (fork of yaml-rust)</td>
<td>Soon</td>
<td>Active</td>
</tr>
<tr class="odd">
<td><a
href="https://github.com/Ethiraric/yaml-rust2">yaml-rust2</a></td>
<td>Pure Rust (fork of yaml-rust)</td>
<td>No</td>
<td>Active (high MSRV)</td>
</tr>
<tr class="even">
<td><a href="https://github.com/chyh1990/yaml-rust">yaml-rust</a></td>
<td>Pure Rust</td>
<td>No</td>
<td>Unmaintained</td>
</tr>
<tr class="odd">
<td>fyaml</td>
<td><a href="https://github.com/pantoniou/libfyaml">libfyaml</a> (C
library via FFI)</td>
<td>Yes</td>
<td>Development</td>
</tr>
</tbody>
</table>
### 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.