Gerenciador de Configuracoes em Rust

Construa um gerenciador de configuracoes em Rust com suporte a multiplos formatos TOML, JSON e YAML, sobrescrita por variaveis de ambiente e validacao.

Toda aplicacao de software precisa de configuracao: enderecos de servidores, credenciais, parametros de execucao, limites e opcoes. Gerenciar configuracoes de forma robusta — suportando multiplos formatos de arquivo, variaveis de ambiente e valores padrao com validacao — e um problema que aparece em praticamente todo projeto. Neste walkthrough, vamos construir um gerenciador de configuracoes completo que carrega dados de arquivos TOML, JSON ou YAML, permite sobrescrever valores via variaveis de ambiente e valida que a configuracao final e consistente.

Este projeto e excelente para dominar o ecossistema serde em Rust, entender traits como Deserialize e Default, e aprender a projetar APIs ergonomicas que outros desenvolvedores podem usar como biblioteca.

O Que Vamos Construir

Nosso gerenciador de configuracoes tera os seguintes recursos:

  • Carregamento de configuracao a partir de arquivos TOML, JSON ou YAML
  • Deteccao automatica do formato pelo nome do arquivo
  • Sobrescrita de valores via variaveis de ambiente
  • Valores padrao para todos os campos
  • Validacao com mensagens de erro descritivas
  • Merge de multiplas fontes (arquivo + ambiente + padrao)
  • Exibicao da configuracao final formatada
  • CLI para inspecionar e validar arquivos de configuracao

Estrutura do Projeto

config-manager/
├── Cargo.toml
└── src/
    ├── main.rs
    ├── config.rs
    ├── carregador.rs
    └── validador.rs

Configurando o Projeto

cargo new config-manager
cd config-manager

Configure o Cargo.toml:

[package]
name = "config-manager"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "0.8"
serde_yaml = "0.9"
clap = { version = "4", features = ["derive"] }
colored = "2"

Usamos serde como framework central de serializacao, com backends para cada formato: toml para TOML, serde_json para JSON e serde_yaml para YAML. O clap fornece a interface de linha de comando e colored a saida formatada.

Passo 1: Definindo a Estrutura de Configuracao

O modulo config.rs define a estrutura da configuracao com valores padrao e suporte a serializacao em todos os formatos.

// src/config.rs
use serde::{Deserialize, Serialize};

/// Configuracao principal da aplicacao
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Configuracao {
    /// Nome da aplicacao
    #[serde(default = "padrao_nome_app")]
    pub nome_app: String,

    /// Ambiente de execucao (desenvolvimento, producao, teste)
    #[serde(default = "padrao_ambiente")]
    pub ambiente: String,

    /// Configuracoes do servidor
    #[serde(default)]
    pub servidor: ConfigServidor,

    /// Configuracoes do banco de dados
    #[serde(default)]
    pub banco_de_dados: ConfigBancoDeDados,

    /// Configuracoes de log
    #[serde(default)]
    pub log: ConfigLog,

    /// Configuracoes de seguranca
    #[serde(default)]
    pub seguranca: ConfigSeguranca,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigServidor {
    /// Endereco de escuta
    #[serde(default = "padrao_host")]
    pub host: String,

    /// Porta do servidor
    #[serde(default = "padrao_porta")]
    pub porta: u16,

    /// Numero maximo de conexoes simultaneas
    #[serde(default = "padrao_max_conexoes")]
    pub max_conexoes: u32,

    /// Timeout de requisicao em segundos
    #[serde(default = "padrao_timeout")]
    pub timeout_segundos: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigBancoDeDados {
    /// URL de conexao com o banco
    #[serde(default = "padrao_url_banco")]
    pub url: String,

    /// Tamanho maximo do pool de conexoes
    #[serde(default = "padrao_pool_max")]
    pub pool_maximo: u32,

    /// Tamanho minimo do pool de conexoes
    #[serde(default = "padrao_pool_min")]
    pub pool_minimo: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigLog {
    /// Nivel de log: trace, debug, info, warn, error
    #[serde(default = "padrao_nivel_log")]
    pub nivel: String,

    /// Formato: texto ou json
    #[serde(default = "padrao_formato_log")]
    pub formato: String,

    /// Caminho do arquivo de log (vazio para stdout)
    #[serde(default)]
    pub arquivo: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigSeguranca {
    /// Chave secreta para tokens
    #[serde(default = "padrao_chave_secreta")]
    pub chave_secreta: String,

    /// Tempo de expiracao do token em horas
    #[serde(default = "padrao_expiracao_token")]
    pub expiracao_token_horas: u32,

    /// Origens permitidas para CORS
    #[serde(default = "padrao_cors_origens")]
    pub cors_origens: Vec<String>,
}

// Funcoes de valores padrao
fn padrao_nome_app() -> String { "minha-app".to_string() }
fn padrao_ambiente() -> String { "desenvolvimento".to_string() }
fn padrao_host() -> String { "127.0.0.1".to_string() }
fn padrao_porta() -> u16 { 8080 }
fn padrao_max_conexoes() -> u32 { 100 }
fn padrao_timeout() -> u64 { 30 }
fn padrao_url_banco() -> String { "sqlite://dados.db".to_string() }
fn padrao_pool_max() -> u32 { 10 }
fn padrao_pool_min() -> u32 { 2 }
fn padrao_nivel_log() -> String { "info".to_string() }
fn padrao_formato_log() -> String { "texto".to_string() }
fn padrao_chave_secreta() -> String { "ALTERAR_EM_PRODUCAO".to_string() }
fn padrao_expiracao_token() -> u32 { 24 }
fn padrao_cors_origens() -> Vec<String> { vec!["http://localhost:3000".to_string()] }

impl Default for Configuracao {
    fn default() -> Self {
        Self {
            nome_app: padrao_nome_app(),
            ambiente: padrao_ambiente(),
            servidor: ConfigServidor::default(),
            banco_de_dados: ConfigBancoDeDados::default(),
            log: ConfigLog::default(),
            seguranca: ConfigSeguranca::default(),
        }
    }
}

impl Default for ConfigServidor {
    fn default() -> Self {
        Self {
            host: padrao_host(),
            porta: padrao_porta(),
            max_conexoes: padrao_max_conexoes(),
            timeout_segundos: padrao_timeout(),
        }
    }
}

impl Default for ConfigBancoDeDados {
    fn default() -> Self {
        Self {
            url: padrao_url_banco(),
            pool_maximo: padrao_pool_max(),
            pool_minimo: padrao_pool_min(),
        }
    }
}

impl Default for ConfigLog {
    fn default() -> Self {
        Self {
            nivel: padrao_nivel_log(),
            formato: padrao_formato_log(),
            arquivo: String::new(),
        }
    }
}

impl Default for ConfigSeguranca {
    fn default() -> Self {
        Self {
            chave_secreta: padrao_chave_secreta(),
            expiracao_token_horas: padrao_expiracao_token(),
            cors_origens: padrao_cors_origens(),
        }
    }
}

Cada campo usa #[serde(default = "funcao")] para definir um valor padrao quando o campo esta ausente no arquivo. Isso garante que a configuracao sempre tenha valores validos, mesmo que o arquivo contenha apenas uma parte dos campos.

Passo 2: Carregador Multi-Formato com Override de Ambiente

O modulo carregador.rs detecta o formato do arquivo, carrega a configuracao e aplica variaveis de ambiente como sobrescrita.

// src/carregador.rs
use crate::config::Configuracao;
use std::env;
use std::fs;
use std::path::Path;

/// Formato do arquivo de configuracao
#[derive(Debug, Clone, PartialEq)]
pub enum Formato {
    Toml,
    Json,
    Yaml,
}

/// Erros possiveis durante o carregamento
#[derive(Debug)]
pub enum ErroCarregamento {
    ArquivoNaoEncontrado(String),
    LeituraFalhou(String),
    FormatoDesconhecido(String),
    ParseFalhou { formato: String, detalhe: String },
}

impl std::fmt::Display for ErroCarregamento {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::ArquivoNaoEncontrado(c) => {
                write!(f, "Arquivo nao encontrado: {}", c)
            }
            Self::LeituraFalhou(e) => write!(f, "Erro ao ler arquivo: {}", e),
            Self::FormatoDesconhecido(ext) => {
                write!(f, "Formato desconhecido: '{}'. Use .toml, .json ou .yaml", ext)
            }
            Self::ParseFalhou { formato, detalhe } => {
                write!(f, "Erro ao processar {} : {}", formato, detalhe)
            }
        }
    }
}

/// Detecta o formato do arquivo pela extensao
pub fn detectar_formato(caminho: &str) -> Result<Formato, ErroCarregamento> {
    let extensao = Path::new(caminho)
        .extension()
        .and_then(|e| e.to_str())
        .unwrap_or("");

    match extensao {
        "toml" => Ok(Formato::Toml),
        "json" => Ok(Formato::Json),
        "yaml" | "yml" => Ok(Formato::Yaml),
        outro => Err(ErroCarregamento::FormatoDesconhecido(outro.to_string())),
    }
}

/// Carrega a configuracao de um arquivo, aplicando valores padrao
pub fn carregar_de_arquivo(caminho: &str) -> Result<Configuracao, ErroCarregamento> {
    // Verifica se o arquivo existe
    if !Path::new(caminho).exists() {
        return Err(ErroCarregamento::ArquivoNaoEncontrado(caminho.to_string()));
    }

    // Le o conteudo do arquivo
    let conteudo = fs::read_to_string(caminho)
        .map_err(|e| ErroCarregamento::LeituraFalhou(e.to_string()))?;

    // Detecta o formato e faz o parse
    let formato = detectar_formato(caminho)?;
    let config = parse_conteudo(&conteudo, &formato)?;

    Ok(config)
}

/// Faz o parse do conteudo de acordo com o formato
fn parse_conteudo(
    conteudo: &str,
    formato: &Formato,
) -> Result<Configuracao, ErroCarregamento> {
    match formato {
        Formato::Toml => {
            toml::from_str(conteudo).map_err(|e| ErroCarregamento::ParseFalhou {
                formato: "TOML".to_string(),
                detalhe: e.to_string(),
            })
        }
        Formato::Json => {
            serde_json::from_str(conteudo).map_err(|e| ErroCarregamento::ParseFalhou {
                formato: "JSON".to_string(),
                detalhe: e.to_string(),
            })
        }
        Formato::Yaml => {
            serde_yaml::from_str(conteudo).map_err(|e| ErroCarregamento::ParseFalhou {
                formato: "YAML".to_string(),
                detalhe: e.to_string(),
            })
        }
    }
}

/// Aplica variaveis de ambiente como sobrescrita na configuracao.
/// Convencao: APP_SECAO_CAMPO (ex: APP_SERVIDOR_PORTA=9090)
pub fn aplicar_variaveis_ambiente(config: &mut Configuracao) {
    // Servidor
    if let Ok(val) = env::var("APP_NOME") {
        config.nome_app = val;
    }
    if let Ok(val) = env::var("APP_AMBIENTE") {
        config.ambiente = val;
    }
    if let Ok(val) = env::var("APP_SERVIDOR_HOST") {
        config.servidor.host = val;
    }
    if let Ok(val) = env::var("APP_SERVIDOR_PORTA") {
        if let Ok(porta) = val.parse::<u16>() {
            config.servidor.porta = porta;
        }
    }
    if let Ok(val) = env::var("APP_SERVIDOR_MAX_CONEXOES") {
        if let Ok(n) = val.parse::<u32>() {
            config.servidor.max_conexoes = n;
        }
    }
    if let Ok(val) = env::var("APP_SERVIDOR_TIMEOUT") {
        if let Ok(n) = val.parse::<u64>() {
            config.servidor.timeout_segundos = n;
        }
    }

    // Banco de dados
    if let Ok(val) = env::var("APP_BANCO_URL") {
        config.banco_de_dados.url = val;
    }
    if let Ok(val) = env::var("APP_BANCO_POOL_MAXIMO") {
        if let Ok(n) = val.parse::<u32>() {
            config.banco_de_dados.pool_maximo = n;
        }
    }
    if let Ok(val) = env::var("APP_BANCO_POOL_MINIMO") {
        if let Ok(n) = val.parse::<u32>() {
            config.banco_de_dados.pool_minimo = n;
        }
    }

    // Log
    if let Ok(val) = env::var("APP_LOG_NIVEL") {
        config.log.nivel = val;
    }
    if let Ok(val) = env::var("APP_LOG_FORMATO") {
        config.log.formato = val;
    }
    if let Ok(val) = env::var("APP_LOG_ARQUIVO") {
        config.log.arquivo = val;
    }

    // Seguranca
    if let Ok(val) = env::var("APP_SEGURANCA_CHAVE_SECRETA") {
        config.seguranca.chave_secreta = val;
    }
    if let Ok(val) = env::var("APP_SEGURANCA_EXPIRACAO_TOKEN") {
        if let Ok(n) = val.parse::<u32>() {
            config.seguranca.expiracao_token_horas = n;
        }
    }
    if let Ok(val) = env::var("APP_SEGURANCA_CORS_ORIGENS") {
        config.seguranca.cors_origens = val
            .split(',')
            .map(|s| s.trim().to_string())
            .collect();
    }
}

/// Converte a configuracao para o formato especificado
pub fn exportar(config: &Configuracao, formato: &Formato) -> Result<String, String> {
    match formato {
        Formato::Toml => {
            toml::to_string_pretty(config).map_err(|e| e.to_string())
        }
        Formato::Json => {
            serde_json::to_string_pretty(config).map_err(|e| e.to_string())
        }
        Formato::Yaml => {
            serde_yaml::to_string(config).map_err(|e| e.to_string())
        }
    }
}

A funcao aplicar_variaveis_ambiente segue a convencao APP_SECAO_CAMPO — um padrao muito comum em aplicacoes de producao (ex: Docker, Kubernetes). Variaveis numericas sao convertidas com parse, e valores invalidos sao silenciosamente ignorados, mantendo o valor original.

Passo 3: Validacao da Configuracao

O modulo validador.rs verifica se a configuracao final e consistente e segura.

// src/validador.rs
use crate::config::Configuracao;

/// Resultado da validacao com lista de erros e avisos
pub struct ResultadoValidacao {
    pub erros: Vec<String>,
    pub avisos: Vec<String>,
}

impl ResultadoValidacao {
    fn nova() -> Self {
        Self {
            erros: Vec::new(),
            avisos: Vec::new(),
        }
    }

    /// Retorna true se nao ha erros (avisos sao permitidos)
    pub fn eh_valido(&self) -> bool {
        self.erros.is_empty()
    }
}

/// Valida a configuracao e retorna erros e avisos
pub fn validar(config: &Configuracao) -> ResultadoValidacao {
    let mut resultado = ResultadoValidacao::nova();

    // Validar nome da aplicacao
    if config.nome_app.trim().is_empty() {
        resultado.erros.push(
            "O nome da aplicacao nao pode ser vazio.".to_string(),
        );
    }

    // Validar ambiente
    let ambientes_validos = ["desenvolvimento", "producao", "teste", "homologacao"];
    if !ambientes_validos.contains(&config.ambiente.as_str()) {
        resultado.erros.push(format!(
            "Ambiente '{}' invalido. Use: {}",
            config.ambiente,
            ambientes_validos.join(", ")
        ));
    }

    // Validar servidor
    if config.servidor.porta == 0 {
        resultado.erros.push(
            "A porta do servidor nao pode ser 0.".to_string(),
        );
    }
    if config.servidor.porta < 1024 && config.servidor.porta != 0 {
        resultado.avisos.push(format!(
            "Porta {} requer privilegios de root.",
            config.servidor.porta
        ));
    }
    if config.servidor.max_conexoes == 0 {
        resultado.erros.push(
            "O numero maximo de conexoes deve ser maior que 0.".to_string(),
        );
    }
    if config.servidor.timeout_segundos == 0 {
        resultado.avisos.push(
            "Timeout de 0 segundos desabilita o timeout.".to_string(),
        );
    }

    // Validar banco de dados
    if config.banco_de_dados.url.trim().is_empty() {
        resultado.erros.push(
            "A URL do banco de dados nao pode ser vazia.".to_string(),
        );
    }
    if config.banco_de_dados.pool_minimo > config.banco_de_dados.pool_maximo {
        resultado.erros.push(format!(
            "Pool minimo ({}) nao pode ser maior que o maximo ({}).",
            config.banco_de_dados.pool_minimo, config.banco_de_dados.pool_maximo
        ));
    }

    // Validar log
    let niveis_validos = ["trace", "debug", "info", "warn", "error"];
    if !niveis_validos.contains(&config.log.nivel.as_str()) {
        resultado.erros.push(format!(
            "Nivel de log '{}' invalido. Use: {}",
            config.log.nivel,
            niveis_validos.join(", ")
        ));
    }
    let formatos_validos = ["texto", "json"];
    if !formatos_validos.contains(&config.log.formato.as_str()) {
        resultado.erros.push(format!(
            "Formato de log '{}' invalido. Use: {}",
            config.log.formato,
            formatos_validos.join(", ")
        ));
    }

    // Validar seguranca
    if config.ambiente == "producao"
        && config.seguranca.chave_secreta == "ALTERAR_EM_PRODUCAO"
    {
        resultado.erros.push(
            "Chave secreta padrao detectada em ambiente de producao! Defina uma chave segura.".to_string(),
        );
    }
    if config.seguranca.chave_secreta.len() < 16 {
        resultado.avisos.push(
            "A chave secreta tem menos de 16 caracteres. Considere usar uma chave mais longa.".to_string(),
        );
    }
    if config.seguranca.expiracao_token_horas == 0 {
        resultado.erros.push(
            "O tempo de expiracao do token deve ser maior que 0.".to_string(),
        );
    }
    if config.seguranca.cors_origens.is_empty() {
        resultado.avisos.push(
            "Nenhuma origem CORS configurada. Requisicoes cross-origin serao bloqueadas.".to_string(),
        );
    }

    resultado
}

A validacao distingue entre erros (problemas que impedem a execucao) e avisos (situacoes que merecem atencao mas nao sao fatais). Isso e especialmente util para detectar configuracoes inseguras em ambiente de producao.

Passo 4: Juntando Tudo no main.rs

// src/main.rs
mod carregador;
mod config;
mod validador;

use carregador::Formato;
use clap::{Parser, Subcommand};
use colored::*;
use config::Configuracao;

#[derive(Parser)]
#[command(name = "config-manager")]
#[command(about = "Gerenciador de configuracoes multi-formato")]
struct Cli {
    #[command(subcommand)]
    comando: Comando,
}

#[derive(Subcommand)]
enum Comando {
    /// Carrega e exibe a configuracao de um arquivo
    Carregar {
        /// Caminho do arquivo de configuracao
        arquivo: String,

        /// Aplicar variaveis de ambiente como sobrescrita
        #[arg(short, long)]
        ambiente: bool,
    },

    /// Valida um arquivo de configuracao
    Validar {
        /// Caminho do arquivo de configuracao
        arquivo: String,
    },

    /// Gera um arquivo de configuracao com valores padrao
    Gerar {
        /// Formato de saida: toml, json ou yaml
        #[arg(short, long, default_value = "toml")]
        formato: String,

        /// Caminho do arquivo de saida (padrao: stdout)
        #[arg(short, long)]
        saida: Option<String>,
    },

    /// Converte um arquivo de configuracao para outro formato
    Converter {
        /// Arquivo de entrada
        entrada: String,

        /// Formato de saida: toml, json ou yaml
        #[arg(short, long)]
        formato: String,
    },
}

fn main() {
    let cli = Cli::parse();

    match cli.comando {
        Comando::Carregar { arquivo, ambiente } => {
            cmd_carregar(&arquivo, ambiente);
        }
        Comando::Validar { arquivo } => {
            cmd_validar(&arquivo);
        }
        Comando::Gerar { formato, saida } => {
            cmd_gerar(&formato, saida.as_deref());
        }
        Comando::Converter { entrada, formato } => {
            cmd_converter(&entrada, &formato);
        }
    }
}

fn cmd_carregar(arquivo: &str, aplicar_env: bool) {
    println!(
        "{} Carregando configuracao de '{}'...",
        "INFO:".blue().bold(),
        arquivo
    );

    let mut config = match carregador::carregar_de_arquivo(arquivo) {
        Ok(c) => c,
        Err(e) => {
            eprintln!("{} {}", "ERRO:".red().bold(), e);
            std::process::exit(1);
        }
    };

    if aplicar_env {
        println!(
            "{} Aplicando variaveis de ambiente...",
            "INFO:".blue().bold()
        );
        carregador::aplicar_variaveis_ambiente(&mut config);
    }

    exibir_config(&config);
}

fn cmd_validar(arquivo: &str) {
    let config = match carregador::carregar_de_arquivo(arquivo) {
        Ok(c) => c,
        Err(e) => {
            eprintln!("{} {}", "ERRO:".red().bold(), e);
            std::process::exit(1);
        }
    };

    let resultado = validador::validar(&config);

    if !resultado.avisos.is_empty() {
        println!("{}", "Avisos:".yellow().bold());
        for aviso in &resultado.avisos {
            println!("  {} {}", "AVISO:".yellow(), aviso);
        }
        println!();
    }

    if !resultado.erros.is_empty() {
        println!("{}", "Erros:".red().bold());
        for erro in &resultado.erros {
            println!("  {} {}", "ERRO:".red(), erro);
        }
        std::process::exit(1);
    }

    println!(
        "{} Configuracao valida! ({} aviso(s))",
        "OK".green().bold(),
        resultado.avisos.len()
    );
}

fn cmd_gerar(formato_str: &str, saida: Option<&str>) {
    let formato = match formato_str {
        "toml" => Formato::Toml,
        "json" => Formato::Json,
        "yaml" | "yml" => Formato::Yaml,
        _ => {
            eprintln!(
                "{} Formato '{}' nao suportado. Use: toml, json, yaml",
                "ERRO:".red().bold(),
                formato_str
            );
            std::process::exit(1);
        }
    };

    let config = Configuracao::default();
    let conteudo = match carregador::exportar(&config, &formato) {
        Ok(c) => c,
        Err(e) => {
            eprintln!("{} {}", "ERRO:".red().bold(), e);
            std::process::exit(1);
        }
    };

    match saida {
        Some(caminho) => {
            if let Err(e) = std::fs::write(caminho, &conteudo) {
                eprintln!("{} {}", "ERRO:".red().bold(), e);
                std::process::exit(1);
            }
            println!(
                "{} Configuracao gerada em '{}'",
                "OK".green().bold(),
                caminho
            );
        }
        None => {
            println!("{}", conteudo);
        }
    }
}

fn cmd_converter(entrada: &str, formato_str: &str) {
    let formato_saida = match formato_str {
        "toml" => Formato::Toml,
        "json" => Formato::Json,
        "yaml" | "yml" => Formato::Yaml,
        _ => {
            eprintln!(
                "{} Formato '{}' nao suportado. Use: toml, json, yaml",
                "ERRO:".red().bold(),
                formato_str
            );
            std::process::exit(1);
        }
    };

    let config = match carregador::carregar_de_arquivo(entrada) {
        Ok(c) => c,
        Err(e) => {
            eprintln!("{} {}", "ERRO:".red().bold(), e);
            std::process::exit(1);
        }
    };

    let conteudo = match carregador::exportar(&config, &formato_saida) {
        Ok(c) => c,
        Err(e) => {
            eprintln!("{} {}", "ERRO:".red().bold(), e);
            std::process::exit(1);
        }
    };

    println!("{}", conteudo);
}

fn exibir_config(config: &Configuracao) {
    println!("\n{}", "=== Configuracao Carregada ===".bold().cyan());
    println!("  Nome:     {}", config.nome_app);
    println!("  Ambiente: {}", config.ambiente);
    println!();
    println!("{}", "  [servidor]".bold());
    println!("    host:            {}", config.servidor.host);
    println!("    porta:           {}", config.servidor.porta);
    println!("    max_conexoes:    {}", config.servidor.max_conexoes);
    println!("    timeout:         {}s", config.servidor.timeout_segundos);
    println!();
    println!("{}", "  [banco_de_dados]".bold());
    println!("    url:             {}", config.banco_de_dados.url);
    println!("    pool_maximo:     {}", config.banco_de_dados.pool_maximo);
    println!("    pool_minimo:     {}", config.banco_de_dados.pool_minimo);
    println!();
    println!("{}", "  [log]".bold());
    println!("    nivel:           {}", config.log.nivel);
    println!("    formato:         {}", config.log.formato);
    println!(
        "    arquivo:         {}",
        if config.log.arquivo.is_empty() {
            "(stdout)"
        } else {
            &config.log.arquivo
        }
    );
    println!();
    println!("{}", "  [seguranca]".bold());
    println!("    chave_secreta:   {}...", &config.seguranca.chave_secreta[..config.seguranca.chave_secreta.len().min(8)]);
    println!("    expiracao_token: {}h", config.seguranca.expiracao_token_horas);
    println!(
        "    cors_origens:    {}",
        config.seguranca.cors_origens.join(", ")
    );
}

Como Executar

cargo build --release

Primeiro, gere um arquivo de configuracao de exemplo:

# Gerar configuracao padrao em TOML
./target/release/config-manager gerar --formato toml --saida config.toml

# Gerar em JSON
./target/release/config-manager gerar --formato json --saida config.json

# Gerar em YAML
./target/release/config-manager gerar --formato yaml --saida config.yaml

Exemplos de uso:

# Carregar e exibir configuracao
./target/release/config-manager carregar config.toml
# === Configuracao Carregada ===
#   Nome:     minha-app
#   Ambiente: desenvolvimento
#   [servidor]
#     host:            127.0.0.1
#     porta:           8080
#     ...

# Carregar com sobrescrita de variaveis de ambiente
APP_SERVIDOR_PORTA=9090 APP_AMBIENTE=producao \
  ./target/release/config-manager carregar config.toml --ambiente

# Validar configuracao
./target/release/config-manager validar config.toml
# OK Configuracao valida! (1 aviso(s))

# Converter de TOML para JSON
./target/release/config-manager converter config.toml --formato json

# Converter de JSON para YAML
./target/release/config-manager converter config.json --formato yaml

Desafios para Expandir

  1. Suporte a profiles: Implemente configuracoes por profile (ex: config.producao.toml, config.teste.toml) que herdam do arquivo base e sobrescrevem apenas os campos especificos do ambiente.

  2. Hot reload: Adicione a capacidade de monitorar o arquivo de configuracao com notify e recarregar automaticamente quando ele for alterado, notificando a aplicacao via canal.

  3. Criptografia de segredos: Implemente criptografia de campos sensiveis (chaves, senhas) no arquivo de configuracao usando a crate aes-gcm, com um comando encrypt e decrypt na CLI.

  4. Validacao com schema: Crie um sistema de schema que descreva os tipos esperados, intervalos validos e dependencias entre campos, permitindo validacao generica de qualquer estrutura.

  5. Merge de multiplos arquivos: Permita carregar e combinar configuracoes de varios arquivos em cascata (ex: base.toml + local.toml), onde arquivos posteriores sobrescrevem valores dos anteriores.

Veja Tambem