← Índice

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 tq para este capítulo está en 4-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.