4.2 El primer rastro
Esta traducción fue asistida por IA y revisada superficialmente. Si encuentras errores o algo no suena natural, escríbeme — lo agradezco.
La gente descubrió bastante rápido que la memoria es poco fiable. Ese descubrimiento no detuvo a nadie de seguir confiando en ella - pero sí produjo la escritura, que es de por sí un resultado notable. La diferencia entre una palabra hablada y una escrita es simple: una desaparece, la otra permanece. A veces demasiado tiempo. El artefacto puede traducir tareas a texto. El texto vive en la memoria del proceso y termina ahí. Eso no es un registro. Es un borrador que nadie lee.
El intento
La tarea es clara: tomar la lista de tareas, convertirlas en JSON, escribir en un archivo.
Crea src/persistence/mod.rs y declara el módulo en src/main.rs con mod persistence;.
La lógica de save parece directa:
// src/persistence/mod.rs
use crate::error::TqResult;
use crate::task::Task;
use std::fs;
use std::path::Path;
pub fn save(tasks: &[Task], path: &Path) -> TqResult<()> {
let json = serde_json::to_string_pretty(tasks)?;
fs::write(path, json)?;
Ok(())
}
cargo build devuelve dos errores:
error[E0277]: `?` couldn't convert the error to `TqError`
--> src/persistence/mod.rs:XX:XX
|
XX | let json = serde_json::to_string_pretty(tasks)?;
| ^ the trait `From<serde_json::Error>`
| is not implemented for `TqError`
error[E0277]: `?` couldn't convert the error to `TqError`
--> src/persistence/mod.rs:XX:XX
|
XX | fs::write(path, json)?;
| ^ the trait `From<std::io::Error>`
| is not implemented for `TqError`
Lo que sabe el compilador
El operador ? intenta convertir el error al tipo que la función espera - es decir,
TqError. Eso requiere una implementación de From<E> for TqError, donde E es el tipo
de error interior. Ni serde_json::Error ni io::Error la tienen. Ambos operadores ?
fallan por la misma razón.
Añadir From no es obligatorio. Basta con hacer la conversión explícita antes de que ?
tenga la oportunidad de exigirla.
El conjuro
Primero - src/error/mod.rs. Se necesita una nueva variante:
// src/error/mod.rs - CAMBIADO
use std::fmt;
#[derive(Debug)]
pub enum TqError {
EmptyTitle,
NotFound(u64),
Parse(String), // NUEVO
}
pub type TqResult<T> = Result<T, TqError>;
impl fmt::Display for TqError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TqError::EmptyTitle => write!(f, "task title cannot be empty"),
TqError::NotFound(id) => write!(f, "task {} not found", id),
TqError::Parse(msg) => write!(f, "parse error: {}", msg), // NUEVO
}
}
}
La nueva variante existe - ahora los errores tienen adónde ir. En src/persistence/mod.rs,
ambas líneas convierten el error en una cadena y lo colocan en TqError::Parse:
// src/persistence/mod.rs - CAMBIADO
use crate::error::{TqError, TqResult};
use crate::task::Task;
use std::fs;
use std::path::Path;
pub fn save(tasks: &[Task], path: &Path) -> TqResult<()> {
let json = serde_json::to_string_pretty(tasks).map_err(|e| TqError::Parse(e.to_string()))?;
fs::write(path, json).map_err(|e| TqError::Parse(e.to_string()))?;
Ok(())
}
#[cfg(test)]
mod tests;
.map_err toma un closure y lo aplica al error si existe. El ? que viene después se
comporta como de costumbre - solo que ahora el tipo es TqError, y no se necesita From.
En src/main.rs - añade use std::path::Path; y la llamada a save antes de Ok(()):
// src/main.rs - añadir antes de Ok(())
persistence::save(store.all(), Path::new("tasks.json"))?;
cargo run termina sin errores. Aparece un archivo tasks.json junto al binario:
[
{
"id": 1,
"title": "Buy coffee",
"status": "Done"
},
{
"id": 2,
"title": "Buy milk",
"status": "Todo"
},
{
"id": 3,
"title": "Buy eggs",
"status": "Todo"
}
]
Dos tests en src/persistence/tests.rs - uno comprueba el contenido del archivo, el otro
que una lista vacía se escribe correctamente:
// src/persistence/tests.rs - NUEVO
use super::*;
use crate::task::Task;
use std::fs;
#[test]
fn save_writes_tasks_as_json() {
let path = std::env::temp_dir().join("tq_test_m42_save.json");
let tasks = vec![Task::new(1, "Buy coffee").unwrap()];
save(&tasks, &path).unwrap();
let content = fs::read_to_string(&path).unwrap();
assert!(content.contains(r#""title": "Buy coffee""#));
assert!(content.contains(r#""status": "Todo""#));
let _ = fs::remove_file(&path);
}
#[test]
fn save_writes_empty_array_for_no_tasks() {
let path = std::env::temp_dir().join("tq_test_m42_empty.json");
save(&[], &path).unwrap();
let content = fs::read_to_string(&path).unwrap();
assert_eq!(content.trim(), "[]");
let _ = fs::remove_file(&path);
}
make ci pasa. tq deja ahora un primer rastro.
El código completo de
tqpara este capítulo está en4-memory/02-the-first-trace/.
Lore: Cómo funciona .map_err
serde_json::to_string_pretty(tasks)
.map_err(|e| TqError::Parse(e.to_string()))?
|e| es el argumento del closure: el error devuelto por serde_json. Los valores Ok
pasan sin cambios. La expansión manual tiene este aspecto:
match serde_json::to_string_pretty(tasks) {
Ok(v) => v,
Err(e) => return Err(TqError::Parse(e.to_string())),
}
.map_err no cambia lo que ocurre - solo hace la notación más corta. Los closures
aparecerán con frecuencia; la capacidad de expandirlos a mano es un reflejo útil.