🦀

cultiva-pipeline — Refactorización Rust Idiomático

Revisión y corrección de antipatrones · 3 módulos · Rust edition 2021
patrones-idiomaticos-rust
CULTIVA IA · Ingeniería MLOps
Junio 2026
6
unwrap() eliminados
3
módulos refactorizados
4
patrones aplicados
0
panics en producción
Módulos refactorizados
⚠️
Módulo 1 — Error Handling con unwrap()
pipeline/processor.rs
2× unwrap() clone innecesario ✓ corregido
Antes — antipatrón
pub fn process_event(raw: &str) -> String {
    let event: Event = serde_json::from_str(raw)
        .unwrap();  // PANIC en prod
    let user = get_user(event.user_id)
        .unwrap();  // PANIC si no existe
    format!("processed:{}",
        user.email.clone()) // Clone innecesario
}
Después — idiomático
use anyhow::{Context, Result};

pub fn process_event(raw: &str) -> Result<String> {
    let event: Event = serde_json::from_str(raw)
        .context("invalid event JSON")?;
    let user = get_user(event.user_id)
        .context("user not found")?;
    Ok(format!("processed:{}", user.email))
}
🔥
Panic eliminado Cambiado a Result<String> con propagación ?. El caller decide cómo manejar el error.
Clone eliminado user.email ya es owned al mover desde la struct; no necesita .clone().
📍
Contexto de error .context() de anyhow añade mensaje legible al stack de error en logs.
🧩
Módulo 2 — Estado de Agente con Booleanos
agents/state.rs
estados imposibles struct flag explosion ✓ corregido
Antes — antipatrón
pub struct AgentState {
    pub is_running: bool,
    pub is_paused:  bool,
    pub is_failed:  bool,
    // Siempre presente, vacío al éxito
    pub error_message: String,
    pub retry_count: u64,
}
// Estado inválido posible:
// is_running=true Y is_failed=true
Después — idiomático
#[derive(Debug, Clone)]
pub enum AgentState {
    Idle,
    Running { task_id: String },
    Paused  { task_id: String, at: u64 },
    Failed  { reason: String, retries: u32 },
    Done    { output: String },
}
// Imposible estar Running Y Failed
// a la vez — el compilador lo garantiza
Antes — match frágil
if state.is_failed {
    retry(&state);
} else if state.is_running {
    monitor(&state);
}
// ¿Qué pasa con is_paused=true?
// Silenciosamente ignorado.
Después — exhaustivo
match &state {
    AgentState::Idle            => schedule_next(),
    AgentState::Running { .. }  => monitor(&state),
    AgentState::Paused  { .. }  => show_resume_btn(),
    AgentState::Failed  { r, .. }=> retry_or_abort(r),
    AgentState::Done    { .. }  => archive(),
} // El compilador obliga a cubrir todos
🛡️
Estados imposibles = irrepresentables El enum elimina 2³=8 combinaciones de booleanos, de las cuales solo 3 eran válidas.
🧠
Datos por variante Cada estado solo lleva los datos que necesita: Failed tiene reason, Running tiene task_id.
🔒
Matching exhaustivo Si añadimos Cancelled, el compilador fuerza a actualizar todos los match.
Módulo 3 — Caché Concurrente con Error Handling
db/queries.rs
2× unwrap() sin tipo de error ✓ corregido
Antes — antipatrón
static DB_CACHE: Lazy<Mutex<HashMap<String,
    Vec<u8>>>> =
    Lazy::new(|| Mutex::new(HashMap::new()));

pub fn fetch_cached(key: String) -> Vec<u8> {
    let cache = DB_CACHE.lock().unwrap();
    cache.get(&key).unwrap().clone()
    // PANIC si la key no existe
}
Después — idiomático
use thiserror::Error;

#[derive(Debug, Error)]
pub enum CacheError {
    #[error("key not found: {0}")]
    NotFound(String),
    #[error("mutex poisoned")]
    Poisoned,
}

pub fn fetch_cached(key: &str)
    -> Result<Vec<u8>, CacheError> {
    let cache = DB_CACHE.lock()
        .map_err(|_| CacheError::Poisoned)?;
    cache.get(key)
        .cloned()
        .ok_or_else(|| CacheError::NotFound(key.into()))
}
🎯
thiserror para librería CacheError con #[derive(Error)] da mensajes legibles y es typeable por el caller.
🔑
&str en lugar de String El parámetro cambia a &str — más flexible, evita allocations innecesarias al llamar.
.cloned() idiomático .cloned() sobre Option<&Vec<u8>> es más limpio que .map(|v| v.clone()).
Tabla de patrones aplicados
Patrón idiomático Dónde se aplicó Impacto Referencia Rust
Result + ? processor.rs — propagación de errores de parse y DB ALTO anyhow::Context
enum state machine state.rs — AgentState como enum con datos por variante ALTO exhaustive match
thiserror queries.rs — CacheError tipado para la capa de librería MEDIO thiserror::Error
&str vs String queries.rs — parámetro de función de String a &str MEDIO borrow don't clone
.ok_or_else() queries.rs — conversión de Option a Result con error tipado BAJO Option combinators
.cloned() queries.rs — evitar .map(|v| v.clone()) verboso BAJO iterator ergonomics
Anti-patrones eliminados
❌ .unwrap() en producción
Provoca panics no manejados. Reemplazado por ? + context() o map_err(). Los errores se propagan al caller limpiamente.
❌ Booleanos de estado
Múltiples bool permiten estados imposibles (running=true & failed=true). Enum con variantes hace los estados inválidos no compilables.
❌ String como parámetro
Fuerza al caller a hacer .to_string() o .clone(). Cambiar a &str acepta &String y literales sin allocations.