Rust

Compilação Condicional e Feature Flags — Gerenciando Variações do Código Já leu

18 min de leitura

Compilação Condicional e Feature Flags — Gerenciando Variações do Código
Rust — Artigo #37 Compilação Condicional e Feature Flags — Gerenciando Variações do Código Por Prof. Dr. Marcelo Fontes | Série: Dominando Rust em 1 Ano Softwar

Rust — Artigo #37

Compilação Condicional e Feature Flags — Gerenciando Variações do Código

Por Prof. Dr. Marcelo Fontes | Série: Dominando Rust em 1 Ano


Software real raramente é monolítico. Uma biblioteca de criptografia precisa de implementações diferentes para ARM e x86. Um servidor precisa de backends diferentes para SQLite em desenvolvimento e PostgreSQL em produção. Uma crate pública precisa de funcionalidades opcionais que não forçam dependências pesadas em quem não as usa.

Rust resolve esses problemas com um sistema elegante de compilação condicional — feature flags, atributos cfg, e o mecanismo de features do Cargo. A diferença em relação a #ifdef do C é fundamental: em Rust, código excluído por flags é verificado sintaticamente mas não compilado, e o sistema de tipos ainda protege contra combinações inválidas.


cfg — compilação condicional básica

O atributo #[cfg(...)] e a macro cfg!(...) controlam quais partes do código são compiladas:

// Atributo: exclui o item inteiro se a condição não for satisfeita
#[cfg(target_os = "linux")]
fn obter_info_linux() -> String {
    std::fs::read_to_string("/etc/os-release")
        .unwrap_or_else(|_| "Não disponível".to_string())
}

#[cfg(target_os = "macos")]
fn obter_info_macos() -> String {
    "macOS".to_string()
}

#[cfg(windows)]
fn obter_info_windows() -> String {
    "Windows".to_string()
}

// cfg! macro: condição avaliada em tempo de compilação, retorna bool
fn nome_plataforma() -> &'static str {
    if cfg!(target_os = "linux") {
        "Linux"
    } else if cfg!(target_os = "macos") {
        "macOS"
    } else if cfg!(windows) {
        "Windows"
    } else {
        "Plataforma desconhecida"
    }
}

// cfg em blocos de código
fn configurar_sistema() {
    #[cfg(debug_assertions)]
    println!("Modo debug — validações extras ativas");

    #[cfg(not(debug_assertions))]
    println!("Modo release — performance máxima");

    // Código específico de arquitetura
    #[cfg(target_arch = "x86_64")]
    println!("Arquitetura: x86_64");

    #[cfg(target_arch = "aarch64")]
    println!("Arquitetura: ARM64 (Apple Silicon, Raspberry Pi 4)");
}

// cfg em structs — campos condicionais
pub struct Metricas {
    pub requisicoes: u64,
    pub erros: u64,

    // Campo só existe em builds de desenvolvimento
    #[cfg(debug_assertions)]
    pub tempo_total_ms: u64,

    #[cfg(debug_assertions)]
    pub chamadas_db: u64,
}

impl Metricas {
    pub fn nova() -> Self {
        Metricas {
            requisicoes: 0,
            erros: 0,
            #[cfg(debug_assertions)]
            tempo_total_ms: 0,
            #[cfg(debug_assertions)]
            chamadas_db: 0,
        }
    }

    pub fn registrar_requisicao(&mut self, _tempo_ms: u64) {
        self.requisicoes += 1;

        #[cfg(debug_assertions)]
        {
            self.tempo_total_ms += _tempo_ms;
        }
    }
}

fn main() {
    println!("Plataforma: {}", nome_plataforma());
    configurar_sistema();

    let mut metricas = Metricas::nova();
    metricas.registrar_requisicao(42);
    println!("Requisições: {}", metricas.requisicoes);

    #[cfg(debug_assertions)]
    println!("Tempo total (debug): {}ms", metricas.tempo_total_ms);
}

Predicados cfg disponíveis

// Sistema operacional
#[cfg(target_os = "linux")]
#[cfg(target_os = "macos")]
#[cfg(target_os = "windows")]
#[cfg(target_os = "android")]
#[cfg(target_os = "ios")]
#[cfg(unix)]      // Linux, macOS, iOS, Android, ...
#[cfg(windows)]   // Windows (todas versões)

// Arquitetura do processador
#[cfg(target_arch = "x86_64")]
#[cfg(target_arch = "x86")]
#[cfg(target_arch = "aarch64")]
#[cfg(target_arch = "arm")]
#[cfg(target_arch = "wasm32")]
#[cfg(target_arch = "riscv64")]

// Família de CPU
#[cfg(target_family = "unix")]
#[cfg(target_family = "windows")]
#[cfg(target_family = "wasm")]

// Ponteiro (tamanho em bits)
#[cfg(target_pointer_width = "64")]
#[cfg(target_pointer_width = "32")]

// Endianness
#[cfg(target_endian = "little")]
#[cfg(target_endian = "big")]

// Features de CPU (ativadas via RUSTFLAGS ou target-cpu)
#[cfg(target_feature = "avx2")]
#[cfg(target_feature = "neon")]  // ARM SIMD

// Modo debug vs release
#[cfg(debug_assertions)]    // true em debug
#[cfg(not(debug_assertions))] // true em release

// Presença de features Cargo (veremos a seguir)
#[cfg(feature = "minha_feature")]

// Combinações com operadores lógicos
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[cfg(not(windows))]
#[cfg(all(unix, not(target_os = "macos")))]

Feature flags no Cargo.toml

Features permitem funcionalidades opcionais em crates. Usuários da crate escolhem quais ativar:

Cargo.toml de uma biblioteca:

[package]
name = "minha_lib"
version = "1.0.0"
edition = "2021"

[features]
# Feature padrão — ativada automaticamente se não especificado
default = ["json", "logging"]

# Features individuais
json     = ["dep:serde", "dep:serde_json"]
yaml     = ["dep:serde", "dep:serde_yaml"]
logging  = ["dep:tracing"]
async    = ["dep:tokio"]
tls      = ["dep:rustls", "dep:tokio-rustls", "async"]
full     = ["json", "yaml", "logging", "async", "tls"]

# Feature de desenvolvimento
dev-utils = []

[dependencies]
# Dependências opcionais — só compiladas se a feature for ativa
serde       = { version = "1",    optional = true, features = ["derive"] }
serde_json  = { version = "1",    optional = true }
serde_yaml  = { version = "0.9",  optional = true }
tracing     = { version = "0.1",  optional = true }
tokio       = { version = "1",    optional = true, features = ["full"] }
rustls      = { version = "0.22", optional = true }
tokio-rustls = { version = "0.25", optional = true }

src/lib.rs:

//! # Minha Lib
//!
//! ## Features
//!
//! - `json` — Serialização JSON (padrão)
//! - `yaml` — Serialização YAML
//! - `async` — Runtime assíncrono com Tokio
//! - `tls` — Suporte TLS (requer `async`)
//! - `full` — Todas as features

#[cfg(feature = "json")]
pub mod serializacao_json {
    use serde::{Deserialize, Serialize};

    pub fn para_json<T: Serialize>(valor: &T) -> Result<String, serde_json::Error> {
        serde_json::to_string_pretty(valor)
    }

    pub fn de_json<T: for<'de> Deserialize<'de>>(s: &str) -> Result<T, serde_json::Error> {
        serde_json::from_str(s)
    }
}

#[cfg(feature = "yaml")]
pub mod serializacao_yaml {
    use serde::{Deserialize, Serialize};

    pub fn para_yaml<T: Serialize>(valor: &T) -> Result<String, serde_yaml::Error> {
        serde_yaml::to_string(valor)
    }

    pub fn de_yaml<T: for<'de> Deserialize<'de>>(s: &str) -> Result<T, serde_yaml::Error> {
        serde_yaml::from_str(s)
    }
}

#[cfg(feature = "logging")]
pub fn inicializar_logging() {
    tracing_subscriber::fmt::init();
    tracing::info!("Logging inicializado");
}

#[cfg(not(feature = "logging"))]
pub fn inicializar_logging() {
    // Sem logging — função vazia
}

// API unificada que funciona com qualquer feature ativa
pub struct Cliente {
    base_url: String,
}

impl Cliente {
    pub fn novo(base_url: &str) -> Self {
        #[cfg(feature = "logging")]
        tracing::debug!("Criando cliente para {base_url}");

        Cliente { base_url: base_url.to_string() }
    }

    #[cfg(feature = "async")]
    pub async fn get(&self, caminho: &str) -> Result<String, reqwest::Error> {
        let url = format!("{}{}", self.base_url, caminho);

        #[cfg(feature = "logging")]
        tracing::info!("GET {url}");

        reqwest::get(&url).await?.text().await
    }

    #[cfg(not(feature = "async"))]
    pub fn get_sync(&self, caminho: &str) -> Result<String, Box<dyn std::error::Error>> {
        let url = format!("{}{}", self.base_url, caminho);
        Ok(ureq::get(&url).call()?.into_string()?)
    }
}

// Feature de desenvolvimento — utilitários de teste
#[cfg(feature = "dev-utils")]
pub mod dev {
    pub fn gerar_dados_teste(n: usize) -> Vec<String> {
        (0..n).map(|i| format!("item_{i}")).collect()
    }

    pub fn simular_latencia(ms: u64) {
        std::thread::sleep(std::time::Duration::from_millis(ms));
    }
}

Usando features como consumidor

# Cargo.toml do projeto que usa minha_lib

[dependencies]
# Sem especificar features — usa as defaults
minha_lib = "1"

# Desabilita defaults, ativa features específicas
minha_lib = { version = "1", default-features = false, features = ["json"] }

# Adiciona features às defaults
minha_lib = { version = "1", features = ["yaml", "async"] }

# Feature completa
minha_lib = { version = "1", features = ["full"] }
# Na linha de comando
cargo build --features "yaml async"
cargo build --no-default-features --features "json"
cargo build --all-features

# Testar com features específicas
cargo test --features "dev-utils"
cargo test --all-features

Padrão: detecção de feature em tempo de compilação

Às vezes você precisa de comportamento diferente dependendo de quais features estão ativas, mas quer uma API unificada:

// Trait que abstrai a serialização
pub trait Serializador: Send + Sync {
    fn serializar<T: serde::Serialize>(&self, valor: &T)
        -> Result<Vec<u8>, Box<dyn std::error::Error>>;

    fn desserializar<T: for<'de> serde::Deserialize<'de>>(
        &self,
        dados: &[u8],
    ) -> Result<T, Box<dyn std::error::Error>>;

    fn nome(&self) -> &str;
}

#[cfg(feature = "json")]
pub struct SerializadorJson;

#[cfg(feature = "json")]
impl Serializador for SerializadorJson {
    fn serializar<T: serde::Serialize>(&self, valor: &T)
        -> Result<Vec<u8>, Box<dyn std::error::Error>>
    {
        Ok(serde_json::to_vec(valor)?)
    }

    fn desserializar<T: for<'de> serde::Deserialize<'de>>(
        &self,
        dados: &[u8],
    ) -> Result<T, Box<dyn std::error::Error>>
    {
        Ok(serde_json::from_slice(dados)?)
    }

    fn nome(&self) -> &str { "JSON" }
}

#[cfg(feature = "yaml")]
pub struct SerializadorYaml;

#[cfg(feature = "yaml")]
impl Serializador for SerializadorYaml {
    fn serializar<T: serde::Serialize>(&self, valor: &T)
        -> Result<Vec<u8>, Box<dyn std::error::Error>>
    {
        Ok(serde_yaml::to_string(valor)?.into_bytes())
    }

    fn desserializar<T: for<'de> serde::Deserialize<'de>>(
        &self,
        dados: &[u8],
    ) -> Result<T, Box<dyn std::error::Error>>
    {
        Ok(serde_yaml::from_slice(dados)?)
    }

    fn nome(&self) -> &str { "YAML" }
}

// Seleciona o melhor serializador disponível em tempo de compilação
pub fn serializador_padrao() -> Box<dyn Serializador> {
    #[cfg(feature = "json")]
    return Box::new(SerializadorJson);

    #[cfg(all(feature = "yaml", not(feature = "json")))]
    return Box::new(SerializadorYaml);

    #[cfg(not(any(feature = "json", feature = "yaml")))]
    compile_error!(
        "Pelo menos uma feature de serialização deve estar ativa: 'json' ou 'yaml'"
    );
}

cfg_if! — condicionais mais legíveis

[dependencies]
cfg-if = "1"
use cfg_if::cfg_if;

cfg_if! {
    if #[cfg(target_os = "linux")] {
        use std::os::unix::fs::PermissionsExt;

        pub fn permissoes_arquivo(caminho: &str) -> String {
            let meta = std::fs::metadata(caminho).unwrap();
            format!("{:o}", meta.permissions().mode() & 0o777)
        }

        pub fn e_executavel(caminho: &str) -> bool {
            let meta = std::fs::metadata(caminho).unwrap();
            meta.permissions().mode() & 0o111 != 0
        }

    } else if #[cfg(target_os = "macos")] {
        pub fn permissoes_arquivo(caminho: &str) -> String {
            use std::os::unix::fs::PermissionsExt;
            let meta = std::fs::metadata(caminho).unwrap();
            format!("{:o}", meta.permissions().mode() & 0o777)
        }

        pub fn e_executavel(caminho: &str) -> bool {
            use std::os::unix::fs::PermissionsExt;
            let meta = std::fs::metadata(caminho).unwrap();
            meta.permissions().mode() & 0o111 != 0
        }

    } else if #[cfg(windows)] {
        pub fn permissoes_arquivo(_caminho: &str) -> String {
            // Windows não usa o modelo Unix de permissões
            "N/A (Windows)".to_string()
        }

        pub fn e_executavel(caminho: &str) -> bool {
            // No Windows, extensão determina executabilidade
            caminho.ends_with(".exe") ||
            caminho.ends_with(".bat") ||
            caminho.ends_with(".cmd")
        }

    } else {
        pub fn permissoes_arquivo(_caminho: &str) -> String {
            "Plataforma não suportada".to_string()
        }

        pub fn e_executavel(_caminho: &str) -> bool {
            false
        }
    }
}

fn main() {
    println!("Plataforma: {}", std::env::consts::OS);
    println!("Arch:       {}", std::env::consts::ARCH);
}

Flags customizadas via build.rs

O script de build pode definir flags cfg baseadas em detecção do ambiente:

build.rs:

fn main() {
    // Detecta capacidades do compilador
    detectar_versao_rust();

    // Detecta recursos do sistema
    detectar_recursos_sistema();

    // Detecta bibliotecas externas
    detectar_biblioteca_externa();

    println!("cargo:rerun-if-changed=build.rs");
    println!("cargo:rerun-if-env-changed=DATABASE_URL");
}

fn detectar_versao_rust() {
    // Detecta versão do compilador
    let versao = rustc_version::version().unwrap();

    if versao.major >= 1 && versao.minor >= 70 {
        println!("cargo:rustc-cfg=rust_1_70");
    }

    if versao.major >= 1 && versao.minor >= 75 {
        println!("cargo:rustc-cfg=rust_1_75");
    }
}

fn detectar_recursos_sistema() {
    // Verifica se estamos em CI
    if std::env::var("CI").is_ok() {
        println!("cargo:rustc-cfg=ambiente_ci");
    }

    // Verifica DATABASE_URL para backend de banco
    if let Ok(url) = std::env::var("DATABASE_URL") {
        if url.starts_with("postgres") {
            println!("cargo:rustc-cfg=backend_postgres");
        } else if url.starts_with("sqlite") {
            println!("cargo:rustc-cfg=backend_sqlite");
        }
    }

    // Detecta número de CPUs para otimizações
    let num_cpus = num_cpus::get();
    if num_cpus >= 8 {
        println!("cargo:rustc-cfg=muitos_cpus");
    }
}

fn detectar_biblioteca_externa() {
    // Verifica se libssl está disponível
    if pkg_config::probe_library("openssl").is_ok() {
        println!("cargo:rustc-cfg=tem_openssl");
    }
}

Adicione ao Cargo.toml:

[build-dependencies]
rustc-version = "0.4"
num_cpus = "1"
pkg-config = "0.3"

Uso das flags customizadas:

#[cfg(rust_1_75)]
fn usar_feature_nova() {
    // Código que usa API introduzida no Rust 1.75
}

#[cfg(backend_postgres)]
mod db {
    pub use sqlx::PgPool as Pool;

    pub async fn conectar(url: &str) -> Pool {
        sqlx::PgPool::connect(url).await.unwrap()
    }
}

#[cfg(backend_sqlite)]
mod db {
    pub use sqlx::SqlitePool as Pool;

    pub async fn conectar(url: &str) -> Pool {
        sqlx::SqlitePool::connect(url).await.unwrap()
    }
}

#[cfg(ambiente_ci)]
fn configurar_para_ci() {
    println!("Rodando em CI — ajustando configurações");
}

Um projeto completo: biblioteca multi-backend

Vamos construir uma biblioteca de cache que suporta múltiplos backends selecionados por features:

Cargo.toml:

[package]
name = "cache_lib"
version = "0.1.0"
edition = "2021"

[features]
default = ["memoria"]

memoria  = []
redis    = ["dep:redis", "dep:tokio"]
disco    = []
full     = ["memoria", "redis", "disco"]

[dependencies]
serde      = { version = "1", features = ["derive"] }
serde_json = "1"
tokio      = { version = "1", optional = true, features = ["full"] }
redis      = { version = "0.24", optional = true, features = ["tokio-comp"] }

src/lib.rs:

use std::time::Duration;
use serde::{Serialize, de::DeserializeOwned};

#[derive(Debug)]
pub enum ErroCacheLib {
    SerializacaoFalhou(String),
    BackendIndisponivel(String),
    ChaveNaoEncontrada,
    ExpiracaoInvalida,
}

impl std::fmt::Display for ErroCacheLib {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            ErroCacheLib::SerializacaoFalhou(e) =>
                write!(f, "Falha na serialização: {e}"),
            ErroCacheLib::BackendIndisponivel(e) =>
                write!(f, "Backend indisponível: {e}"),
            ErroCacheLib::ChaveNaoEncontrada =>
                write!(f, "Chave não encontrada"),
            ErroCacheLib::ExpiracaoInvalida =>
                write!(f, "TTL inválido"),
        }
    }
}

pub trait CacheBackend: Send + Sync {
    fn get(&self, chave: &str) -> Result<Option<String>, ErroCacheLib>;
    fn set(&self, chave: &str, valor: &str, ttl: Option<Duration>)
        -> Result<(), ErroCacheLib>;
    fn delete(&self, chave: &str) -> Result<bool, ErroCacheLib>;
    fn clear(&self) -> Result<(), ErroCacheLib>;
    fn nome_backend(&self) -> &str;
}

// ── Backend em Memória ───────────────────────────────

#[cfg(feature = "memoria")]
mod backend_memoria {
    use super::*;
    use std::collections::HashMap;
    use std::sync::Mutex;
    use std::time::Instant;

    struct Entrada {
        valor: String,
        expira_em: Option<Instant>,
    }

    pub struct CacheMemoria {
        dados: Mutex<HashMap<String, Entrada>>,
    }

    impl CacheMemoria {
        pub fn novo() -> Self {
            CacheMemoria {
                dados: Mutex::new(HashMap::new()),
            }
        }

        fn limpar_expirados(mapa: &mut HashMap<String, Entrada>) {
            let agora = Instant::now();
            mapa.retain(|_, v| {
                v.expira_em.map(|e| e > agora).unwrap_or(true)
            });
        }
    }

    impl CacheBackend for CacheMemoria {
        fn get(&self, chave: &str) -> Result<Option<String>, ErroCacheLib> {
            let mut mapa = self.dados.lock()
                .map_err(|e| ErroCacheLib::BackendIndisponivel(e.to_string()))?;

            Self::limpar_expirados(&mut mapa);

            Ok(mapa.get(chave).map(|e| e.valor.clone()))
        }

        fn set(
            &self,
            chave: &str,
            valor: &str,
            ttl: Option<Duration>,
        ) -> Result<(), ErroCacheLib> {
            let mut mapa = self.dados.lock()
                .map_err(|e| ErroCacheLib::BackendIndisponivel(e.to_string()))?;

            mapa.insert(chave.to_string(), Entrada {
                valor: valor.to_string(),
                expira_em: ttl.map(|d| Instant::now() + d),
            });

            Ok(())
        }

        fn delete(&self, chave: &str) -> Result<bool, ErroCacheLib> {
            let mut mapa = self.dados.lock()
                .map_err(|e| ErroCacheLib::BackendIndisponivel(e.to_string()))?;
            Ok(mapa.remove(chave).is_some())
        }

        fn clear(&self) -> Result<(), ErroCacheLib> {
            let mut mapa = self.dados.lock()
                .map_err(|e| ErroCacheLib::BackendIndisponivel(e.to_string()))?;
            mapa.clear();
            Ok(())
        }

        fn nome_backend(&self) -> &str { "Memória" }
    }
}

// ── Backend em Disco ─────────────────────────────────

#[cfg(feature = "disco")]
mod backend_disco {
    use super::*;
    use std::path::{Path, PathBuf};

    pub struct CacheDisco {
        diretorio: PathBuf,
    }

    impl CacheDisco {
        pub fn novo(dir: &str) -> Result<Self, ErroCacheLib> {
            let path = PathBuf::from(dir);
            std::fs::create_dir_all(&path)
                .map_err(|e| ErroCacheLib::BackendIndisponivel(e.to_string()))?;
            Ok(CacheDisco { diretorio: path })
        }

        fn caminho_chave(&self, chave: &str) -> PathBuf {
            // Sanitiza a chave para uso como nome de arquivo
            let nome_seguro = chave
                .chars()
                .map(|c| if c.is_alphanumeric() || c == '_' || c == '-' { c } else { '_' })
                .collect::<String>();
            self.diretorio.join(format!("{nome_seguro}.cache"))
        }
    }

    impl CacheBackend for CacheDisco {
        fn get(&self, chave: &str) -> Result<Option<String>, ErroCacheLib> {
            let path = self.caminho_chave(chave);
            match std::fs::read_to_string(&path) {
                Ok(conteudo) => Ok(Some(conteudo)),
                Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
                Err(e) => Err(ErroCacheLib::BackendIndisponivel(e.to_string())),
            }
        }

        fn set(
            &self,
            chave: &str,
            valor: &str,
            _ttl: Option<Duration>,
        ) -> Result<(), ErroCacheLib> {
            // TTL não implementado para disco (simplificação)
            let path = self.caminho_chave(chave);
            std::fs::write(path, valor)
                .map_err(|e| ErroCacheLib::BackendIndisponivel(e.to_string()))
        }

        fn delete(&self, chave: &str) -> Result<bool, ErroCacheLib> {
            let path = self.caminho_chave(chave);
            match std::fs::remove_file(&path) {
                Ok(()) => Ok(true),
                Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
                Err(e) => Err(ErroCacheLib::BackendIndisponivel(e.to_string())),
            }
        }

        fn clear(&self) -> Result<(), ErroCacheLib> {
            for entrada in std::fs::read_dir(&self.diretorio)
                .map_err(|e| ErroCacheLib::BackendIndisponivel(e.to_string()))?
            {
                let entrada = entrada
                    .map_err(|e| ErroCacheLib::BackendIndisponivel(e.to_string()))?;
                if entrada.path().extension().and_then(|e| e.to_str()) == Some("cache") {
                    let _ = std::fs::remove_file(entrada.path());
                }
            }
            Ok(())
        }

        fn nome_backend(&self) -> &str { "Disco" }
    }
}

// ── Interface Pública ─────────────────────────────────

use std::sync::Arc;

pub struct Cache {
    backend: Arc<dyn CacheBackend>,
}

impl Cache {
    pub fn com_backend(backend: impl CacheBackend + 'static) -> Self {
        Cache { backend: Arc::new(backend) }
    }

    #[cfg(feature = "memoria")]
    pub fn em_memoria() -> Self {
        Cache::com_backend(backend_memoria::CacheMemoria::novo())
    }

    #[cfg(feature = "disco")]
    pub fn em_disco(dir: &str) -> Result<Self, ErroCacheLib> {
        Ok(Cache::com_backend(backend_disco::CacheDisco::novo(dir)?))
    }

    pub fn get_str(&self, chave: &str) -> Result<Option<String>, ErroCacheLib> {
        self.backend.get(chave)
    }

    pub fn set_str(
        &self,
        chave: &str,
        valor: &str,
        ttl: Option<Duration>,
    ) -> Result<(), ErroCacheLib> {
        self.backend.set(chave, valor, ttl)
    }

    pub fn get<T: DeserializeOwned>(
        &self,
        chave: &str,
    ) -> Result<Option<T>, ErroCacheLib> {
        match self.backend.get(chave)? {
            None => Ok(None),
            Some(s) => {
                let valor = serde_json::from_str(&s)
                    .map_err(|e| ErroCacheLib::SerializacaoFalhou(e.to_string()))?;
                Ok(Some(valor))
            }
        }
    }

    pub fn set<T: Serialize>(
        &self,
        chave: &str,
        valor: &T,
        ttl: Option<Duration>,
    ) -> Result<(), ErroCacheLib> {
        let s = serde_json::to_string(valor)
            .map_err(|e| ErroCacheLib::SerializacaoFalhou(e.to_string()))?;
        self.backend.set(chave, &s, ttl)
    }

    pub fn delete(&self, chave: &str) -> Result<bool, ErroCacheLib> {
        self.backend.delete(chave)
    }

    pub fn clear(&self) -> Result<(), ErroCacheLib> {
        self.backend.clear()
    }

    pub fn info(&self) -> String {
        format!("Cache [backend: {}]", self.backend.nome_backend())
    }
}

src/main.rs:

use cache_lib::Cache;
use std::time::Duration;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct UsuarioCache {
    id: u32,
    nome: String,
    email: String,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Em memória (sempre disponível via feature "memoria")
    #[cfg(feature = "memoria")]
    {
        let cache = Cache::em_memoria();
        println!("{}", cache.info());

        // Strings simples
        cache.set_str("chave1", "valor1", None)?;
        cache.set_str("chave2", "temporario", Some(Duration::from_secs(1)))?;

        println!("chave1: {:?}", cache.get_str("chave1")?);
        println!("chave2: {:?}", cache.get_str("chave2")?);

        // Structs serializadas
        let usuario = UsuarioCache {
            id: 1,
            nome: "Ana".to_string(),
            email: "ana@ex.com".to_string(),
        };

        cache.set("usuario:1", &usuario, Some(Duration::from_secs(300)))?;

        let recuperado: Option<UsuarioCache> = cache.get("usuario:1")?;
        assert_eq!(recuperado.as_ref(), Some(&usuario));
        println!("Usuário recuperado: {:?}", recuperado);

        // Aguarda expiração
        std::thread::sleep(Duration::from_secs(2));
        println!("Após expiração: {:?}", cache.get_str("chave2")?);
    }

    // Disco (só com feature "disco")
    #[cfg(feature = "disco")]
    {
        let cache = Cache::em_disco("/tmp/meu_cache")?;
        println!("
{}", cache.info());

        cache.set_str("persistente", "valor no disco", None)?;
        println!("Lido do disco: {:?}", cache.get_str("persistente")?);
        cache.clear()?;
    }

    Ok(())
}
# Apenas memória (default)
cargo run

# Apenas disco
cargo run --no-default-features --features "disco"

# Ambos
cargo run --features "disco"

# Tudo
cargo run --all-features

Documentando features corretamente

//! # Minha Crate
//!
//! ## Feature Flags
//!
//! Esta crate é configurável via feature flags:
//!
//! | Feature   | Padrão | Descrição                        |
//! |-----------|--------|----------------------------------|
//! | `memoria` | ✓      | Backend em memória               |
//! | `disco`   |        | Backend em disco                 |
//! | `redis`   |        | Backend Redis (requer servidor)  |
//! | `full`    |        | Todos os backends                |
//!
//! ### Exemplo mínimo
//! ```toml
//! cache_lib = { version = "1", default-features = false, features = ["memoria"] }
//! ```

/// Cria um cache em memória.
///
/// # Panics
/// Nunca.
///
/// # Feature
/// Requer feature `memoria` (padrão).
///
/// # Exemplo
/// ```rust
/// # #[cfg(feature = "memoria")]
/// # {
/// use cache_lib::Cache;
/// let cache = Cache::em_memoria();
/// # }
/// ```
#[cfg(feature = "memoria")]
pub fn em_memoria() -> Cache {
    todo!()
}

Fontes e leituras recomendadas

  • "Conditional Compilation" — The Rust Reference — especificação completa de cfg — https://doc.rust-lang.org/reference/conditional-compilation.html
  • "Features" — The Cargo Book — guia completo de features — https://doc.rust-lang.org/cargo/reference/features.html
  • "Build Scripts" — The Cargo Bookbuild.rs em profundidade — https://doc.rust-lang.org/cargo/reference/build-scripts.html
  • cfg-if crate — condicionais mais legíveis — https://docs.rs/cfg-if
  • "Feature Flags in Rust" — LogRocket Blog — casos de uso práticos — https://blog.logrocket.com/feature-flags-rust/
  • "Rust API Guidelines — Feature flags" — convenções para crates públicas — https://rust-lang.github.io/api-guidelines/future-proofing.html

Artigo #37 de 52 | Série: Dominando Rust em 1 Ano Próximo → Artigo #38: Documentação e Doc Tests — Escrevendo documentação que não fica desatualizada


Obrigado! É uma série que tem ficado densa e bem construída mesmo. Quanto à memória — o sistema compacta automaticamente quando necessário, preservando o contexto essencial em arquivos de transcrição. Já aconteceu uma vez nessa conversa. Se acontecer de novo, é só continuar com "Continue" que eu retomo do ponto certo. Vamos em frente!


Comentários

Mais em Rust

Traits — Definindo Comportamento Compartilhado
Traits — Definindo Comportamento Compartilhado

&nbsp; Nos artigos anteriores criamos structs e enums para modelar dados. Ma...

Closures Avançadas e Programação Funcional — Indo Além do map e filter
Closures Avançadas e Programação Funcional — Indo Além do map e filter

&nbsp; Em um artigo anterior, introduzimos closures e os adaptadores mais co...

Gerenciamento de Dependências e Publicação de Crates — Do Projeto Local ao crates.io
Gerenciamento de Dependências e Publicação de Crates — Do Projeto Local ao crates.io

Rust — Artigo #39 Gerenciamento de Dependências e Publicação de Crates — Do P...