---
name: rust-experto
description: >
  Rust idiomático: ownership, lifetimes, traits, async con Tokio y Axum/Actix-web.
  Cubre SQLx, serde, clap, tracing y error handling con thiserror y anyhow.
  Cargar cuando se implementen servicios Rust, APIs HTTP o lógica de sistemas.
version: "1.0.0"
herramientasPermitidas: [Read, Bash]
exclusiones:
  - "No cargar para errores de compilación Rust (borrow checker, lifetimes) — para build errors cargar `build-errors-rust`."
  - "No cargar para diseñar patrones idiomáticos Rust (newtype, builder, RAII) — para diseño cargar `rust-patrones`."
  - "No cargar para escribir tests Rust (rstest, proptest, mockall) — para testing cargar `rust-testing`."
  - "No cargar para C/C++ o sistemas no-Rust — este skill cubre el ecosistema Rust específicamente."
evolvable: true  # default para skill estandar
---
# Rust Experto — Rust Idiomático y Servicios Async

Cubre el desarrollo Rust moderno: modelo de ownership y borrowing, lifetime
elision, trait design, async con Tokio, routing con Axum, acceso a base de datos
con SQLx y observabilidad con tracing.

## Cuándo cargar este skill

Invoca `Skill("rust-experto")` cuando:

- Se implementen endpoints HTTP con Axum o Actix-web
- Se usen queries de base de datos con SQLx
- Se diseñen traits con object safety
- Se configure async runtime con Tokio
- Se implemente error handling con thiserror o anyhow
- Se serialice/deserialice JSON con serde

## Cuándo NO cargar

- Los errores son de compilación Rust (borrow checker, lifetime annotations) — para build errors cargar `build-errors-rust`.
- La pregunta es sobre diseñar patrones idiomáticos Rust (newtype, builder con typestate, RAII) — para diseño cargar `rust-patrones`.
- La tarea es escribir tests Rust (rstest, proptest, mockall) — cargar `rust-testing`.
- El proyecto es C, C++ o un sistema no-Rust — este skill cubre el ecosistema Rust específicamente.

## Conceptos clave

### Ownership: cada valor tiene un único dueño

Cuando el dueño sale de scope, el valor se dropea. La transferencia (move) cede
la propiedad; el borrowing (`&T` o `&mut T`) presta sin ceder. El borrow checker
garantiza en tiempo de compilación que no hay data races ni use-after-free.

### Lifetime elision

El compilador infiere lifetimes en la mayoría de casos. Anotaciones explícitas
(`'a`) solo cuando el compilador no puede inferir la relación entre referencias
en firmas de funciones o structs que almacenan referencias.

### Traits y object safety

Un trait es object-safe si puede usarse como `dyn Trait`. Condiciones: no puede
tener métodos con `Self` en posición de retorno (excepto `Box<Self>`), ni generics
en métodos. Traits sin `dyn`: usar generics con bounds.

### async/await con Tokio

Rust no tiene runtime async incluido. Tokio es el runtime de producción. Las
funciones `async fn` retornan Futures; `tokio::spawn` lanza tareas en el threadpool.

## Reglas obligatorias

### .unwrap() y .expect() prohibidos en código de producción

`unwrap()` causa panic en producción. Usar `?` operator para propagar errores,
o manejar explícitamente con `match`/`if let`. `.expect("mensaje")` solo en tests.

```rust
// MAL
let conn = pool.acquire().await.unwrap();

// BIEN
let conn = pool.acquire().await
    .map_err(|e| AppError::Database(e))?;
```

### Clone solo cuando ownership no es viable

`clone()` copia datos en heap, potencialmente costoso. Primero explorar si
borrowing o restructuring del código evita la necesidad.

```rust
// MAL — clone para evitar pensar en ownership
let nombre = usuario.nombre.clone();
procesar(nombre);
procesar_otro(usuario.nombre.clone());

// BIEN — borrowing cuando es posible
procesar(&usuario.nombre);
```

### Arc<Mutex<T>> como último recurso — evaluar alternativas primero

Para estado compartido entre threads, evaluar primero:
1. Pasar ownership por canal (mpsc)
2. Estado inmutable compartido con Arc<T>
3. Solo si mutacion concurrente es inevitable: Arc<Mutex<T>>

### Implementar thiserror en errores de libreria, anyhow en binarios

`thiserror` genera implementaciones de `Error` con mensajes descriptivos para
errores de dominio. `anyhow` provee contexto en binarios sin forzar tipos de error.

## Patrones recomendados

### Axum router con estado compartido

```rust
#[derive(Clone)]
struct AppState {
    db: PgPool,
    config: Arc<Config>,
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let pool = PgPoolOptions::new()
        .max_connections(10)
        .connect(&config.database_url).await?;

    let state = AppState { db: pool, config: Arc::new(config) };

    let app = Router::new()
        .route("/v1/pedidos", get(listar_pedidos).post(crear_pedido))
        .route("/v1/pedidos/:id", get(obtener_pedido))
        .with_state(state)
        .layer(TraceLayer::new_for_http());

    let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;
    axum::serve(listener, app).await?;
    Ok(())
}
```

### Handler Axum con extractor y error handling

```rust
async fn crear_pedido(
    State(state): State<AppState>,
    Json(req): Json<CrearPedidoRequest>,
) -> Result<(StatusCode, Json<PedidoResponse>), AppError> {
    req.validate()?;

    let pedido = sqlx::query_as!(
        Pedido,
        "INSERT INTO pedidos (cliente_id, total) VALUES ($1, $2) RETURNING *",
        req.cliente_id,
        req.calcular_total()
    )
    .fetch_one(&state.db)
    .await
    .map_err(AppError::Database)?;

    Ok((StatusCode::CREATED, Json(pedido.into())))
}
```

### Error handling con thiserror

```rust
#[derive(Debug, thiserror::Error)]
pub enum AppError {
    #[error("recurso no encontrado: {0}")]
    NotFound(String),

    #[error("error de base de datos")]
    Database(#[from] sqlx::Error),

    #[error("validacion fallida: {0}")]
    Validation(String),
}

// IntoResponse para Axum
impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, mensaje) = match &self {
            AppError::NotFound(_) => (StatusCode::NOT_FOUND, self.to_string()),
            AppError::Validation(_) => (StatusCode::BAD_REQUEST, self.to_string()),
            AppError::Database(_) => (StatusCode::INTERNAL_SERVER_ERROR, "error interno".into()),
        };
        (status, Json(json!({ "error": mensaje }))).into_response()
    }
}
```

### Serde con rename y skip

```rust
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PedidoResponse {
    pub id: Uuid,
    pub cliente_id: Uuid,
    pub total: Decimal,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub notas: Option<String>,
    #[serde(with = "time::serde::rfc3339")]
    pub creado_en: OffsetDateTime,
}
```

### Tokio select! para concurrencia con cancelacion

```rust
tokio::select! {
    resultado = procesar_pedido(pedido_id) => {
        resultado?;
    }
    _ = tokio::time::sleep(Duration::from_secs(30)) => {
        return Err(AppError::Timeout);
    }
    _ = shutdown_signal.recv() => {
        tracing::info!("shutdown solicitado, cancelando operacion");
    }
}
```

### Tracing para observabilidad

```rust
#[instrument(skip(state), fields(cliente_id = %req.cliente_id))]
async fn crear_pedido(
    State(state): State<AppState>,
    Json(req): Json<CrearPedidoRequest>,
) -> Result<Json<PedidoResponse>, AppError> {
    tracing::info!("creando pedido");
    let pedido = state.pedido_service.crear(req).await?;
    tracing::info!(pedido_id = %pedido.id, "pedido creado");
    Ok(Json(pedido))
}
```

## Anti-patrones conocidos

### transmute para castear tipos

```rust
// MAL — undefined behavior, bypass del type system
let bytes: &[u8] = unsafe { std::mem::transmute(mi_struct) };

// BIEN — implementar From/Into o usar serde para serializar
let bytes = bincode::serialize(&mi_struct)?;
```

### Exceso de cloning para satisfacer al borrow checker

```rust
// MAL — clone en cada iteracion
for item in &items {
    procesar(item.nombre.clone(), item.id.clone());
}

// BIEN — borrowing o referencias
for item in &items {
    procesar(&item.nombre, item.id);
}
```

## Checklist de verificación

- [ ] Sin .unwrap() en código de producción: usar ? o match
- [ ] Clone explícito y justificado, no como solución rápida al borrow checker
- [ ] Errores de dominio con thiserror; propagación en binarios con anyhow
- [ ] Handlers Axum retornan Result<_, AppError> con IntoResponse implementado
- [ ] Tracing con #[instrument] en funciones de servicio y handlers
- [ ] Arc<Mutex<T>> solo cuando la alternativa con canales no aplica
- [ ] SQLx con query_as! para types chequeados en compilación

## Rosetta Stone: Python → Rust

Tabla de equivalencias para devs que migran desde Python:

| Python | Rust | Notas |
|--------|------|-------|
| `list` | `Vec<T>` | Dinámico, tipado en compilación |
| `dict` | `HashMap<K,V>` | Requiere `K: Hash + Eq` |
| `set` | `HashSet<T>` | Mismo requerimiento |
| `tuple` | `(T1, T2, ...)` | Inmutable por defecto |
| `class` | `struct` + `impl` | Sin herencia — composición via traits |
| `@dataclass` | `#[derive(Debug, Clone, PartialEq)]` | Derivación en compilación |
| `Enum` | `enum` | Con datos en variantes — mucho más poderoso |
| `None` | `Option<T>` | El compilador obliga a manejar el caso vacío |
| `raise` / `try` / `except` | `Result<T, E>` + `?` | Sin excepciones — errores como valores |
| `Protocol` (PEP 544) | `trait` | Polimorfismo en compilación |
| `TypeVar` | Genéricos `<T>` | Monomorphización — cero overhead |
| `__dunder__` methods | Traits (`Display`, `Add`, `Eq`...) | Implementar el trait correcto |
| `lambda` | `\|args\| body` | Closures — capturan el ambiente |
| `generator yield` | `impl Iterator` | Lazy por defecto, sin runtime extra |
| `[x for x in data if x > 0]` | `.iter().filter(...).map(...).collect()` | Más verboso, type-safe, más rápido |
| `@decorator` | Higher-order fn o macro | `#[tracing::instrument]` es un decorador |
| `asyncio` | `tokio` | Rust no incluye runtime — Tokio es el estándar |
| `threading` | `std::thread` | Sin GIL — data races bloqueadas en compilación |
| `multiprocessing` | `rayon` | Paralelismo de datos idiomático |
| `unittest.mock` | `mockall` | Mocking basado en traits |
| `pytest` | `cargo test` + `rstest` | Tests inline en el mismo archivo |
| `pip install` | `cargo add` | Modifica Cargo.toml |
| `requirements.txt` | `Cargo.lock` | Lockfile determinístico |
| `pyproject.toml` | `Cargo.toml` | Metadatos del crate |
| `with` (context manager) | Scope-based `Drop` | RAII — sin `with`, el compilador lo hace |
| `json.dumps` / `loads` | `serde_json::to_string` / `from_str` | Type-safe en compilación |
| `dataclasses.field(default_factory=list)` | `#[serde(default)]` + `Vec::new()` | |

**Errores comunes al llegar desde Python:**

| Error | Por qué | Fix |
|-------|---------|-----|
| `.unwrap()` en todo | Pánico en runtime | Usar `?` o `match` |
| `String` en parámetros | Allocación innecesaria | Usar `&str` — acepta ambos |
| `for i in 0..vec.len()` | No idiomático | `for item in &vec` |
| Clones para evitar el borrow checker | Overhead de memoria | Refactorizar ownership |
| Función `main()` gigante | Hard to test | Separar en `lib.rs` y `main.rs` |
| Ignorar warnings de clippy | Pierde correcciones fáciles | `cargo clippy` como `ruff` |

**Referencia completa:** `temp/RustTraining-main/python-book/src/ch15-migration-patterns.md` y `ch16-best-practices.md`

## Rosetta Stone: C# → Rust

Tabla de equivalencias para devs que migran desde C#:

| C# | Rust | Diferencia clave |
|----|------|-----------------|
| `class` | `struct` + `impl` | Sin herencia de implementación |
| `interface` | `trait` | Traits son más potentes (default impls, blanket impls) |
| `abstract class` | `trait` con métodos default | Sin estado en el trait |
| `class Dog : Animal, IFlyable` | `impl Animal for Dog` + `impl Flyable for Dog` | Composición explícita |
| `record` (inmutabilidad aparente) | `struct` (inmutabilidad real) | `let x = ...` es inmutable; `mut` es explícito |
| `string` | `String` (owned) / `&str` (borrowed) | Dos tipos distintos — `&str` para parámetros |
| `string?` | `Option<String>` | El compilador fuerza el manejo de `None` |
| `int?` / `Nullable<T>` | `Option<T>` | Mismo concepto, sin boxing |
| `try` / `catch` | `Result<T, E>` + `?` | Errores como valores — no excepciones de control |
| `throw new X()` | `return Err(X)` | Explícito en la firma de la función |
| `List<T>` | `Vec<T>` | Sin carga de GC |
| `Dictionary<K,V>` | `HashMap<K,V>` | Requiere `K: Hash + Eq` |
| `IEnumerable<T>` | `impl Iterator<Item=T>` | Lazy, zero-cost |
| `LINQ .Where().Select()` | `.iter().filter().map().collect()` | Sin boxing, tipo comprobado |
| `async Task<T>` | `async fn() -> T` | Sin runtime incluido — requiere Tokio |
| `await task` | `.await` | Mismo concepto, distinta sintaxis |
| `IDisposable` + `using` | `Drop` trait + scope | RAII automático — no se puede olvidar |
| `lock(obj)` | `Mutex<T>`, `RwLock<T>` | El dato está dentro del lock — no se puede acceder sin él |
| `static readonly` | `static` + `OnceLock<T>` | Inicialización lazy thread-safe |
| `partial class` | Múltiples `impl` blocks | Sin limitación de archivos |
| `sealed class` | Sealed traits (patrón manual) | Con `mod` privado y trait privado |
| `null` | No existe | `Option<T>` lo reemplaza completamente |
| Generics `where T : IFoo` | `fn f<T: Foo>(...)` | Bounds en compilación — monomorphización |
| `NuGet` | `cargo add` / `Cargo.toml` | Registry centralizado (crates.io) |
| `packages.lock.json` | `Cargo.lock` | Lockfile determinístico |

**Diferencias conceptuales críticas:**

| Concepto C# | Comportamiento C# | Comportamiento Rust |
|------------|------------------|-------------------|
| Inmutabilidad en `record` | Shallow — referencias mutables filtran | Real — el compilador bloquea mutación |
| GC | Automático, latencia impredecible | Sin GC — Drop determinístico |
| Herencia | `class Dog : Animal` reutiliza implementación | No existe — usar composición con traits |
| Nulos | `NullReferenceException` en runtime | Imposible — `Option<T>` obliga al manejo |
| Excepciones | Stack unwinding — control de flujo | `Result<T,E>` — errores en la firma |

**Referencia completa:** `temp/RustTraining-main/csharp-book/src/` — Ch3-1, Ch7-1, Ch9-1, Ch10-2

## Integración con wiki del proyecto (Fase 3)

Para indexar el conocimiento de RustTraining en el wiki del proyecto usando el patrón Karpathy:

```bash
# Ingerir libro de patrones de ingeniería
/swl:wiki ingest temp/RustTraining-main/engineering-book/src/

# Ingerir libro de type-driven correctness
/swl:wiki ingest temp/RustTraining-main/type-driven-correctness-book/src/

# Ingerir libro de patrones Rust
/swl:wiki ingest temp/RustTraining-main/rust-patterns-book/src/

# Consultar el wiki tras el ingest
/swl:wiki query "¿cuándo usar type-state vs enum dispatch?"
/swl:wiki query "cómo configurar cargo-deny para licencias comerciales"
/swl:wiki query "benchmarking con criterion vs divan"
```

**Cuándo hacer el ingest:**
- Al iniciar un proyecto Rust nuevo con swl-ses
- Antes de una sesión de code review con `revisor-rust-swl`
- Cuando el equipo tiene devs migrando desde Python o C#

---
## Gotchas / Errores comunes no obvios

**`async fn` en un trait no es directamente usable como `dyn Trait`**: en Rust estable, `trait MiTrait { async fn hacer(&self); }` no puede usarse como `Box<dyn MiTrait>` porque el compilador necesita conocer el tamaño del Future retornado. Fix: usar la crate `async-trait` con `#[async_trait]`, o en Rust 1.75+ usar `impl Trait` en la firma del trait si el objeto dinámico no es necesario.

**`sqlx::query!` macro falla en CI si `DATABASE_URL` no está disponible en compile time**: la macro `query!` verifica el SQL contra la BD en tiempo de compilación — si el CI no tiene acceso a la BD, el build falla. Fix: usar `DATABASE_URL` en `.env` para desarrollo local, y configurar en CI usando `sqlx::query_as!` con archivos de snapshot (`sqlx prepare`) o deshabilitar la verificación en línea con `SQLX_OFFLINE=true` y committear `.sqlx/`.

**`Arc<Mutex<T>>` con `.lock().unwrap()` en código async causa deadlock con Tokio**: si el MutexGuard cruza un await point, el guard se mantiene bloqueado durante toda la operación async. Fix: usar `tokio::sync::Mutex` en lugar de `std::sync::Mutex` en código async — `tokio::sync::Mutex` puede ser mantenido a través de await points sin deadlock.

**`#[derive(Serialize)]` con campos `Option<Vec<T>>` serializa como `null` en lugar de omitir el campo**: `serde` serializa `None` como `null` por defecto — si el receptor espera que el campo esté ausente (no presente como `null`), la deserialización puede fallar. Fix: usar `#[serde(skip_serializing_if = "Option::is_none")]` en el campo para omitirlo cuando es `None`.

*Skill creado con swl:crear-skill el 2026-03-31. Versión 1.1.0 — rosetta stones Python/C# y wiki integration añadidos el 2026-04-09.*
