Configuração com config ou Figment

1. Introdução à Gestão de Configuração em Rust

Em qualquer aplicação Rust que pretenda ser mais do que um script descartável, a gestão de configuração é um requisito fundamental. Sem uma biblioteca dedicada, o desenvolvedor acaba misturando std::env::var(), parsing manual de arquivos e condicionais para valores padrão — o que rapidamente se torna frágil e difícil de manter.

O ecossistema Rust oferece duas crates principais para este fim: config e Figment. A crate config é a mais antiga e tradicional, mantida pela comunidade com suporte a múltiplos formatos (JSON, TOML, YAML, HJSON, INI) e fontes (arquivos, variáveis de ambiente, strings). Já Figment nasceu como uma alternativa mais flexível e tipada, focada em composição de providers e validação.

A escolha entre elas depende do perfil do projeto: config é ideal para aplicações que precisam de suporte amplo a formatos e hierarquia simples; Figment brilha quando a tipagem forte e a validação rigorosa são prioridades.

2. Configuração com a Crate config

A crate config utiliza um ConfigBuilder para agregar fontes de configuração em uma única estrutura. O processo típico envolve definir uma struct que implementa Deserialize e fazer o merge de múltiplas fontes.

Cargo.toml:

[dependencies]
config = "0.14"
serde = { version = "1", features = ["derive"] }

Exemplo prático:

use config::{Config, ConfigError, Environment, File};
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct AppConfig {
    database_url: String,
    host: String,
    port: u16,
    log_level: String,
}

fn load_config() -> Result<AppConfig, ConfigError> {
    let cfg = Config::builder()
        // Fonte 1: valores padrão embutidos
        .set_default("host", "127.0.0.1")?
        .set_default("port", 8080)?
        .set_default("log_level", "info")?
        // Fonte 2: arquivo de configuração base
        .add_source(File::with_name("config/default"))
        // Fonte 3: arquivo específico do ambiente (ex: config/production.toml)
        .add_source(File::with_name(&format!("config/{}", get_env())).required(false))
        // Fonte 4: variáveis de ambiente com prefixo APP_
        .add_source(Environment::with_prefix("APP").separator("_"))
        .build()?;

    cfg.try_deserialize()
}

fn get_env() -> String {
    std::env::var("APP_ENV").unwrap_or_else(|_| "development".into())
}

fn main() -> Result<(), ConfigError> {
    let config = load_config()?;
    println!("Conectando a {}", config.database_url);
    Ok(())
}

Este exemplo demonstra a leitura de múltiplas fontes: defaults, arquivos por ambiente e variáveis de ambiente — tudo coalescido em uma única struct tipada.

3. Hierarquia e Sobrescrita com config

A ordem em que as fontes são adicionadas ao ConfigBuilder define a precedência: fontes posteriores sobrescrevem anteriores. É comum definir a ordem como: defaults < arquivo base < arquivo de ambiente < variáveis de ambiente.

Trabalhando com namespaces:

.add_source(Environment::with_prefix("APP")
    .prefix_separator("_")
    .list_separator(",")
    .keep_prefix(true))

Isso permite que APP_DATABASE_URL seja mapeada para database_url na struct.

Tratamento de erros:

match load_config() {
    Ok(config) => run_app(config),
    Err(ConfigError::NotFound(_)) => {
        eprintln!("Arquivo de configuração não encontrado, usando defaults");
        run_app(AppConfig::default())
    }
    Err(e) => {
        eprintln!("Erro crítico de configuração: {}", e);
        std::process::exit(1);
    }
}

Para valores opcionais, utilize Option<T> nos campos da struct — o config tratará a ausência como None.

4. Introdução ao Figment: Tipagem e Composição

Figment introduz o conceito de Provider (qualquer fonte de configuração) e Profile (separação por ambiente: dev, prod, test). A composição é feita através do método merge().

Cargo.toml:

[dependencies]
figment = { version = "0.10", features = ["toml", "env"] }
serde = { version = "1", features = ["derive"] }

Exemplo básico:

use figment::{Figment, providers::{Toml, Env, Format}};
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Serialize)]
struct AppConfig {
    database_url: String,
    host: String,
    port: u16,
    log_level: String,
}

fn load_config() -> Result<AppConfig, figment::Error> {
    let config: AppConfig = Figment::new()
        // Profile padrão: development
        .merge(Toml::file("config/default.toml"))
        // Profile específico (ex: production)
        .merge(Toml::file(format!("config/{}.toml", get_env())))
        // Variáveis de ambiente com prefixo APP_
        .merge(Env::prefixed("APP_"))
        // Extrair valores ausentes do profile
        .select(get_env())
        .extract()?;

    Ok(config)
}

fn get_env() -> String {
    std::env::var("APP_ENV").unwrap_or_else(|_| "development".into())
}

Diferente do config, Figment exige que o profile seja explicitamente selecionado, o que dá mais controle sobre qual conjunto de valores será usado.

5. Validação e Transformação com Figment

Figment permite validar e transformar valores durante o processo de extração através de Validator e Adapter.

Exemplo com validação personalizada:

use figment::{Figment, providers::{Toml, Format}, value::{Value, Dict}};
use serde::Deserialize;
use std::time::Duration;

#[derive(Debug, Deserialize)]
struct ServiceConfig {
    #[serde(deserialize_with = "deserialize_duration")]
    timeout: Duration,
    max_connections: u32,
}

fn deserialize_duration<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let s: String = serde::Deserialize::deserialize(deserializer)?;
    let duration = parse_duration::parse(&s)
        .map_err(serde::de::Error::custom)?;
    Ok(duration)
}

fn validate_max_connections(cfg: &ServiceConfig) -> Result<(), figment::Error> {
    if cfg.max_connections > 1000 {
        return Err(figment::Error::from("max_connections não pode exceder 1000"));
    }
    Ok(())
}

fn load_service_config() -> Result<ServiceConfig, figment::Error> {
    let config: ServiceConfig = Figment::new()
        .merge(Toml::file("config/service.toml"))
        .merge(Env::prefixed("SERVICE_"))
        .extract()?;

    validate_max_connections(&config)?;
    Ok(config)
}

Para validações mais complexas, é possível implementar o trait Validator para um provider customizado.

6. Práticas Avançadas: Segredos e Configuração Dinâmica

Gerenciamento de segredos com arquivos .env:

// Com config
.add_source(File::with_name(".env").required(false))

// Com Figment
.merge(Toml::file(".env").required(false))

Ambas as crates suportam o formato chave=valor de arquivos .env.

Recarregamento em tempo de execução (hot-reload):

A crate config pode ser combinada com notify para recarregar automaticamente:

use notify::{Watcher, RecursiveMode, watcher};
use std::sync::mpsc::channel;

fn watch_config(path: &str) {
    let (tx, rx) = channel();
    let mut watcher = watcher(tx, Duration::from_secs(2)).unwrap();
    watcher.watch(path, RecursiveMode::NonRecursive).unwrap();

    loop {
        match rx.recv() {
            Ok(_) => {
                // Recarregar configuração
                match load_config() {
                    Ok(new_cfg) => update_app(new_cfg),
                    Err(e) => eprintln!("Erro ao recarregar: {}", e),
                }
            }
            Err(e) => eprintln!("Erro no watcher: {}", e),
        }
    }
}

Figment não possui suporte nativo a hot-reload, sendo mais indicado para configurações estáticas.

7. Comparação Final e Decisão de Projeto

Característica config Figment
Suporte a formatos JSON, TOML, YAML, HJSON, INI TOML, JSON, YAML (via features)
Profiles/ambientes Manual (nomes de arquivo) Nativo (Profile)
Validação Manual (pós-deserialize) Validators e Adapters
Hot-reload Sim (com notify) Não nativo
Maturidade Mais antigo, estável Mais novo, ativo
Tipagem Deserialize padrão Deserialize + Serialize

Recomendação:
- Microsserviços e aplicações cloud: Figment — a separação clara por profiles e a validação integrada facilitam a gestão de múltiplos ambientes.
- Aplicações monolíticas ou legadas: config — suporte mais amplo a formatos e hot-reload são vantagens para sistemas que precisam de flexibilidade.
- Projetos com validação complexa: Figment — os validators permitem regras de negócio embutidas na configuração.

8. Exemplo Completo: Aplicação CLI com ambas as crates

Estrutura de projeto:

meu_app/
├── Cargo.toml
├── config/
│   ├── default.toml
│   ├── development.toml
│   └── production.toml
└── src/
    ├── main.rs
    ├── config_legacy.rs
    └── config_figment.rs

config/default.toml:

host = "127.0.0.1"
port = 3000
database_url = "sqlite://dev.db"
log_level = "debug"

src/main.rs:

mod config_legacy;
mod config_figment;

use axum::{Router, routing::get, Server};
use std::net::SocketAddr;

#[tokio::main]
async fn main() {
    // Escolha qual módulo usar:
    // let cfg = config_legacy::load_config().unwrap();
    let cfg = config_figment::load_config().unwrap();

    let app = Router::new()
        .route("/", get(|| async { "Hello, world!" }));

    let addr: SocketAddr = format!("{}:{}", cfg.host, cfg.port)
        .parse()
        .expect("Endereço inválido");

    println!("Servidor rodando em {}", addr);
    Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

src/config_legacy.rs:

use config::{Config, ConfigError, Environment, File};
use serde::Deserialize;

#[derive(Debug, Deserialize)]
pub struct AppConfig {
    pub host: String,
    pub port: u16,
    pub database_url: String,
    pub log_level: String,
}

pub fn load_config() -> Result<AppConfig, ConfigError> {
    let env = std::env::var("APP_ENV").unwrap_or_else(|_| "development".into());
    Config::builder()
        .add_source(File::with_name("config/default"))
        .add_source(File::with_name(&format!("config/{}", env)).required(false))
        .add_source(Environment::with_prefix("APP").separator("_"))
        .build()?
        .try_deserialize()
}

src/config_figment.rs:

use figment::{Figment, providers::{Toml, Env, Format}};
use serde::Deserialize;

#[derive(Debug, Deserialize)]
pub struct AppConfig {
    pub host: String,
    pub port: u16,
    pub database_url: String,
    pub log_level: String,
}

pub fn load_config() -> Result<AppConfig, figment::Error> {
    let env = std::env::var("APP_ENV").unwrap_or_else(|_| "development".into());
    Figment::new()
        .merge(Toml::file("config/default.toml"))
        .merge(Toml::file(format!("config/{}.toml", env)).required(false))
        .merge(Env::prefixed("APP_"))
        .select(env)
        .extract()
}

Ambos os módulos produzem a mesma estrutura AppConfig, demonstrando que a escolha entre config e Figment é mais sobre preferências de API e necessidades específicas do que sobre funcionalidade básica.

Referências