yami 0.1.0

A lightweight, zero-copy, minimal-allocation YAML parser in Rust
Documentation

yami

CI Crates.io Documentation Downloads Codecov License Rust MSRV

A lightweight, zero-copy, minimal-allocation YAML parser in Rust.


Overview

yami is an ultra-fast, zero-copy YAML parser designed for minimal heap allocations, deterministic behavior, and strict parsing semantics.

Unlike traditional YAML libraries that allocate owned Strings for every scalar, key, and value in a document, yami returns an Abstract Syntax Tree (AST) where all scalars borrow directly from the input text slice (&'a str). This eliminates heap churn and enables zero-allocation deserialization into domain structs.

Background & Motivation

In the Rust ecosystem, standard YAML parsers like serde_yaml cannot deserialize into structs containing borrowed string slices (&'a str) due to internal stream buffering and DeserializeOwned trait bounds:

// Attempting zero-copy deserialization with serde_yaml:
struct App<'a> {
    name: &'a str,
}

let app: App = serde_yaml::from_str(&yaml_str).unwrap();
//  ^^^ ERROR: the trait `for<'de> Deserialize<'de>` is not implemented for `App<'_>`
//      note: required by `serde_yaml::from_str` due to `DeserializeOwned` requirements

This limitation is documented in dtolnay/serde-yaml#94 ("Can't deserialize borrowed str with from_str") and highlighted in dtolnay/request-for-implementation#9 ("Minimal YAML parser").

yami was built specifically to solve this problem:

  1. Zero-Copy AST: Directly parses &'a str into Yaml<'a> with scalars borrowing from the source buffer without heap allocations.
  2. Strict Semantics: Inspired by StrictYAML, yami eliminates the complexity and ambiguities of full YAML 1.2 (such as arbitrary object tags and silent type coercions), focusing on fast, clean, and deterministic configuration parsing.

Why yami?

  • ⚡ Zero-Copy Scalars: All plain scalars, quoted strings ('...' / "..."), booleans, and numbers borrow directly as &'a str from the input buffer.
  • 🚀 Ultra-Fast Throughput: Parses real-world configuration payloads at ~440,000 parses/sec (~2.26 µs per document, ~260 MB/sec throughput).
  • 🛡️ Lifetime Safe: Verified with compile_fail doctests to ensure borrowed AST nodes cannot outlive their source buffer.
  • 🎯 Precise Diagnostics: Reports exact 1-indexed (line, column) coordinates and structured error variants via thiserror.
  • 📐 Strict & Unambiguous: Rejects ambiguous constructs such as tabs in indentation positions (ErrorKind::TabInIndentation) and detects duplicate keys (ErrorKind::DuplicateKey).
  • 🧩 Block & Flow Hybrid: Seamlessly parses block mappings, block sequences, compact mappings (- key: val), and JSON-style inline flow collections ([...] and {...}).

Installation

Add yami to your Cargo.toml:

cargo add yami

Or manually specify it in Cargo.toml:

[dependencies]
yami = "0.1.0"

Quick Start

use yami::{parse, Yaml};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let input = r#"
service:
  name: auth-microservice
  port: 8080
  debug: true

database:
  host: db.internal
  replicas:
    - replica-1.internal
    - replica-2.internal

tags: [security, fast, zero-copy]
"#;

    // Parse into zero-copy AST
    let doc = parse(input)?;

    // Access nested values via ergonomic indexing
    assert_eq!(doc["service"]["name"], Yaml::Scalar("auth-microservice"));
    assert_eq!(doc["service"]["port"].to_i64()?, 8080);
    assert!(doc["service"]["debug"].to_bool()?);

    // Iterate over sequence elements
    let replicas = doc["database"]["replicas"].as_sequence().unwrap();
    assert_eq!(replicas[0], Yaml::Scalar("replica-1.internal"));

    // Flow sequences are parsed seamlessly
    let tags = doc["tags"].as_sequence().unwrap();
    assert_eq!(tags.len(), 3);
    assert_eq!(tags[0], Yaml::Scalar("security"));

    Ok(())
}

Zero-Copy Struct Extraction

You can extract configuration data directly into Rust domain models without allocating any String heap buffers:

use yami::{parse, Yaml};

#[derive(Debug, PartialEq)]
pub struct ServerConfig<'a> {
    pub name: &'a str,
    pub host: &'a str,
    pub port: i64,
    pub debug: bool,
    pub replicas: Vec<&'a str>,
}

impl<'a> ServerConfig<'a> {
    pub fn from_yaml(doc: &Yaml<'a>) -> Result<Self, Box<dyn std::error::Error>> {
        let name = doc["service"]["name"].as_scalar().ok_or("missing name")?;
        let host = doc["service"]["host"].as_scalar().ok_or("missing host")?;
        let port = doc["service"]["port"].to_i64()?;
        let debug = doc["service"]["debug"].to_bool()?;

        let replicas = doc["database"]["replicas"]
            .as_sequence()
            .ok_or("missing replicas")?
            .iter()
            .filter_map(|item| item.as_scalar())
            .collect();

        Ok(Self { name, host, port, debug, replicas })
    }
}

Core Data Model

pub enum Yaml<'a> {
    /// Zero-copy scalar slice into the input text
    Scalar(&'a str),
    /// Sequence of YAML nodes (block `- ` or flow `[...]`)
    Sequence(Vec<Yaml<'a>>),
    /// Key-value association mapping (block `key: val` or flow `{...}`)
    Mapping(Vec<Entry<'a>>),
}

pub struct Entry<'a> {
    pub key: Yaml<'a>,
    pub value: Yaml<'a>,
}

Navigation & Helper Methods

Method Return Type Description
doc["key"] &Yaml<'a> Index mapping by key (returns &Yaml::Scalar("") if missing)
doc[index] &Yaml<'a> Index sequence by position (returns &Yaml::Scalar("") if out-of-bounds)
doc.get("key") Option<&Yaml<'a>> Lookup mapping value by key
doc.as_scalar() Option<&'a str> Borrows scalar slice
doc.as_sequence() Option<&[Yaml<'a>]> Borrows sequence slice
doc.as_mapping() Option<&[Entry<'a>]> Borrows mapping entries slice
doc.to_bool() Result<bool, YamlError> Parses true/false, yes/no, on/off, 1/0
doc.to_i64() Result<i64, YamlError> Parses integer scalar
doc.to_f64() Result<f64, YamlError> Parses floating-point scalar

Error Handling & Diagnostics

Errors provide structured variants via thiserror and 1-indexed (line, column) source locations:

use yami::{parse, ErrorKind};

let malformed = "
server:
\thost: localhost  # Tabs in indentation are strictly forbidden
";

match parse(malformed) {
    Ok(_) => unreachable!(),
    Err(err) => {
        println!("Error: {}", err);
        // Output: yaml parse error at line 3, column 1: tab character is not allowed for indentation

        assert!(matches!(err.kind, ErrorKind::TabInIndentation));
        assert_eq!(err.position.line, 3);
        assert_eq!(err.position.column, 1);
    }
}

Benchmarks & Performance

Run the release benchmark on your machine:

cargo run --release --example benchmark

Benchmark Results (Apple Silicon M-Series)

============================================================
  yami Zero-Copy YAML Parser Benchmark
============================================================
Payload size:     623 bytes
Iterations:       100,000
Total time:       226.41 ms
Time per parse:   2.26 µs (2264 ns)
Throughput:       441,683 parses/sec
Bandwidth:        262.42 MB/sec
============================================================

Development & Testing

# Run unit and integration tests
cargo test

# Run snapshot regression tests with insta
cargo test --test snapshot_tests

# Run property-based fuzz tests with quickcheck
cargo test --test property_tests

# Run doctests (including compile_fail lifetime tests)
cargo test --doc

# Run linter checks
cargo clippy --all-targets --all-features -- -D warnings

# Check code formatting
cargo fmt --all -- --check

Requirements

  • Minimum Supported Rust Version (MSRV): Rust 1.80.0 or later.

License

Licensed under either of:

at your option.