Skip to content
← Back to Examples

YAML Parser

RustRubydata formatsintermediate

Parse a subset of YAML (YAML Ain't Markup Language), a human-friendly data serialization format commonly used for configuration files and data exchange.

Key Concepts

  • indentation-based structure
  • key-value pairs
  • lists and maps
  • type inference
  • inline collections

Why Parse This Format?

YAML is widely used for configuration in CI/CD pipelines, Kubernetes, Docker Compose, and static site generators. Its indentation-based structure makes it highly readable for humans.

Use Cases

  • CI/CD pipeline configuration (GitHub Actions, GitLab CI)
  • Kubernetes resource definitions
  • Docker Compose files
  • Static site generator front matter (Jekyll, Hugo)
  • Application configuration files

Input Format

YAML uses indentation to define structure. Key-value pairs use `key: value` syntax, lists use `- item` prefix, and nested structures are defined by increased indentation.

key: value | - list_item | # comment

Examples

name: Example version: 1.0 enabled: trueVALID

Basic key-value pairs with scalar types

items: - apple - banana - cherryVALID

List with nested items

server: host: localhost port: 8080VALID

Nested map structure

inline: [a, b, c] object: { x: 1, y: 2 }VALID

Flow (inline) collections

quoted: "hello world" single: 'single quotes'VALID

Quoted string values

Output Format

The parser returns a YamlValue enum that can be Null, Boolean, Integer, Float, String, Array, or Object, matching YAML's type system.

Structure

type

YamlValue enum with variants: Null, Boolean, Integer, Float, String, Array, Object

Object

HashMap<String, YamlValue> for key-value mappings

Array

Vec<YamlValue> for sequences

Visual Overview

YAML Parser diagram

How the Code Works

This section explains how the parsing rules map to the format semantics.

Scalar Type Parsing

.rule("null_val", str("null").or(str("~")).or(str("")))
.rule(
    "bool_val",
    str("true").or(str("false")).or(str("yes")).or(str("no")),
)
.rule(
    "number",
    choice(vec![
        dynamic(re("-?[0-9]+\\.[0-9]+")), // float
        dynamic(re("-?[0-9]+")),          // integer
    ]),
)

YAML supports multiple scalar types: Null can be `null`, `~`, or empty; booleans accept `yes`/`no`.

String Parsing

.rule(
    "double_quoted",
    seq(vec![
        dynamic(str("\"")),
        dynamic(re("[^\"\\\\]*(\\\\.[^\"\\\\]*)*")),
        dynamic(str("\"")),
    ]),
)
.rule(
    "single_quoted",
    seq(vec![
        dynamic(str("'")),
        dynamic(re("[^']*")),
        dynamic(str("'")),
    ]),
)
.rule("plain_string", re("[^:#\\[\\]{}\\n\\r][^#\\n\\r]*"))

Strings can be quoted or plain: Double-quoted strings support escapes; single-quoted are literal.

Inline Collections

.rule(
    "flow_seq",
    seq(vec![
        dynamic(str("[")),
        dynamic(/* items separated by commas */),
        dynamic(str("]")),
    ]),
)
.rule(
    "flow_map",
    seq(vec![
        dynamic(str("{")),
        dynamic(/* key: value pairs */),
        dynamic(str("}")),
    ]),
)

YAML supports inline arrays and objects: These are JSON-like inline syntax.

Key-Value Parsing

.rule(
    "keyval",
    seq(vec![
        dynamic(re("[a-zA-Z_][a-zA-Z0-9_-]*|\"[^\"]+\"|'[^']+'")), // key
        dynamic(re(":[ \t]*")),                                    // colon
        dynamic(choice(vec![
            dynamic(re("[^#\\n\\r]+")), // inline value
            dynamic(re("")),            // empty (nested)
        ])),
    ]),
)

YAML uses `:` for key-value separation: Empty values indicate nested content on following lines.

List Item Parsing

.rule(
    "list_item",
    seq(vec![
        dynamic(re("[ \t]*")),
        dynamic(str("-")),
        dynamic(re("[ \t]+")),
        dynamic(/* value */),
    ]),
)

Lists use `-` prefix: Indentation determines nesting level.

Value Parsing

fn parse_yaml_value(s: &str) -> YamlValue {
    // Null
    if s.is_empty() || s == "null" || s == "~" {
        return YamlValue::Null;
    }

    // Boolean
    if s == "true" || s == "yes" {
        return YamlValue::Boolean(true);
    }

    // Inline array: [a, b, c]
    if s.starts_with('[') && s.ends_with(']') {
        let items: Vec<YamlValue> = inner.split(',')
            .map(|item| parse_yaml_value(item.trim()))
            .collect();
        return YamlValue::Array(items);
    }

    // ... more type detection
}

Values are parsed by type detection:

Value Parsing

fn parse_yaml_value(s: &str) -> YamlValue {
    // Null
    if s.is_empty() || s == "null" || s == "~" {
        return YamlValue::Null;
    }

    // Boolean
    if s == "true" || s == "yes" {
        return YamlValue::Boolean(true);
    }

    // Inline array: [a, b, c]
    if s.starts_with('[') && s.ends_with(']') {
        let items: Vec<YamlValue> = inner.split(',')
            .map(|item| parse_yaml_value(item.trim()))
            .collect();
        return YamlValue::Array(items);
    }

    // ... more type detection
}

Values are parsed by type detection:

Source Code

Rust
//! YAML Subset Parser Example
//!
//! This example demonstrates parsing a subset of YAML (YAML Ain't Markup Language).
//! YAML is a human-friendly data serialization format commonly used for configuration.
//!
//! Supported features:
//! - Key-value pairs: `key: value`
//! - Scalars: strings, numbers, booleans, null
//! - Lists: `- item`
//! - Nested maps: indentation-based
//! - Quoted strings: single and double
//! - Block scalars: `|` and `>`
//!
//! Run with: cargo run --example yaml/basic --no-default-features

use parsanol::portable::{
    parser_dsl::{choice, dynamic, re, seq, str, GrammarBuilder, ParsletExt},
    AstArena, Grammar, PortableParser,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Build YAML grammar (subset)
fn build_yaml_grammar() -> Grammar {
    GrammarBuilder::new()
        // Indentation
        .rule("indent", re("[ ]*"))
        // Comments
        .rule("comment", re("#.*"))
        // Scalar types
        .rule("null_val", str("null").or(str("~")).or(str("")))
        .rule(
            "bool_val",
            str("true").or(str("false")).or(str("yes")).or(str("no")),
        )
        .rule(
            "number",
            choice(vec![
                dynamic(re("-?[0-9]+\\.[0-9]+")), // float
                dynamic(re("-?[0-9]+")),          // integer
            ]),
        )
        // Quoted strings
        .rule(
            "double_quoted",
            seq(vec![
                dynamic(str("\"")),
                dynamic(re("[^\"\\\\]*(\\\\.[^\"\\\\]*)*")),
                dynamic(str("\"")),
            ]),
        )
        .rule(
            "single_quoted",
            seq(vec![
                dynamic(str("'")),
                dynamic(re("[^']*")),
                dynamic(str("'")),
            ]),
        )
        // Plain (unquoted) string
        .rule("plain_string", re("[^:#\\[\\]{}\\n\\r][^#\\n\\r]*"))
        // Block scalar indicators
        .rule("literal_block", str("|")) // Preserves newlines
        .rule("folded_block", str(">")) // Folds newlines
        // Flow sequences (inline arrays)
        .rule(
            "flow_seq",
            seq(vec![
                dynamic(str("[")),
                dynamic(re("[ \\t\\n\\r]*")),
                dynamic(
                    seq(vec![
                        dynamic(re("[^\\]]+")),
                        dynamic(
                            seq(vec![
                                dynamic(re("[ \\t\\n\\r]*")),
                                dynamic(str(",")),
                                dynamic(re("[ \\t\\n\\r]*")),
                                dynamic(re("[^\\]]+")),
                            ])
                            .many(),
                        ),
                    ])
                    .optional(),
                ),
                dynamic(re("[ \\t\\n\\r]*")),
                dynamic(str("]")),
            ]),
        )
        // Flow mappings (inline objects)
        .rule(
            "flow_map",
            seq(vec![
                dynamic(str("{")),
                dynamic(re("[ \\t\\n\\r]*")),
                dynamic(
                    seq(vec![
                        dynamic(re("[^}]+")),
                        dynamic(
                            seq(vec![
                                dynamic(re("[ \\t\\n\\r]*")),
                                dynamic(str(",")),
                                dynamic(re("[ \\t\\n\\r]*")),
                                dynamic(re("[^}]+")),
                            ])
                            .many(),
                        ),
                    ])
                    .optional(),
                ),
                dynamic(re("[ \\t\\n\\r]*")),
                dynamic(str("}")),
            ]),
        )
        // Value (scalar or inline collection)
        .rule(
            "value",
            choice(vec![
                dynamic(re("\\[[^\\]]*\\]")),                    // flow sequence
                dynamic(re("\\{[^}]*\\}")),                      // flow mapping
                dynamic(re("\"[^\"\\\\]*(\\\\.[^\"\\\\]*)*\"")), // double quoted
                dynamic(re("'[^']*'")),                          // single quoted
                dynamic(re("true|false|yes|no")),                // boolean
                dynamic(re("null|~")),                           // null
                dynamic(re("-?[0-9]+\\.[0-9]+")),                // float
                dynamic(re("-?[0-9]+")),                         // integer
                dynamic(re("[^:#\\[\\]{}\\n\\r][^#\\n\\r]*")),   // plain string
            ]),
        )
        // Key (with optional quotes)
        .rule(
            "key",
            choice(vec![
                dynamic(re("\"[^\"\\\\]*(\\\\.[^\"\\\\]*)*\"")),
                dynamic(re("'[^']*'")),
                dynamic(re("[a-zA-Z_][a-zA-Z0-9_-]*")),
            ]),
        )
        // Key-value pair: `key: value` or `key:` (for nested)
        .rule(
            "keyval",
            seq(vec![
                dynamic(re("[a-zA-Z_][a-zA-Z0-9_-]*|\"[^\"]+\"|'[^']+'")), // key
                dynamic(re(":[ \t]*")),                                    // colon
                dynamic(choice(vec![
                    dynamic(re("[^#\\n\\r]+")), // inline value
                    dynamic(re("")),            // empty (nested)
                ])),
            ]),
        )
        // List item: `- value`
        .rule(
            "list_item",
            seq(vec![
                dynamic(re("[ \t]*")),
                dynamic(str("-")),
                dynamic(re("[ \t]+")),
                dynamic(choice(vec![
                    dynamic(re("\\[[^\\]]*\\]")),
                    dynamic(re("\\{[^}]*\\}")),
                    dynamic(re("[^#\\n\\r]+")),
                ])),
            ]),
        )
        // Document structure
        .rule(
            "document",
            seq(vec![
                dynamic(
                    seq(vec![
                        dynamic(choice(vec![
                            dynamic(re("[a-zA-Z_][a-zA-Z0-9_-]*:[ \t]*[^#\\n\\r]*")), // keyval
                            dynamic(re("[ \t]*-[^#\\n\\r]*")),                        // list item
                            dynamic(re("#.*")),                                       // comment
                            dynamic(re("")),                                          // empty
                        ])),
                        dynamic(re("[\r]?[\n]")),
                    ])
                    .many(),
                ),
                dynamic(
                    choice(vec![
                        dynamic(re("[a-zA-Z_][a-zA-Z0-9_-]*:[ \t]*[^#\\n\\r]*")),
                        dynamic(re("[ \t]*-[^#\\n\\r]*")),
                        dynamic(re("#.*")),
                        dynamic(re("")),
                    ])
                    .optional(),
                ),
            ]),
        )
        .build()
}

/// YAML value types
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum YamlValue {
    Null,
    Boolean(bool),
    Integer(i64),
    Float(f64),
    String(String),
    Array(Vec<YamlValue>),
    Object(HashMap<String, YamlValue>),
}

/// Parse YAML string
pub fn parse_yaml(input: &str) -> Result<String, String> {
    let grammar = build_yaml_grammar();
    let mut arena = AstArena::for_input(input.len());
    let mut parser = PortableParser::new(&grammar, input, &mut arena);
    let _ast = parser
        .parse()
        .map_err(|e| format!("Parse error: {:?}", e))?;

    // Manual parsing for structured output
    let result = parse_yaml_document(input)?;
    serde_json::to_string_pretty(&result).map_err(|e| e.to_string())
}

/// Parse a YAML document
fn parse_yaml_document(input: &str) -> Result<YamlValue, String> {
    let mut result: HashMap<String, YamlValue> = HashMap::new();
    let mut current_list: Option<Vec<YamlValue>> = None;
    let mut list_key: Option<String> = None;

    for line in input.lines() {
        // Skip empty lines and comments
        let trimmed = line.trim();
        if trimmed.is_empty() || trimmed.starts_with('#') {
            continue;
        }

        // List item
        if trimmed.starts_with("- ") || trimmed.starts_with("-\t") {
            let value = parse_yaml_value(trimmed[1..].trim());
            if let Some(ref mut list) = current_list {
                list.push(value);
            } else if let Some(key) = &list_key {
                let list = result
                    .entry(key.clone())
                    .or_insert_with(|| YamlValue::Array(vec![]));
                if let YamlValue::Array(ref mut arr) = list {
                    arr.push(value);
                }
            }
            continue;
        }

        // Key-value pair
        if let Some(colon_pos) = trimmed.find(':') {
            let key = trimmed[..colon_pos].trim();
            let value_str = trimmed[colon_pos + 1..].trim();

            // Check if this starts a list
            if value_str.is_empty() {
                list_key = Some(key.to_string());
                current_list = Some(vec![]);
                result.insert(key.to_string(), YamlValue::Array(vec![]));
            } else {
                list_key = None;
                current_list = None;
                let value = parse_yaml_value(value_str);
                result.insert(key.to_string(), value);
            }
        }
    }

    Ok(YamlValue::Object(result))
}

/// Parse a YAML value
fn parse_yaml_value(s: &str) -> YamlValue {
    let s = s.trim();

    // Remove trailing comment
    let s = if let Some(pos) = s.find('#') {
        &s[..pos]
    } else {
        s
    };
    let s = s.trim();

    // Null
    if s.is_empty() || s == "null" || s == "~" {
        return YamlValue::Null;
    }

    // Boolean
    if s == "true" || s == "yes" {
        return YamlValue::Boolean(true);
    }
    if s == "false" || s == "no" {
        return YamlValue::Boolean(false);
    }

    // Quoted string
    if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
        return YamlValue::String(s[1..s.len() - 1].to_string());
    }

    // Inline array: [a, b, c]
    if s.starts_with('[') && s.ends_with(']') {
        let inner = &s[1..s.len() - 1];
        if inner.trim().is_empty() {
            return YamlValue::Array(vec![]);
        }
        let items: Vec<YamlValue> = inner
            .split(',')
            .map(|item| parse_yaml_value(item.trim()))
            .collect();
        return YamlValue::Array(items);
    }

    // Inline object: {a: 1, b: 2}
    if s.starts_with('{') && s.ends_with('}') {
        let inner = &s[1..s.len() - 1];
        let mut obj = HashMap::new();
        if !inner.trim().is_empty() {
            for pair in inner.split(',') {
                if let Some(colon_pos) = pair.find(':') {
                    let k = pair[..colon_pos].trim();
                    let v = pair[colon_pos + 1..].trim();
                    obj.insert(k.to_string(), parse_yaml_value(v));
                }
            }
        }
        return YamlValue::Object(obj);
    }

    // Float
    if s.contains('.') {
        if let Ok(n) = s.parse::<f64>() {
            return YamlValue::Float(n);
        }
    }

    // Integer
    if let Ok(n) = s.parse::<i64>() {
        return YamlValue::Integer(n);
    }

    // Plain string
    YamlValue::String(s.to_string())
}

fn main() {
    println!("YAML Subset Parser Example");
    println!("==========================\n");

    let examples = [
        (
            r#"name: YAML Example
version: 1.0
enabled: true
count: 42
pi: 3.14
"#,
            "Basic key-value pairs",
        ),
        (
            r#"server:
  host: localhost
  port: 8080
database:
  host: db.example.com
  port: 5432
"#,
            "Nested configuration",
        ),
        (
            r#"items:
  - apple
  - banana
  - cherry
numbers:
  - 1
  - 2
  - 3
"#,
            "Lists",
        ),
        (
            r#"inline: [a, b, c]
object: { x: 1, y: 2 }
quoted: "hello world"
single: 'single quotes'
"#,
            "Inline and quoted values",
        ),
    ];

    for (input, description) in examples {
        println!("Input: ({})", description);
        println!("{}\n", input);
        match parse_yaml(input) {
            Ok(json) => println!("Output:\n{}\n", json),
            Err(e) => println!("Error: {}\n", e),
        }
        println!("{}", "-".repeat(60));
        println!();
    }
}
Ruby
# frozen_string_literal: true

# YAML Parser - Ruby Implementation
#
# Parse a subset of YAML: key-value pairs, lists, nested maps, scalars.
#
# Run with: ruby example/yaml/basic.rb

$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib"

require "parsanol/parslet"

# YAML parser (subset)
class YamlParser < Parsanol::Parser
  root :document

  # Document is a sequence of mappings or list items
  rule(:document) do
    (mapping | list_item | comment | blank_line).repeat(1).as(:document)
  end

  # Comment: # to end of line
  rule(:comment) do
    (space? >> str("#") >> (newline.absent? >> any).repeat).as(:comment) >> newline
  end

  # Blank line
  rule(:blank_line) { space? >> newline }

  # Mapping (key-value)
  rule(:mapping) do
    (key.as(:key) >>
     colon >>
     (inline_value | indented_value)).as(:mapping)
  end

  rule(:key) do
    (match("[a-zA-Z_]") >> match("[a-zA-Z0-9_]").repeat).as(:key)
  end

  rule(:colon) { str(":") >> space }

  # Inline value: on same line as key
  rule(:inline_value) do
    space? >> (string | integer | float | boolean | null).as(:value) >> newline
  end

  # Indented value: nested block
  rule(:indented_value) do
    newline >> indented_block.as(:block)
  end

  rule(:indented_block) do
    (indent >> (mapping | list_item) >> (newline >> indent >> (mapping | list_item)).repeat).as(:block)
  end

  # List item: - value
  rule(:list_item) do
    (str("-") >> space >>
     (inline_list_value | indented_value)).as(:list_item)
  end

  rule(:inline_list_value) do
    space? >> (string | integer | float | boolean | null).as(:value) >> newline
  end

  # Scalar types
  rule(:string) do
    quoted_string | plain_string
  end

  rule(:quoted_string) do
    (str('"') >>
     ((str("\\").ignore >> any) | (str('"').absent? >> any)).repeat.as(:string) >>
     str('"')) |
      (str("'") >>
       (str("'").absent? >> any).repeat.as(:string) >>
       str("'"))
  end

  rule(:plain_string) do
    (newline.absent? >> str(":").absent? >> any).repeat(1).as(:string)
  end

  rule(:integer) do
    (str("+") | str("-")).maybe >>
      match("[0-9]").repeat(1)
  end

  rule(:float) do
    (str("+") | str("-")).maybe >>
      match("[0-9]").repeat(1) >>
      str(".") >>
      match("[0-9]").repeat(1)
  end

  rule(:boolean) do
    str("true") | str("false")
  end

  rule(:null) do
    str("null") | str("~")
  end

  # Helpers
  rule(:space) { match('\s').repeat(1) }
  rule(:space?) { match('\s').repeat }
  rule(:newline) { match('\n') | match('\r\n') }
  rule(:indent) { str("  ") | str("\t") }
end

# YAML result classes
YamlDocument = Struct.new(:entries) do
  def to_h
    result = {}
    entries.each do |entry|
      case entry
      when YamlMapping
        result[entry.key] = entry.value
      end
    end
    result
  end
end

YamlMapping = Struct.new(:key, :value)
YamlListItem = Struct.new(:value)
YamlComment = Struct.new(:text)

# Transform parse tree to AST
class YamlTransform < Parsanol::Transform
  rule(document: sequence(:entries)) { YamlDocument.new(entries) }

  rule(mapping: { key: simple(:k), value: simple(:v) }) do
    YamlMapping.new(k.to_s, v)
  end

  rule(mapping: { key: simple(:k), block: simple(:b) }) do
    YamlMapping.new(k.to_s, b)
  end

  rule(list_item: { value: simple(:v) }) do
    YamlListItem.new(v)
  end

  rule(list_item: { block: simple(:b) }) do
    YamlListItem.new(b)
  end

  rule(comment: simple(:c)) { YamlComment.new(c.to_s) }

  # Value transformations
  rule(string: simple(:s)) { s.to_s.strip }
  rule(integer: simple(:i)) { i.to_s.to_i }
  rule(float: simple(:f)) { f.to_s.to_f }
  rule(value: simple(:v)) { v }
  rule(block: simple(:b)) { b }
  rule(block: sequence(:bs)) do
    result = {}
    bs.each do |b|
      result[b.key] = b.value if b.is_a?(YamlMapping)
    end
    result
  end
end

# Parse YAML string
def parse_yaml(str)
  parser = YamlParser.new
  transform = YamlTransform.new

  tree = parser.parse(str)
  transform.apply(tree)
rescue Parsanol::ParseError => e
  puts "Parse error: #{e.message}"
  nil
end

# Main demo
if __FILE__ == $PROGRAM_NAME
  puts "YAML Parser"
  puts "=" * 50
  puts

  yaml = <<~YAML
    # Configuration file
    name: Example Application
    version: 1.0.0
    debug: true
    timeout: 30.5

    database:
      host: localhost
      port: 5432
      name: myapp

    servers:
      - alpha
      - beta
      - gamma
  YAML

  puts "Input:"
  puts "-" * 50
  puts yaml
  puts "-" * 50
  puts

  result = parse_yaml(yaml)

  if result
    puts "Parsed AST:"
    pp result
    puts
    puts "Hash Output:"
    pp result.to_h
  end
end