← Índice

6.1 Dos crates, una caja de herramientas

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 donde las herramientas están hechas para una tarea, todo está junto. Es cómodo - mientras la tarea es una. Cuando llega una segunda, comienza la conversación sobre qué pertenece a quién y quién está alcanzando qué. Esa conversación nunca es corta y siempre termina con un reordenamiento.

Lo que necesitas

Todo tq es un único crate: tareas, el almacén, configuración, manejo de errores, e interfaz de comandos - juntos. Mientras es un solo programa, está bien.

Pero en el momento en que tq también deba gestionar tareas a través de la web, se necesita una segunda solución. Un servidor web es un binario separado; no necesita las capacidades de línea de comandos. Pero la lógica es la misma: las mismas tareas, el mismo almacén, la misma config. Dos crates binarios no pueden compartir código directamente - cada uno se compila en un artefacto separado.

La solución es un workspace de Cargo. El código necesario por ambos se mueve a un crate de biblioteca, tq-core. La CLI se queda en tq-cli, dependiendo ahora de tq-core. La lógica no cambiará, los tests no cambiarán. cargo run -p tq-cli hará exactamente lo que antes hacía cargo run.

La construcción

La estructura final del proyecto:

.
├── Cargo.toml
├── Makefile
└── crates/
    ├── core/
    │   ├── Cargo.toml
    │   └── src/
    │       ├── lib.rs
    │       ├── config/
    │       ├── error/
    │       ├── persistence/
    │       ├── store/
    │       └── task/
    └── cli/
        ├── Cargo.toml
        └── src/
            ├── main.rs
            └── cli/

El Cargo.toml raíz se convierte en un manifiesto de workspace, no de paquete. Todo lo que necesita:

# Cargo.toml - CAMBIADO
[workspace]
members = ["crates/core", "crates/cli"]
resolver = "2"

members - la lista de crates. Cargo encontrará sus archivos Cargo.toml por sí solo. resolver = "2" - la versión del algoritmo de resolución de dependencias; para workspaces con edition 2021 y posteriores este es el valor estándar.

Ahora crea los directorios y mueve los archivos. En Linux o macOS estos comandos pueden ejecutarse desde la raíz del proyecto:

# crear directorios de crates
$ mkdir -p crates/core/src crates/cli/src

# mover módulos a core
$ mv src/task src/store src/error src/persistence src/config crates/core/src/

# mover main.rs y cli/ a tq-cli
$ mv src/main.rs crates/cli/src/
$ mv src/cli crates/cli/src/

# eliminar el directorio ahora vacío
$ rmdir src

Los directorios están en su lugar. Ahora añade un manifiesto a cada crate.

crates/core/Cargo.toml describe el crate de biblioteca. clap no es necesario aquí - no hay interfaz de comandos en core:

# crates/core/Cargo.toml - NUEVO
[package]
name = "tq-core"
version = "0.1.0"
edition = "2024"

[dependencies]
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "1.1"

crates/cli/Cargo.toml depende de tq-core - no a través de crates.io, sino directamente por ruta:

# crates/cli/Cargo.toml - NUEVO
[package]
name = "tq-cli"
version = "0.1.0"
edition = "2024"

[[bin]]
name = "tq"
path = "src/main.rs"

[dependencies]
clap = { version = "4", features = ["derive"] }
tq-core = { path = "../core" }

Los módulos task/, store/, error/, persistence/, config/ se movieron a crates/core/src/ sin cambios - el código en ellos es el mismo, solo la ubicación es nueva. Pero todavía falta un archivo: lib.rs. Sin él Cargo no sabe que crates/core es un crate de biblioteca y se negará a compilar el workspace. Créalo ahora:

// crates/core/src/lib.rs - NUEVO
pub mod config;
pub mod error;
pub mod persistence;
pub mod store;
pub mod task;

Sin pub los módulos permanecerían privados - tq-cli no los vería.

En crates/cli/src/main.rs las cinco declaraciones mod se eliminan: esos módulos ya no están en este crate. En su lugar - importaciones desde tq_core:

// crates/cli/src/main.rs - CAMBIADO
mod cli;

use clap::Parser;
use cli::Cli;
use std::path::Path;
use tq_core::config::Config;
use tq_core::error::TqResult;

fn main() {
    if let Err(e) = run() {
        eprintln!("tq: {e}");
        std::process::exit(1);
    }
}

fn run() -> TqResult<()> {
    let cli = Cli::parse();
    let config = Config::load(Path::new("config.toml"), cli.data_dir.clone())?;
    cli::run(cli, config)
}

mod cli; permanece - cli/mod.rs vive en este crate.

En crates/cli/src/cli/mod.rs solo cambian las líneas use. crate:: se refería al crate actual; los tipos necesarios están ahora en otro:

// crates/cli/src/cli/mod.rs - CAMBIADO (solo líneas use)
use tq_core::config::Config;             // antes: use crate::config::Config
use tq_core::error::TqResult;           // antes: use crate::error::TqResult
use tq_core::persistence;               // antes: use crate::persistence
use tq_core::store::{Store, TaskStore}; // antes: use crate::store::{Store, TaskStore}
use tq_core::task::Task;                // antes: use crate::task::Task

Todo lo demás en cli/mod.rs - el struct Cli, el enum Commands, la función run - queda intacto.

El resultado

$ mkdir -p /tmp/tq-m61
$ cargo run -p tq-cli -- --data-dir /tmp/tq-m61 add "Fix the latch"
#1: Fix the latch [Todo] (2026-06-06)

-p tq-cli le dice a Cargo qué binario ejecutar. En un workspace sin este flag, cargo run no sabe qué crate se quiere.

make ci pasa. Los tests - los mismos 38, ahora distribuidos en dos crates: 33 en tq-core, 5 en tq-cli.

El código completo de tq para este capítulo está en 6-the-port/01-two-crates-one-toolbox/.


Lore: El crate de biblioteca

Un crate viene en dos tipos. Un crate binario ([[bin]] en Cargo.toml) produce un ejecutable - tiene un main. Un crate de biblioteca ([lib]) es un artefacto que otros crates importan; no tiene main, y su punto de entrada es lib.rs.

tq-core es una biblioteca: no ejecuta nada por sí mismo. tq-cli depende de él - y ejecuta.


Lore: Workspace

Un workspace es varios crates bajo un Cargo.toml raíz. El archivo raíz contiene solo [workspace]; no tiene paquete propio.

Los comandos sin -p cubren todo el workspace: cargo build compila todos los crates, cargo test ejecuta los tests en todos lados. Con -p <nombre> - solo el crate nombrado.

Las dependencias dentro de un workspace se declaran con path:

tq-core = { path = "../core" }

Cargo toma el crate del disco, no del registro.