toml

serializationRustTOMLconfiguration-filesCargo

Serialization Library

toml

Overview

The toml crate is a TOML (Tom's Obvious, Minimal Language) parser and serializer for Rust. TOML is a human-readable configuration file format that's widely adopted in the Rust community as the standard format for configuration files, including Cargo.toml. Complete integration with Serde enables type-safe configuration management.

Details

The toml crate provides comprehensive functionality for reading, writing, and parsing TOML format data. TOML has a minimal and intuitive syntax, designed to be easy for both humans and machines to work with.

Key Features:

  • Human-Readable Format: Intuitive key-value pair structure
  • Serde Integration: Full compatibility with Serde's Serialize/Deserialize traits
  • Rich Data Types: Support for strings, numbers, booleans, datetimes, arrays, and tables
  • Error Handling: Detailed error messages with parse position information
  • Cargo Compatibility: Standard configuration format in the Rust ecosystem

TOML Format Characteristics:

  • Hierarchical structure with sections (tables)
  • Comment support (#)
  • Multi-line strings
  • Native date and time support
  • Inline tables and arrays

Technical Details:

  • Table type: Mapping from String to Value enum
  • Value enum: Includes String, Integer, Float, Boolean, Datetime, Array, and Table
  • toml::from_str: Deserialization from TOML strings
  • toml::to_string: Serialization to TOML format

Pros and Cons

Pros

  • Excellent readability and ease of editing
  • Standard format in the Rust community
  • Rich data type support
  • Compatibility with semantic versioning
  • Documentation through comments
  • Clear syntax error messages

Cons

  • Less adoption compared to JSON or YAML
  • Not suitable for complex nested structures
  • Schema validation requires additional tools
  • Some advanced features (references, anchors) not supported

References

Code Examples

Basic Usage

use serde::{Serialize, Deserialize};
use toml;

#[derive(Serialize, Deserialize, Debug)]
struct Config {
    title: String,
    owner: Owner,
    database: Database,
}

#[derive(Serialize, Deserialize, Debug)]
struct Owner {
    name: String,
    email: String,
}

#[derive(Serialize, Deserialize, Debug)]
struct Database {
    server: String,
    port: u16,
    max_connections: u32,
    enabled: bool,
}

fn main() -> Result<(), toml::de::Error> {
    let toml_str = r#"
        title = "My Application"

        [owner]
        name = "John Doe"
        email = "[email protected]"

        [database]
        server = "192.168.1.1"
        port = 5432
        max_connections = 100
        enabled = true
    "#;
    
    // Deserialize from TOML
    let config: Config = toml::from_str(toml_str)?;
    println!("Config: {:#?}", config);
    
    // Serialize to TOML
    let toml_string = toml::to_string(&config)?;
    println!("Serialized:\n{}", toml_string);
    
    Ok(())
}

Cargo.toml-style Configuration

use serde::{Serialize, Deserialize};
use std::collections::HashMap;

#[derive(Serialize, Deserialize, Debug)]
struct Package {
    name: String,
    version: String,
    edition: String,
    authors: Vec<String>,
    description: Option<String>,
    license: Option<String>,
}

#[derive(Serialize, Deserialize, Debug)]
struct Dependency {
    version: Option<String>,
    features: Option<Vec<String>>,
    default_features: Option<bool>,
}

#[derive(Serialize, Deserialize, Debug)]
struct Manifest {
    package: Package,
    dependencies: HashMap<String, toml::Value>,
    #[serde(rename = "dev-dependencies")]
    dev_dependencies: Option<HashMap<String, toml::Value>>,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let toml_content = r#"
        [package]
        name = "my-project"
        version = "0.1.0"
        edition = "2021"
        authors = ["Alice <[email protected]>", "Bob <[email protected]>"]
        description = "A sample Rust project"
        license = "MIT"

        [dependencies]
        serde = { version = "1.0", features = ["derive"] }
        tokio = { version = "1.0", features = ["full"] }
        reqwest = "0.11"

        [dev-dependencies]
        criterion = "0.5"
    "#;
    
    let manifest: Manifest = toml::from_str(toml_content)?;
    println!("Project name: {}", manifest.package.name);
    println!("Dependencies: {:#?}", manifest.dependencies);
    
    Ok(())
}

File I/O

use serde::{Serialize, Deserialize};
use std::fs;

#[derive(Serialize, Deserialize, Debug)]
struct AppConfig {
    app: AppSettings,
    server: ServerSettings,
    features: Features,
}

#[derive(Serialize, Deserialize, Debug)]
struct AppSettings {
    name: String,
    version: String,
    debug: bool,
}

#[derive(Serialize, Deserialize, Debug)]
struct ServerSettings {
    host: String,
    port: u16,
    workers: usize,
    timeout: u64,
}

#[derive(Serialize, Deserialize, Debug)]
struct Features {
    authentication: bool,
    logging: bool,
    metrics: bool,
}

fn load_config(path: &str) -> Result<AppConfig, Box<dyn std::error::Error>> {
    let contents = fs::read_to_string(path)?;
    let config = toml::from_str(&contents)?;
    Ok(config)
}

fn save_config(config: &AppConfig, path: &str) -> Result<(), Box<dyn std::error::Error>> {
    let toml_string = toml::to_string_pretty(config)?;
    fs::write(path, toml_string)?;
    Ok(())
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = AppConfig {
        app: AppSettings {
            name: "MyApp".to_string(),
            version: "1.0.0".to_string(),
            debug: false,
        },
        server: ServerSettings {
            host: "127.0.0.1".to_string(),
            port: 8080,
            workers: 4,
            timeout: 30,
        },
        features: Features {
            authentication: true,
            logging: true,
            metrics: false,
        },
    };
    
    // Save configuration
    save_config(&config, "config.toml")?;
    
    // Load configuration
    let loaded_config = load_config("config.toml")?;
    println!("Loaded config: {:#?}", loaded_config);
    
    Ok(())
}

Complex Data Structures

use serde::{Serialize, Deserialize};
use chrono::{DateTime, Utc};

#[derive(Serialize, Deserialize, Debug)]
struct Project {
    meta: Metadata,
    environments: Vec<Environment>,
    tasks: Vec<Task>,
}

#[derive(Serialize, Deserialize, Debug)]
struct Metadata {
    created_at: DateTime<Utc>,
    updated_at: DateTime<Utc>,
    tags: Vec<String>,
}

#[derive(Serialize, Deserialize, Debug)]
struct Environment {
    name: String,
    variables: toml::Table,
    active: bool,
}

#[derive(Serialize, Deserialize, Debug)]
struct Task {
    id: u32,
    name: String,
    command: String,
    args: Vec<String>,
    env: Option<String>,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let toml_content = r#"
        [meta]
        created_at = 2024-01-01T00:00:00Z
        updated_at = 2024-01-15T12:00:00Z
        tags = ["rust", "project", "example"]

        [[environments]]
        name = "development"
        active = true
        
        [environments.variables]
        DATABASE_URL = "postgresql://localhost/dev_db"
        LOG_LEVEL = "debug"
        
        [[environments]]
        name = "production"
        active = false
        
        [environments.variables]
        DATABASE_URL = "postgresql://prod.example.com/prod_db"
        LOG_LEVEL = "info"

        [[tasks]]
        id = 1
        name = "build"
        command = "cargo"
        args = ["build", "--release"]
        
        [[tasks]]
        id = 2
        name = "test"
        command = "cargo"
        args = ["test"]
        env = "development"
    "#;
    
    let project: Project = toml::from_str(toml_content)?;
    
    println!("Project created at: {}", project.meta.created_at);
    println!("Environments:");
    for env in &project.environments {
        println!("  - {} (active: {})", env.name, env.active);
        for (key, value) in &env.variables {
            println!("    {}: {}", key, value);
        }
    }
    
    Ok(())
}

Dynamic TOML Manipulation

use toml::{Table, Value};

fn main() {
    // Build TOML dynamically
    let mut root = Table::new();
    
    // Add basic values
    root.insert("title".to_string(), Value::String("Dynamic Config".to_string()));
    root.insert("version".to_string(), Value::Integer(1));
    root.insert("enabled".to_string(), Value::Boolean(true));
    
    // Create nested table
    let mut database = Table::new();
    database.insert("host".to_string(), Value::String("localhost".to_string()));
    database.insert("port".to_string(), Value::Integer(5432));
    root.insert("database".to_string(), Value::Table(database));
    
    // Add array
    let features = vec![
        Value::String("async".to_string()),
        Value::String("logging".to_string()),
        Value::String("metrics".to_string()),
    ];
    root.insert("features".to_string(), Value::Array(features));
    
    // Output as TOML
    let toml_string = toml::to_string_pretty(&root).unwrap();
    println!("{}", toml_string);
    
    // Access values
    if let Some(Value::String(title)) = root.get("title") {
        println!("Title: {}", title);
    }
    
    if let Some(Value::Table(db)) = root.get("database") {
        if let Some(Value::Integer(port)) = db.get("port") {
            println!("Database port: {}", port);
        }
    }
}

Error Handling

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct StrictConfig {
    name: String,
    port: u16,
    timeout: u32,
}

fn parse_config(toml_str: &str) -> Result<StrictConfig, toml::de::Error> {
    toml::from_str(toml_str)
}

fn main() {
    // Valid TOML
    let valid_toml = r#"
        name = "MyService"
        port = 8080
        timeout = 30
    "#;
    
    match parse_config(valid_toml) {
        Ok(config) => println!("Valid config: {:?}", config),
        Err(e) => eprintln!("Error: {}", e),
    }
    
    // TOML with type error
    let invalid_toml = r#"
        name = "MyService"
        port = "not a number"
        timeout = 30
    "#;
    
    match parse_config(invalid_toml) {
        Ok(config) => println!("Config: {:?}", config),
        Err(e) => {
            eprintln!("Parse error: {}", e);
            // Error details
            if let Some(span) = e.span() {
                eprintln!("Error at line {}, column {}", 
                    span.start + 1, 
                    span.end + 1
                );
            }
        }
    }
}