4.5 Leer sus propias instrucciones
Esta traducción fue asistida por IA y revisada superficialmente. Si encuentras errores o algo no suena natural, escríbeme — lo agradezco.
En un taller bien organizado hay una hoja en el banco con los ajustes: dónde guardar el trabajo, qué modo usar, qué comprobar primero. El artesano no se la lleva cuando se va - la hoja es para la herramienta, no para el artesano. La herramienta también funciona sin ella. Hace todo como siempre lo ha hecho, independientemente de lo que haya cambiado fuera. Esto suele llamarse fiabilidad. También tiene otro nombre.
Lo que necesitas
La ruta al archivo de tareas está actualmente codificada en duro. Eso funciona - hasta que
las tareas necesiten almacenarse en otro lugar. Se necesita un struct Config para guardar
los ajustes, y un archivo config.toml del que leerlos. Si el archivo está ausente, se
aplican los valores por defecto.
La construcción
toml es un crate para leer archivos en el formato del mismo nombre. cargo add toml lo
añade a las dependencias. En Cargo.toml:
# Cargo.toml - CAMBIADO
[dependencies]
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "1.1"
Config es un módulo nuevo. En src/config/mod.rs:
// src/config/mod.rs - NUEVO
use crate::error::{TqError, TqResult};
use serde::Deserialize;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Deserialize)]
pub struct Config {
pub data_dir: PathBuf,
}
impl Default for Config {
fn default() -> Self {
Config {
data_dir: PathBuf::from("."),
}
}
}
impl Config {
pub fn load(path: &Path) -> TqResult<Config> {
let config = if !path.exists() {
Config::default()
} else {
let content = fs::read_to_string(path).map_err(|e| TqError::Parse(e.to_string()))?;
toml::from_str(&content).map_err(|e| TqError::Parse(e.to_string()))?
};
if !config.data_dir.exists() {
return Err(TqError::Parse(format!(
"data_dir '{}' does not exist; create it with: mkdir -p {}",
config.data_dir.display(),
config.data_dir.display()
)));
}
Ok(config)
}
}
#[derive(Deserialize)] - el mismo mecanismo que para Task, pero sin Serialize: la
configuración se lee, no se escribe. toml::from_str lee el contenido del archivo y rellena
el struct. Si el archivo está ausente, Config::default() proporciona los valores por
defecto.
impl Default es un trait estándar de Rust para valores por defecto. Permite escribir
Config::default() donde sea que se necesite una config “de la nada”, y declara
explícitamente qué significa eso - no a través de constantes mágicas dispersas por el código.
El propio archivo config.toml vive en la raíz del proyecto, junto a Cargo.toml. cargo run también se lanza desde ahí, así que Path::new("config.toml") lo encuentra
correctamente. Una línea:
data_dir = "."
En src/main.rs la ruta ahora viene de la config. Añade mod config y use config::Config,
y reemplaza las dos líneas de apertura de main:
// src/main.rs - CAMBIADO
fn main() -> TqResult<()> {
let config = Config::load(Path::new("config.toml"))?;
let path = config.data_dir.join("tasks.json"); // CAMBIADO
let tasks = persistence::load(&path)?;
// ...
persistence::save(store.all(), &path)?;
Una línea con la ruta codificada en duro se convirtió en dos: cargar la config, construir la
ruta a partir de ella. Antes path era un &Path - Path::new devuelve una referencia
directamente. Ahora join devuelve un nuevo PathBuf, no una referencia - por eso aparece
&path en las llamadas. El resto de main no cambia.
El resultado
Sin config.toml - el comportamiento no cambia; tq busca tasks.json en el directorio
actual. Con el archivo:
data_dir = "/home/user/.tq"
las tareas se almacenan ahí - siempre que el directorio exista. Si no:
Error: Parse("data_dir '/home/user/.tq' does not exist; create it with: mkdir -p /home/user/.tq")
load comprueba data_dir inmediatamente después de analizar la config - el mismo patrón
if !path.exists() ya presente en persistence::load. El error no solo informa del
problema; dice qué hacer. El código no cambia. Solo la hoja en el banco de trabajo.
Cuatro tests cubren todos los caminos. En src/config/tests.rs:
// src/config/tests.rs - NUEVO
use super::*;
#[test]
fn config_defaults_to_current_dir() {
let config = Config::default();
assert_eq!(config.data_dir, PathBuf::from("."));
}
#[test]
fn config_load_missing_file_returns_default() {
let path = std::env::temp_dir().join("tq_test_m45_no_config.toml");
let _ = std::fs::remove_file(&path);
let config = Config::load(&path).unwrap();
assert_eq!(config.data_dir, PathBuf::from("."));
}
#[test]
fn config_load_parses_data_dir() {
let path = std::env::temp_dir().join("tq_test_m45_config.toml");
std::fs::write(&path, r#"data_dir = "/tmp""#).unwrap();
let config = Config::load(&path).unwrap();
assert_eq!(config.data_dir, PathBuf::from("/tmp"));
let _ = std::fs::remove_file(&path);
}
#[test]
fn config_load_rejects_missing_data_dir() {
let config_path = std::env::temp_dir().join("tq_test_m45_bad_config.toml");
let missing = "/tmp/tq_nonexistent_xyz";
let _ = std::fs::remove_dir_all(missing);
std::fs::write(&config_path, format!(r#"data_dir = "{}""#, missing)).unwrap();
let result = Config::load(&config_path);
assert!(result.is_err());
let _ = std::fs::remove_file(&config_path);
}
make ci pasa.
El código completo de
tqpara este capítulo está en4-memory/05-reading-its-own-instructions/.
Lore: use - un alias, no un candado
En el capítulo 4.1 la línea use serde::{Deserialize, Serialize} apareció en el código sin
explicación, como si no hubiera otra forma. En realidad use no concede acceso a un crate:
cualquier crate en Cargo.toml es accesible por su ruta completa en cualquier parte del
código sin un solo use.
use es un alias. Permite escribir un nombre corto en lugar de la ruta completa. En el
capítulo 4.1 el alias era necesario: #[derive(Deserialize)] es un nombre corto en un
atributo - sin use, habría que escribir #[derive(serde::Deserialize)]. En este capítulo
toml::from_str es la ruta completa, usada una vez, y la notación es simple - un alias
sería redundante.
La misma lógica se aplica a use std::fmt y use std::path::Path - conveniencia, no
obligación.
Lore: TOML como formato de configuración
JSON es conveniente para intercambiar datos entre programas. Para archivos que un humano
edita, tiene una propiedad incómoda: los comentarios están prohibidos. Escribir # ruta a los datos junto a un valor es un error de sintaxis.
TOML fue diseñado específicamente para archivos de configuración. Los comentarios son nativos:
# Dónde se almacenan las tareas
data_dir = "."
Jerarquía a través de secciones, fechas como tipo de primera clase, cadenas multilínea. Una persona abre el archivo y lo entiende sin documentación. Por eso la mayoría de herramientas en el ecosistema de Rust - el propio Cargo incluido - usan TOML para la configuración.