Skip to content
← Back to Examples

TOML Parser

RustRubydata formatsintermediate

Parse TOML (Tom's Obvious Minimal Language) configuration files. TOML is a minimal configuration file format that's easy to read and maps unambiguously to a hash table.

Key Concepts

  • key-value pairs
  • tables and nested tables
  • arrays and inline tables
  • type inference
  • comments

Why Parse This Format?

TOML is the standard configuration format for Rust projects (Cargo.toml) and many modern tools. It's designed to be more readable than JSON while being more strictly defined than YAML.

Use Cases

  • Rust project configuration (Cargo.toml)
  • Python project configuration (pyproject.toml)
  • Application settings files
  • Build system configuration
  • Environment-specific configuration

Input Format

TOML files contain key-value pairs, sections (tables), and support various data types including strings, integers, floats, booleans, arrays, and inline tables.

key = value | [section] | [[array_table]]

Examples

title = "TOML Example" [owner] name = "Tom"VALID

Basic key-value with section

numbers = [1, 2, 3] mixed = ["a", 42, true]VALID

Arrays with mixed types

config = { host = "localhost", port = 8080 }VALID

Inline table syntax

[[items]] name = "item1" [[items]] name = "item2"VALID

Array of tables for repeated structures

[server.ssl] enabled = trueVALID

Nested table (dotted key)

Output Format

The parser returns a TomlDocument with root key-values and named sections, each containing typed TomlValue enums.

Structure

root

Global key-value pairs before any section header

tables

Map of section names to their key-value pairs

Visual Overview

TOML Parser diagram

How the Code Works

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

String Parsing

.rule(
    "basic_string",
    seq(vec![
        dynamic(str("\"")),
        dynamic(choice(vec![
            dynamic(re(r#"\\."#)),     // any escape
            dynamic(re(r#"[^"\\]+"#)), // normal chars
        ]).many()),
        dynamic(str("\"")),
    ]),
)
.rule(
    "literal_string",
    seq(vec![
        dynamic(str("'")),
        dynamic(re("[^']*").or(str("''"))),
        dynamic(str("'")),
    ]),
)

TOML supports basic and literal strings: Basic strings support escapes (`\n`, `\t`); literal strings are verbatim.

Number Parsing

.rule("integer", re("-?[0-9]+"))
.rule("float", re("-?[0-9]+\\.[0-9]+([eE][+-]?[0-9]+)?"))
.rule("hex_int", re("0x[0-9a-fA-F]+"))
.rule("oct_int", re("0o[0-7]+"))
.rule("bin_int", re("0b[01]+"))

Multiple number formats are supported: Hex, octal, and binary use `0x`, `0o`, `0b` prefixes.

Array Parsing

.rule(
    "array",
    seq(vec![
        dynamic(str("[")),
        dynamic(re("[ \t\r\n]*")),
        dynamic(/* items separated by commas */),
        dynamic(re("[ \t\r\n]*")),
        dynamic(str("]")),
    ]),
)

Arrays can contain mixed types: Arrays can span multiple lines.

Table Parsing

.rule(
    "table",
    seq(vec![
        dynamic(str("[")),
        dynamic(re("[a-zA-Z_][a-zA-Z0-9_.-]*")), // table name
        dynamic(str("]")),
    ]),
)
.rule(
    "array_table",
    seq(vec![
        dynamic(str("[[")),
        dynamic(re("[a-zA-Z_][a-zA-Z0-9_.-]*")),
        dynamic(str("]]")),
    ]),
)

Tables define sections: Array tables (`[[items]]`) create arrays of tables.

Document Parsing

pub fn parse_toml(input: &str) -> Result<String, String> {
    let mut doc = TomlDocument {
        root: HashMap::new(),
        tables: HashMap::new(),
    };
    let mut current_table = String::new();

    for line in input.lines() {
        // Table header
        if line.starts_with('[') && line.ends_with(']') {
            current_table = line[1..len-1].to_string();
            continue;
        }

        // Key-value pair
        if let Some((key, value)) = line.split_once('=') {
            if current_table.is_empty() {
                doc.root.insert(key, parse_toml_value(value));
            } else {
                doc.tables.entry(current_table.clone()).or_default().insert(key, value);
            }
        }
    }
}

Documents are parsed line by line:

Document Parsing

pub fn parse_toml(input: &str) -> Result<String, String> {
    let mut doc = TomlDocument {
        root: HashMap::new(),
        tables: HashMap::new(),
    };
    let mut current_table = String::new();

    for line in input.lines() {
        // Table header
        if line.starts_with('[') && line.ends_with(']') {
            current_table = line[1..len-1].to_string();
            continue;
        }

        // Key-value pair
        if let Some((key, value)) = line.split_once('=') {
            if current_table.is_empty() {
                doc.root.insert(key, parse_toml_value(value));
            } else {
                doc.tables.entry(current_table.clone()).or_default().insert(key, value);
            }
        }
    }
}

Documents are parsed line by line:

Source Code

Rust
//! TOML Parser Example
//!
//! This example demonstrates parsing TOML (Tom's Obvious Minimal Language)
//! configuration files. TOML is a configuration file format that's easy to read
//! and maps unambiguously to a hash table.
//!
//! Supported features:
//! - Key/value pairs: `key = "value"`
//! - Strings (basic and literal)
//! - Integers, floats, booleans
//! - Arrays: `[1, 2, 3]`
//! - Tables: `[section]`
//! - Inline tables: `{ key = "value" }`
//! - Nested tables: `[section.subsection]`
//!
//! Run with: cargo run --example toml/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 TOML grammar
fn build_toml_grammar() -> Grammar {
    GrammarBuilder::new()
        // Whitespace and comments
        .rule("ws", re("[ \t]+"))
        .rule("comment", re("#.*"))
        .rule("newline", re("[\r]?[\n]"))
        // Basic string: "..." with escape sequences
        .rule(
            "basic_string",
            seq(vec![
                dynamic(str("\"")),
                dynamic(
                    choice(vec![
                        dynamic(re(r#"\\."#)),         // any escape
                        dynamic(re(r#"[^"\\]+"#)),     // normal chars
                    ])
                    .many(),
                ),
                dynamic(str("\"")),
            ]),
        )
        // Literal string: '...' (no escapes)
        .rule(
            "literal_string",
            seq(vec![
                dynamic(str("'")),
                dynamic(re("[^']*").or(str("''"))),
                dynamic(str("'")),
            ]),
        )
        // Multiline basic string: """..."""
        .rule(
            "ml_basic_string",
            seq(vec![
                dynamic(str("\"\"\"")),
                dynamic(re("[^\"]*")),
                dynamic(str("\"\"\"")),
            ]),
        )
        // String values
        .rule(
            "string",
            choice(vec![
                dynamic(re(r#""""[^"]*""""#)), // multiline basic
                dynamic(re(r#""[^"\\]*(\\.[^"\\]*)*""#)), // basic string
                dynamic(re(r#"'[^']*'"#)), // literal string
            ]),
        )
        // Numbers
        .rule("integer", re("-?[0-9]+"))
        .rule("float", re("-?[0-9]+\\.[0-9]+([eE][+-]?[0-9]+)?"))
        .rule("hex_int", re("0x[0-9a-fA-F]+"))
        .rule("oct_int", re("0o[0-7]+"))
        .rule("bin_int", re("0b[01]+"))
        .rule(
            "number",
            choice(vec![
                dynamic(re("-?[0-9]+\\.[0-9]+([eE][+-]?[0-9]+)?")), // float
                dynamic(re("0x[0-9a-fA-F]+")),                      // hex
                dynamic(re("0o[0-7]+")),                           // octal
                dynamic(re("0b[01]+")),                            // binary
                dynamic(re("-?[0-9]+")),                           // integer
            ]),
        )
        // Boolean
        .rule("boolean", str("true").or(str("false")))
        // Date/time (ISO 8601 subset)
        .rule(
            "datetime",
            re("[0-9]{4}-[0-9]{2}-[0-9]{2}[T ][0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?"),
        )
        .rule("date", re("[0-9]{4}-[0-9]{2}-[0-9]{2}"))
        .rule("time", re("[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?"))
        // Values
        .rule(
            "value",
            choice(vec![
                dynamic(re("true|false")),                        // boolean
                dynamic(re("[0-9]{4}-[0-9]{2}-[0-9]{2}")),        // date
                dynamic(re("-?[0-9]+\\.[0-9]+([eE][+-]?[0-9]+)?")), // float
                dynamic(re("-?[0-9]+")),                          // integer
                dynamic(re(r#""[^"\\]*(\\.[^"\\]*)*""#)),         // string
                dynamic(re(r#"'[^']*'"#)),                        // literal string
            ]),
        )
        // Array
        .rule(
            "array",
            seq(vec![
                dynamic(str("[")),
                dynamic(re("[ \t\r\n]*")),
                dynamic(
                    seq(vec![
                        dynamic(re(r#"(true|false|[0-9.-]+|"[^"]*"|'[^']*')"#)),
                        dynamic(
                            seq(vec![
                                dynamic(re("[ \t\r\n]*")),
                                dynamic(str(",")),
                                dynamic(re("[ \t\r\n]*")),
                                dynamic(re(r#"(true|false|[0-9.-]+|"[^"]*"|'[^']*')"#)),
                            ])
                            .many(),
                        ),
                    ])
                    .optional(),
                ),
                dynamic(re("[ \t\r\n]*")),
                dynamic(str("]")),
            ]),
        )
        // Inline table
        .rule(
            "inline_table",
            seq(vec![
                dynamic(str("{")),
                dynamic(re("[ \t]*")),
                dynamic(
                    seq(vec![
                        dynamic(re("[a-zA-Z_][a-zA-Z0-9_]*")), // key
                        dynamic(re("[ \t]*=[ \t]*")),
                        dynamic(re(r#"(true|false|[0-9.-]+|"[^"]*"|'[^']*')"#)), // value
                        dynamic(
                            seq(vec![
                                dynamic(re("[ \t]*,[ \t]*")),
                                dynamic(re("[a-zA-Z_][a-zA-Z0-9_]*")),
                                dynamic(re("[ \t]*=[ \t]*")),
                                dynamic(re(r#"(true|false|[0-9.-]+|"[^"]*"|'[^']*')"#)),
                            ])
                            .many(),
                        ),
                    ])
                    .optional(),
                ),
                dynamic(re("[ \t]*")),
                dynamic(str("}")),
            ]),
        )
        // Key
        .rule("bare_key", re("[a-zA-Z_][a-zA-Z0-9_-]*"))
        .rule("quoted_key", re(r#""[^"]+""#))
        .rule("key", re(r#"[a-zA-Z_][a-zA-Z0-9_-]*|"[^"]+""#))
        // Key-value pair
        .rule(
            "keyval",
            seq(vec![
                dynamic(re("[a-zA-Z_][a-zA-Z0-9_-]*")), // key
                dynamic(re("[ \t]*=[ \t]*")),          // equals
                dynamic(
                    choice(vec![
                        // value, array, or inline table
                        dynamic(re(r#"\{[^}]*\}"#)),               // inline table
                        dynamic(re(r#"\[[^\]]*\]"#)),              // array
                        dynamic(re(r#"(true|false|[0-9.-]+|"[^"]*"|'[^']*')"#)), // scalar
                    ]),
                ),
            ]),
        )
        // Table header: [section] or [section.subsection]
        .rule(
            "table",
            seq(vec![
                dynamic(str("[")),
                dynamic(re("[a-zA-Z_][a-zA-Z0-9_.-]*")), // table name
                dynamic(str("]")),
            ]),
        )
        // Array of tables: [[items]]
        .rule(
            "array_table",
            seq(vec![
                dynamic(str("[[")),
                dynamic(re("[a-zA-Z_][a-zA-Z0-9_.-]*")),
                dynamic(str("]]")),
            ]),
        )
        // Line: table header, array table, keyval, comment, or empty
        .rule(
            "line",
            choice(vec![
                dynamic(re(r#"\[\[[a-zA-Z_][a-zA-Z0-9_.-]*\]\]"#)), // array table
                dynamic(re(r#"\[[a-zA-Z_][a-zA-Z0-9_.-]*\]"#)),     // table
                dynamic(re(r#"[a-zA-Z_][a-zA-Z0-9_-]*[ \t]*=[ \t].+"#)), // keyval
                dynamic(re("#.*")),                                  // comment
                dynamic(re("[ \t]*")),                               // empty
            ]),
        )
        // Root: document is a sequence of lines
        .rule(
            "document",
            seq(vec![
                dynamic(
                    seq(vec![
                        dynamic(choice(vec![
                            dynamic(re(r#"\[\[[a-zA-Z_][a-zA-Z0-9_.-]*\]\]"#)),
                            dynamic(re(r#"\[[a-zA-Z_][a-zA-Z0-9_.-]*\]"#)),
                            dynamic(re(r#"[a-zA-Z_][a-zA-Z0-9_-]*[ \t]*=[ \t].+"#)),
                            dynamic(re("#.*")),
                            dynamic(re("[ \t]*")),
                        ])),
                        dynamic(re("[\r]?[\n]")),
                    ])
                    .many(),
                ),
                dynamic(
                    choice(vec![
                        dynamic(re(r#"\[\[[a-zA-Z_][a-zA-Z0-9_.-]*\]\]"#)),
                        dynamic(re(r#"\[[a-zA-Z_][a-zA-Z0-9_.-]*\]"#)),
                        dynamic(re(r#"[a-zA-Z_][a-zA-Z0-9_-]*[ \t]*=[ \t].+"#)),
                        dynamic(re("#.*")),
                        dynamic(re("[ \t]*")),
                    ])
                    .optional(),
                ),
            ]),
        )
        .build()
}

/// Parsed TOML value
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum TomlValue {
    String(String),
    Integer(i64),
    Float(f64),
    Boolean(bool),
    Array(Vec<TomlValue>),
    Table(HashMap<String, TomlValue>),
}

/// Parsed TOML document
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TomlDocument {
    pub root: HashMap<String, TomlValue>,
    pub tables: HashMap<String, HashMap<String, TomlValue>>,
}

/// Parse a TOML string
pub fn parse_toml(input: &str) -> Result<String, String> {
    let grammar = build_toml_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))?;

    // Parse manually for structured output
    let mut doc = TomlDocument {
        root: HashMap::new(),
        tables: HashMap::new(),
    };

    let mut current_table = String::new();

    for line in input.lines() {
        let line = line.trim();

        // Skip empty lines and comments
        if line.is_empty() || line.starts_with('#') {
            continue;
        }

        // Array of tables: [[items]]
        if line.starts_with("[[") && line.ends_with("]]") {
            current_table = line[2..line.len() - 2].to_string();
            doc.tables.entry(current_table.clone()).or_default();
            continue;
        }

        // Table: [section]
        if line.starts_with('[') && line.ends_with(']') {
            current_table = line[1..line.len() - 1].to_string();
            doc.tables.entry(current_table.clone()).or_default();
            continue;
        }

        // Key-value pair
        if let Some((key, value)) = line.split_once('=') {
            let key = key.trim().to_string();
            let value = parse_toml_value(value.trim());

            if current_table.is_empty() {
                doc.root.insert(key, value);
            } else {
                doc.tables
                    .entry(current_table.clone())
                    .or_default()
                    .insert(key, value);
            }
        }
    }

    serde_json::to_string_pretty(&doc).map_err(|e| e.to_string())
}

/// Parse a TOML value
fn parse_toml_value(s: &str) -> TomlValue {
    let s = s.trim();

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

    // Boolean
    if s == "true" {
        return TomlValue::Boolean(true);
    }
    if s == "false" {
        return TomlValue::Boolean(false);
    }

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

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

    // Inline table
    if s.starts_with('{') && s.ends_with('}') {
        let inner = &s[1..s.len() - 1];
        let mut table = HashMap::new();
        if !inner.trim().is_empty() {
            for pair in inner.split(',') {
                if let Some((k, v)) = pair.split_once('=') {
                    table.insert(k.trim().to_string(), parse_toml_value(v.trim()));
                }
            }
        }
        return TomlValue::Table(table);
    }

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

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

    // Fallback to string
    TomlValue::String(s.to_string())
}

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

    let examples = [
        (
            r#"title = "TOML Example"

[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27

[database]
server = "192.168.1.1"
ports = [8000, 8001, 8002]"#,
            "Basic TOML document",
        ),
        (
            r#"[server]
host = "localhost"
port = 8080
enabled = true

[server.ssl]
enabled = false
cert = "/path/to/cert.pem""#,
            "Nested tables",
        ),
        (
            r#"numbers = [1, 2, 3]
mixed = ["hello", 42, true]
inline = { x = 1, y = 2 }"#,
            "Arrays and inline tables",
        ),
    ];

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

# TOML Parser - Ruby Implementation
#
# Parse TOML configuration files: key-value pairs, tables, arrays.
#
# Run with: ruby example/toml/basic.rb

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

require "parsanol/parslet"

# TOML parser
class TomlParser < Parsanol::Parser
  root :document

  # Document is a sequence of entries
  rule(:document) do
    (comment | table | key_value | newline).repeat.as(:document)
  end

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

  # Table: [name] or [name.sub]
  rule(:table) do
    (str("[") >>
     table_name.as(:name) >>
     str("]") >>
     newline).as(:table)
  end

  rule(:table_name) do
    (match("[a-zA-Z0-9_]") | str(".") | str("-")).repeat(1)
  end

  # Key-value pair: key = value
  rule(:key_value) do
    (key.as(:key) >>
     space? >>
     str("=") >>
     space? >>
     value.as(:value) >>
     newline).as(:key_value)
  end

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

  # Value types
  rule(:value) do
    string |
      integer |
      float |
      boolean |
      array |
      inline_table
  end

  # String: basic "..." or literal '...'
  rule(:string) do
    basic_string | literal_string
  end

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

  rule(:literal_string) do
    (str("'") >>
     (str("'").absent? >> any).repeat.as(:string) >>
     str("'")).as(:literal_string)
  end

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

  # Float: digits.digits or scientific notation
  rule(:float) do
    ((str("+") | str("-")).maybe >>
     match("[0-9]").repeat(1) >>
     str(".") >>
     match("[0-9]").repeat(1) >>
     (match("[eE]") >> (str("+") | str("-")).maybe >> match("[0-9]").repeat(1)).maybe).as(:float)
  end

  # Boolean: true or false
  rule(:boolean) do
    (str("true") | str("false")).as(:boolean)
  end

  # Array: [...]
  rule(:array) do
    str("[") >>
      space? >>
      (value >> (comma >> value).repeat).maybe.as(:elements) >>
      space? >>
      str("]").as(:array)
  end

  # Inline table: {...}
  rule(:inline_table) do
    (str("{") >>
     space? >>
     (key_value_inline >> (comma >> key_value_inline).repeat).maybe.as(:pairs) >>
     space? >>
     str("}")).as(:inline_table)
  end

  rule(:key_value_inline) do
    key.as(:key) >> space? >> str("=") >> space? >> value.as(:value)
  end

  # Helpers
  rule(:space?) { match('\s').repeat }
  rule(:comma) { str(",") >> space? }
  rule(:newline) { match('\n') | match('\r\n') }
end

# TOML result classes
TomlDocument = Struct.new(:entries) do
  def to_h
    result = {}
    current_table = nil

    entries.each do |entry|
      case entry
      when TomlTable
        current_table = entry.name
        result[current_table] ||= {}
      when TomlKeyValue
        if current_table
          result[current_table][entry.key] = entry.value
        else
          result[entry.key] = entry.value
        end
      end
    end

    result
  end
end

TomlTable = Struct.new(:name)
TomlKeyValue = Struct.new(:key, :value)
TomlComment = Struct.new(:text)

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

  rule(table: { name: simple(:n) }) { TomlTable.new(n.to_s) }

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

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

  # Value transformations
  rule(basic_string: simple(:s)) { s.to_s }
  rule(literal_string: simple(:s)) { s.to_s }
  rule(integer: simple(:i)) { i.to_s.to_i }
  rule(float: simple(:f)) { f.to_s.to_f }
  rule(boolean: simple(:b)) { b.to_s == "true" }
  rule(array: { elements: simple(:e) }) { [e] }
  rule(array: { elements: sequence(:es) }) { es }
  rule(inline_table: { pairs: simple(:p) }) { { p[:key] => p[:value] } }
  rule(inline_table: { pairs: sequence(:ps) }) do
    ps.to_h { |p| [p[:key], p[:value]] }
  end
end

# Parse TOML string
def parse_toml(str)
  parser = TomlParser.new
  transform = TomlTransform.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 "TOML Parser"
  puts "=" * 50
  puts

  toml = <<~TOML
    # This is a comment
    title = "TOML Example"

    [database]
    host = "localhost"
    port = 5432
    enabled = true
    connection_timeout = 30.5

    [server]
    hosts = ["alpha", "beta", "gamma"]
    ports = [8080, 8081, 8082]
  TOML

  puts "Input:"
  puts "-" * 50
  puts toml
  puts "-" * 50
  puts

  result = parse_toml(toml)

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