4.3 El regreso
Esta traducción fue asistida por IA y revisada superficialmente. Si encuentras errores o algo no suena natural, escríbeme — lo agradezco.
Escribir es la mitad del trabajo. La segunda mitad se llama leer. Un artesano que sabe registrar piezas en un libro de cuentas pero no sabe recuperarlas de él lleva un inventario muy disciplinado de lo que ha perdido para siempre. El rastro existe. Todavía no hay un camino de vuelta por él.
Lo que necesitas
save escribe las tareas en un archivo. load debe recuperarlas. Sin ello, cada ejecución
comienza con un almacén vacío - sea lo que sea lo que haya en tasks.json.
La primera ejecución es un caso especial: el archivo todavía no existe. La ausencia de un
archivo no es un error - es simplemente un comienzo. load debe tenerlo en cuenta.
La construcción
load es el espejo de save. En src/persistence/mod.rs:
// src/persistence/mod.rs - CAMBIADO
pub fn load(path: &Path) -> TqResult<Vec<Task>> {
if !path.exists() {
return Ok(Vec::new());
}
let content = fs::read_to_string(path)
.map_err(|e| TqError::Parse(e.to_string()))?;
let tasks: Vec<Task> = serde_json::from_str(&content)
.map_err(|e| TqError::Parse(e.to_string()))?;
Ok(tasks)
}
Cargar tareas al almacén a través del habitual add no funcionará - add asigna a cada
tarea un nuevo identificador, sobreescribiendo el que se guardó. new recibe una nueva
firma: ahora acepta una lista de tareas ya construida. En src/store/mod.rs:
// src/store/mod.rs - CAMBIADO
impl TaskStore {
pub fn new(tasks: Vec<Task>) -> Self { // CAMBIADO: acepta tareas directamente
let next_id = tasks.iter().map(|t| t.id).max().unwrap_or(0) + 1;
TaskStore { items: tasks, next_id }
}
}
unwrap_or(0) + 1 da next_id = 1 para una lista vacía - en la primera ejecución el
almacén empieza desde uno. Para tareas cargadas se desplaza por encima del máximo: los
nuevos identificadores no colisionarán con los antiguos.
En src/main.rs - cargar al inicio, guardar al salir:
// src/main.rs - CAMBIADO
fn main() -> TqResult<()> {
let path = Path::new("tasks.json");
let tasks = persistence::load(path)?; // NUEVO
let mut store = TaskStore::new(tasks); // CAMBIADO: new(tasks)
println!("Loaded: {} task(s)", store.all().len()); // NUEVO
store.add("Buy coffee")?;
store.add("Buy milk")?;
store.add("Buy eggs")?;
for task in store.all() {
println!("{}", task);
}
match store.get(1) {
Ok(task) => println!("found: {}", task),
Err(e) => println!("{}", e),
}
match store.get(99) {
Ok(task) => println!("{}", task),
Err(e) => println!("{}", e),
}
store.get_mut(1)?.complete();
println!("done: {}", store.get(1)?.is_done());
match Task::new(0, "") {
Ok(task) => store.add(task)?,
Err(e) => println!("rejected: {}", e),
}
persistence::save(store.all(), path)?; // CAMBIADO: path en lugar de Path::new(...)
Ok(())
}
El resultado
Para empezar limpio, elimina el tasks.json creado antes:
rm tasks.json
La primera ejecución produce:
Loaded: 0 task(s)
#1: Buy coffee [Todo]
#2: Buy milk [Todo]
#3: Buy eggs [Todo]
found: #1: Buy coffee [Todo]
task 99 not found
done: true
rejected: task title cannot be empty
La segunda ejecución:
Loaded: 3 task(s)
#1: Buy coffee [Done]
#2: Buy milk [Todo]
#3: Buy eggs [Todo]
#4: Buy coffee [Todo]
#5: Buy milk [Todo]
#6: Buy eggs [Todo]
found: #1: Buy coffee [Done]
task 99 not found
done: true
rejected: task title cannot be empty
Las tareas de la primera ejecución están vivas. El proceso terminó - ellas permanecieron.
El test clave es el círculo completo: guardar y cargar de vuelta. En src/persistence/tests.rs:
// src/persistence/tests.rs - CAMBIADO
#[test]
fn save_and_load_roundtrip() {
let path = std::env::temp_dir().join("tq_test_m43_roundtrip.json");
let tasks = vec![Task::new(1, "Buy coffee").unwrap()];
save(&tasks, &path).unwrap();
let loaded = load(&path).unwrap();
assert_eq!(loaded.len(), 1);
assert_eq!(loaded[0].title, "Buy coffee");
let _ = fs::remove_file(&path);
}
Los tests para un archivo ausente y una lista vacía están en tq/.
make ci pasa. Las tareas sobreviven a un reinicio.
El código completo de
tqpara este capítulo está en4-memory/03-the-return/.
Lore: path.exists() y una condición de carrera
path.exists() es una comprobación tras la cual el archivo podría teóricamente eliminarse
antes de fs::read_to_string. En la práctica esto no es un problema: tq es
monohilo y solo él modifica el archivo. Pero hay otro enfoque - abrir el archivo y manejar
su ausencia explícitamente:
use std::io::ErrorKind;
pub fn load(path: &Path) -> TqResult<Vec<Task>> {
match fs::read_to_string(path) {
Ok(content) => serde_json::from_str(&content)
.map_err(|e| TqError::Parse(e.to_string())),
Err(e) if e.kind() == ErrorKind::NotFound => Ok(Vec::new()),
Err(e) => Err(TqError::Parse(e.to_string())),
}
}
e.kind() == ErrorKind::NotFound es un filtro directamente en el brazo del match. Un
archivo ausente va a Ok(Vec::new()); cualquier otro error de E/S va a Err. En tq,
ambos enfoques son equivalentes.