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
- Documentação oficial da crate config — API completa, exemplos de uso com múltiplas fontes e formatos.
- Documentação oficial da crate Figment — Guia de referência com providers, profiles e validators.
- Configuração em Rust: guia prático com config e Figment (blog) — Tutorial comparativo com exemplos reais de aplicações.
- Rust Design Patterns: Configuration — Padrões de design para gerenciamento de configuração em Rust.
- Serde documentation for Deserialize — Essencial para entender como estruturas de configuração são mapeadas.
- Crate notify para hot-reload — Biblioteca para monitoramento de arquivos, útil com config para recarregamento dinâmico.
- Figment vs config: uma análise comparativa — Discussão da comunidade sobre prós e contras de cada abordagem.