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
tqpara este capítulo está en6-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.