TOML Parser
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"VALIDBasic key-value with section
numbers = [1, 2, 3]
mixed = ["a", 42, true]VALIDArrays with mixed types
config = { host = "localhost", port = 8080 }VALIDInline table syntax
[[items]]
name = "item1"
[[items]]
name = "item2"VALIDArray of tables for repeated structures
[server.ssl]
enabled = trueVALIDNested table (dotted key)
Output Format
The parser returns a TomlDocument with root key-values and named sections, each containing typed TomlValue enums.
Structure
rootGlobal key-value pairs before any section header
tablesMap of section names to their key-value pairs
Visual Overview
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
//! 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!();
}
}
# 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