Como Ler Arquivo TOML em Rust

Aprenda a ler e escrever arquivos TOML em Rust com as crates toml e serde: config files, structs tipadas, Cargo.toml parsing e valores padrão. Código completo.

TOML (Tom’s Obvious, Minimal Language) é o formato de configuração padrão do ecossistema Rust — o próprio Cargo.toml usa este formato. Ele é mais legível que JSON e menos ambíguo que YAML, sendo ideal para arquivos de configuração. Nesta receita, você vai aprender a ler, escrever e manipular arquivos TOML em Rust.

Dependências

[package]
name = "receita-toml"
version = "0.1.0"
edition = "2021"

[dependencies]
toml = "0.8"
serde = { version = "1", features = ["derive"] }

Código Completo

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

// =============================================
// 1. Definir structs de configuração
// =============================================
#[derive(Debug, Serialize, Deserialize)]
struct Config {
    titulo: String,
    versao: String,
    debug: Option<bool>,

    servidor: Servidor,
    banco: Banco,

    #[serde(default)]
    tags: Vec<String>,

    #[serde(default)]
    extras: HashMap<String, String>,
}

#[derive(Debug, Serialize, Deserialize)]
struct Servidor {
    host: String,
    porta: u16,

    #[serde(default = "default_timeout")]
    timeout_ms: u64,

    #[serde(default)]
    cors_origens: Vec<String>,
}

#[derive(Debug, Serialize, Deserialize)]
struct Banco {
    url: String,
    pool_size: Option<u32>,

    #[serde(default = "default_pool_size")]
    max_conexoes: u32,
}

fn default_timeout() -> u64 { 5000 }
fn default_pool_size() -> u32 { 10 }

fn main() {
    // =============================================
    // 2. Parse de string TOML
    // =============================================
    println!("=== Parse de String TOML ===");

    let toml_str = r#"
titulo = "Minha Aplicação"
versao = "1.0.0"
debug = true
tags = ["web", "api", "rust"]

[servidor]
host = "0.0.0.0"
porta = 8080
timeout_ms = 3000
cors_origens = ["http://localhost:3000", "https://meusite.com"]

[banco]
url = "postgresql://localhost:5432/meudb"
pool_size = 20
max_conexoes = 50

[extras]
ambiente = "produção"
regiao = "sa-east-1"
"#;

    let config: Config = toml::from_str(toml_str).unwrap();
    println!("Título: {}", config.titulo);
    println!("Versão: {}", config.versao);
    println!("Debug: {:?}", config.debug);
    println!("Servidor: {}:{}", config.servidor.host, config.servidor.porta);
    println!("Timeout: {}ms", config.servidor.timeout_ms);
    println!("Banco: {}", config.banco.url);
    println!("Pool: {:?}", config.banco.pool_size);
    println!("Tags: {:?}", config.tags);
    println!("Extras: {:?}", config.extras);

    // =============================================
    // 3. Parse com valores padrão (campos ausentes)
    // =============================================
    println!("\n=== Valores Padrão ===");

    let toml_minimal = r#"
titulo = "App Simples"
versao = "0.1.0"

[servidor]
host = "localhost"
porta = 3000

[banco]
url = "sqlite://dados.db"
"#;

    let config: Config = toml::from_str(toml_minimal).unwrap();
    println!("Timeout (padrão): {}ms", config.servidor.timeout_ms); // 5000
    println!("Max conexões (padrão): {}", config.banco.max_conexoes); // 10
    println!("Debug (ausente): {:?}", config.debug); // None
    println!("Tags (padrão vazio): {:?}", config.tags); // []
    println!("CORS (padrão vazio): {:?}", config.servidor.cors_origens);

    // =============================================
    // 4. Ler e escrever arquivo TOML
    // =============================================
    println!("\n=== Ler/Escrever Arquivo ===");

    let caminho = "/tmp/config_teste.toml";

    // Escrever
    let config_nova = Config {
        titulo: "Meu Projeto".into(),
        versao: "2.0.0".into(),
        debug: Some(false),
        servidor: Servidor {
            host: "0.0.0.0".into(),
            porta: 9090,
            timeout_ms: 10000,
            cors_origens: vec!["*".into()],
        },
        banco: Banco {
            url: "postgresql://prod:5432/app".into(),
            pool_size: Some(30),
            max_conexoes: 100,
        },
        tags: vec!["produção".into(), "v2".into()],
        extras: HashMap::from([
            ("ambiente".into(), "prod".into()),
        ]),
    };

    let toml_output = toml::to_string_pretty(&config_nova).unwrap();
    fs::write(caminho, &toml_output).unwrap();
    println!("Arquivo salvo em: {}", caminho);
    println!("Conteúdo:\n{}", toml_output);

    // Ler de volta
    let conteudo = fs::read_to_string(caminho).unwrap();
    let config_lida: Config = toml::from_str(&conteudo).unwrap();
    println!("Lido de volta: {} v{}", config_lida.titulo, config_lida.versao);

    // Limpar
    let _ = fs::remove_file(caminho);

    // =============================================
    // 5. Parse genérico (sem struct definida)
    // =============================================
    println!("\n=== Parse Genérico ===");

    let toml_str = r#"
nome = "exemplo"
porta = 8080
ativo = true

[database]
host = "localhost"
nome = "mydb"
"#;

    let valor: toml::Value = toml::from_str(toml_str).unwrap();

    // Acessar valores com indexação
    println!("Nome: {}", valor["nome"].as_str().unwrap());
    println!("Porta: {}", valor["porta"].as_integer().unwrap());
    println!("Ativo: {}", valor["ativo"].as_bool().unwrap());
    println!("DB Host: {}", valor["database"]["host"].as_str().unwrap());

    // Verificar se chave existe
    if let Some(v) = valor.get("inexistente") {
        println!("Encontrado: {}", v);
    } else {
        println!("Chave 'inexistente' não encontrada");
    }

    // =============================================
    // 6. Tratamento de erros de parse
    // =============================================
    println!("\n=== Tratamento de Erros ===");

    let toml_invalido = r#"
titulo = "Teste"
porta = "não é número"  # campo esperava u16
"#;

    #[derive(Debug, Deserialize)]
    struct ConfigSimples {
        titulo: String,
        porta: u16,
    }

    match toml::from_str::<ConfigSimples>(toml_invalido) {
        Ok(c) => println!("Config: {:?}", c),
        Err(e) => println!("Erro de parse:\n  {}", e),
    }

    // TOML com sintaxe inválida
    let toml_quebrado = "titulo = sem aspas";
    match toml::from_str::<toml::Value>(toml_quebrado) {
        Ok(v) => println!("Valor: {:?}", v),
        Err(e) => println!("Erro de sintaxe:\n  {}", e),
    }

    // =============================================
    // 7. Exemplo prático: config de aplicação
    // =============================================
    println!("\n=== Exemplo Prático ===");

    fn carregar_config(caminho: &str) -> Result<Config, Box<dyn std::error::Error>> {
        let conteudo = fs::read_to_string(caminho)?;
        let config: Config = toml::from_str(&conteudo)?;
        Ok(config)
    }

    match carregar_config("/tmp/config_app.toml") {
        Ok(config) => println!("Config carregada: {}", config.titulo),
        Err(e) => println!("Usando config padrão (erro: {})", e),
    }
}

Saída do Programa

=== Parse de String TOML ===
Título: Minha Aplicação
Versão: 1.0.0
Debug: Some(true)
Servidor: 0.0.0.0:8080
Timeout: 3000ms
Banco: postgresql://localhost:5432/meudb
Pool: Some(20)
Tags: ["web", "api", "rust"]
Extras: {"ambiente": "produção", "regiao": "sa-east-1"}

=== Valores Padrão ===
Timeout (padrão): 5000ms
Max conexões (padrão): 10
Debug (ausente): None
Tags (padrão vazio): []
CORS (padrão vazio): []

=== Ler/Escrever Arquivo ===
Arquivo salvo em: /tmp/config_teste.toml
Conteúdo:
titulo = "Meu Projeto"
versao = "2.0.0"
debug = false
tags = ["produção", "v2"]
...

Lido de volta: Meu Projeto v2.0.0

=== Parse Genérico ===
Nome: exemplo
Porta: 8080
Ativo: true
DB Host: localhost
Chave 'inexistente' não encontrada

=== Tratamento de Erros ===
Erro de parse:
  TOML parse error at line 3, column 9...

=== Exemplo Prático ===
Usando config padrão (erro: No such file or directory...)

Dicas

  • Use Option<T> para campos que podem estar ausentes no TOML.
  • Use #[serde(default)] para fornecer valores padrão quando campos não existem.
  • Use #[serde(default = "funcao")] para valores padrão calculados por uma função.
  • toml::Value permite parse genérico quando você não conhece a estrutura de antemão.
  • toml::to_string_pretty() gera TOML formatado e legível para escrita.

Veja Também