Documentar: rustdoc, /// y docs.rs
rustdoc convierte los comentarios /// en una web navegable, con secciones convencionales (# Examples, # Panics, # Errors, # Safety), enlaces intra-doc que el compilador verifica y ejemplos que son doc-tests. Y docs.rs construye y aloja esa documentación de cada versión publicada, sin que muevas un dedo. La documentación como artefacto de primera clase.
En la mayoría de los lenguajes documentar es un acto de fe y de disciplina: escribes prosa en algún formato, la generas con una herramienta que instalas aparte, la subes a un sitio que mantienes tú, y rezas para que no envejezca hasta mentir. Rust colapsa toda esa cadena en el propio flujo del lenguaje. Los comentarios /// no son texto muerto: rustdoc los compila en una web navegable con búsqueda, enlaces cruzados y tipos resaltados; sus ejemplos se ejecutan como pruebas —lo viste en el nivel de testing—; y docs.rs, un servicio neutral del ecosistema, construye y aloja esa documentación para cada versión publicada de cada crate, automáticamente, en cuanto haces cargo publish. El resultado es una norma cultural que casi ningún ecosistema logra: que toda librería tenga documentación hospedada, versionada e interconectada, generada desde su fuente real.
- Escribir documentación con
///y//!, usando Markdown y las secciones convencionales que el ecosistema espera. - Enlazar tipos y métodos con enlaces intra-doc que el compilador resuelve y verifica.
- Gobernar
rustdoccon atributos:#![warn(missing_docs)],#[doc(hidden)],cfgde documentación. - Entender cómo
docs.rsconstruye la doc de cada versión y cómo configurarla desdeCargo.toml.
Los comentarios que son documentación
Rust distingue dos comentarios de documentación, y la diferencia es de dirección. /// documenta el elemento que le sigue; //! documenta el elemento que lo contiene, desde dentro —ideal para la portada de un módulo o del crate entero, en la cabecera de lib.rs—. El cuerpo es Markdown, y una convención de secciones tan asentada que funciona como vocabulario compartido:
//! # mi_crate
//!
//! Un parser tolerante a errores. Empieza por [`Parser::new`].
/// Analiza una cadena de configuracion.
///
/// # Examples
///
/// ```
/// # use mi_crate::Parser;
/// let cfg = Parser::new().parse("clave = 1")?;
/// assert_eq!(cfg.get("clave"), Some(&1));
/// # Ok::<(), mi_crate::Error>(())
/// ```
///
/// # Errors
///
/// Devuelve [`Error::Sintaxis`] si la entrada no es TOML valido.
///
/// # Panics
///
/// Entra en panico si se invoca antes de [`Parser::init`].
pub fn parse(&self, texto: &str) -> Result<Config, Error> { /* ... */ }
Esas cabeceras —# Examples, # Errors, # Panics, y para funciones unsafe, # Safety— no son adorno tipográfico. Son un contrato que todo Rustacean sabe leer: un lector experimentado busca directamente # Panics para saber qué hace estallar la función y # Safety para conocer las invariantes que debe garantizar. Y como aprendiste en la lección de doc-tests, el bloque bajo # Examples se compila y ejecuta: tu documentación no puede mentir sobre cómo se usa la API, porque el compilador la verifica en cada cargo test.
Enlaces intra-doc: la web que el compilador teje
La navegabilidad de la doc de Rust nace de una función que parece magia y es rigor: los enlaces intra-doc. Escribes el nombre de un tipo o método entre corchetes y rustdoc lo resuelve a su página, verificando que exista:
/// Convierte esto en un [`Config`], igual que [`Config::from_str`].
///
/// Si prefieres el camino perezoso, mira [`Parser::stream`] o
/// el modulo [`crate::io`]. Un enlace con texto: [la guia](crate::guia).
pub fn to_config(&self) -> Config { /* ... */ }
La clave: si renombras Config::from_str o lo eliminas, ese enlace rompe la compilación de la doc. No es un href estático que se pudre cuando el código evoluciona, sino una referencia resuelta contra el árbol real de símbolos, tan viva como una llamada a función. Combinado con los doc-tests, esto cierra el círculo: tanto lo que tu documentación afirma (los ejemplos) como lo que referencia (los enlaces) queda atado a la verdad del código.
/// hacia abajo
Documenta el elemento siguiente. El caballo de batalla: funciones, struct, métodos, variantes.
//! hacia dentro
Documenta el contenedor desde su interior. La portada del crate vive en la cabecera de lib.rs.
[`Tipo`] enlaza y verifica
Los enlaces intra-doc se resuelven contra los símbolos reales; si el destino desaparece, la doc no compila.
#[doc(hidden)]
Oculta de la doc pública un elemento que la API expone por necesidad técnica pero nadie debe usar.
Gobernar rustdoc con atributos
rustdoc se controla con atributos que van desde exigir documentación hasta esculpir qué se muestra. Los más útiles a la hora de mantener una librería seria:
#![warn(missing_docs)] // avisa de cada item publico sin documentar
#![doc(html_logo_url = "https://.../logo.svg")]
/// Detalle interno que la API expone por fuerza mayor, no para usarse.
#[doc(hidden)]
pub struct DetalleInterno;
/// Solo disponible con la feature `async`.
#[cfg(feature = "async")]
#[doc(cfg(feature = "async"))] // muestra en la doc que exige esa feature
pub async fn parse_stream() { /* ... */ }
#![warn(missing_docs)] en la raíz del crate convierte cada pub sin documentar en una advertencia: la forma canónica de garantizar cobertura. #[doc(hidden)] esconde de la web lo que debe ser público en el sentido técnico pero no forma parte del contrato —lo que a menudo generan las macros—. Y #[doc(cfg(...))] renderiza una etiqueta que avisa: «esto solo existe si activas tal feature», resolviendo la confusión de una API que cambia de forma según la configuración.
docs.rs: la documentación como servicio del ecosistema
Aquí está el multiplicador. No tienes que generar ni alojar nada: cuando publicas en crates.io, el servicio docs.rs clona tu fuente, ejecuta cargo doc en un entorno reproducible y publica el resultado en https://docs.rs/mi_crate, con una página por cada versión que hayas subido. Documentación hospedada, versionada, uniforme y construida por un tercero neutral, gratis y para siempre. Puedes ajustar cómo la construye desde el manifiesto:
[package.metadata.docs.rs]
all-features = true # documenta con todas las features
rustdoc-args = ["--cfg", "docsrs"] # activa el cfg para #[doc(cfg(...))]
targets = ["x86_64-unknown-linux-gnu"] # o varios objetivos
Localmente, cargo doc --open genera y abre exactamente la misma web para tu crate y todas sus dependencias. La coherencia es el punto: la doc de serde, la tuya y la de cualquier crate transitivo se leen con la misma interfaz, se enlazan entre sí y se navegan igual.
flowchart TD A[Escribes /// y //! en el codigo fuente] --> B[cargo doc genera la web local] A --> C[cargo test ejecuta los ejemplos como doc-tests] A --> D[cargo publish sube la fuente a crates.io] D --> E[docs.rs clona y ejecuta cargo doc] E --> F[Documentacion hospedada por version en docs.rs] B --> G[Enlaces intra-doc verificados contra los simbolos] style A fill:#cba6f7,color:#11111b style C fill:#a6e3a1,color:#11111b style F fill:#89b4fa,color:#11111b style G fill:#f9e2af,color:#11111b
Repara en el cambio de categoría que Rust opera sobre un acto que otros lenguajes tratan como marginal. En casi todas partes, la documentación es un texto paralelo al código: vive en otro fichero o en otra web, la sincroniza un humano por buena voluntad, y la entropía la degrada sin falta hasta que el ejemplo ya no compila y el enlace apunta a un método difunto. Rust rehúsa esa fe y hace tres cosas que, juntas, cambian la naturaleza del objeto. Primero, la ejecuta: los ejemplos son pruebas, así que la documentación no puede afirmar un uso falso sin que el compilador lo detenga. Segundo, la enlaza contra el árbol real de símbolos: una referencia rota no es un 404 que alguien descubrirá dentro de meses, sino un error de compilación que aparece hoy. Tercero, y esto es lo que trasciende al lenguaje para volverse cultura, la aloja un tercero neutral: docs.rs garantiza que toda librería publicada tenga documentación hospedada, versionada y con la misma interfaz, sin depender de que su autor monte un sitio o lo mantenga vivo. El efecto acumulado no es «mejor documentación»; es un ecosistema navegable, donde puedes saltar de tu crate a serde y de serde a std sin cambiar de herramienta ni de convención, leyendo secciones # Panics y # Safety que significan lo mismo en todas partes. Documentar deja de ser un impuesto que se paga al final y se abandona, y pasa a ser un artefacto de primera clase que se rompe con el código, se repara con él y se publica solo. La lección para el diseñador de sistemas es honda: la calidad que quieres que sea la norma no la consigues pidiéndola, la consigues volviéndola el camino de menor resistencia. Rust no exhorta a documentar; hace que no documentar sea lo raro.
/// documenta hacia abajo, //! hacia dentro; el cuerpo es Markdown con secciones convencionales # Examples, # Errors, # Panics, # Safety. Los enlaces intra-doc [Tipo] se verifican contra los símbolos reales. #![warn(missing_docs)] exige cobertura, #[doc(hidden)] oculta lo técnico-público, #[doc(cfg(...))] señala dependencias de feature. docs.rs construye y aloja la doc de cada versión publicada; se configura con [package.metadata.docs.rs]. cargo doc --open reproduce la misma web en local.
- Documenta un módulo con
//!en su cabecera y tres funcionespubcon///, usando las secciones# Examplesy# Errorsdonde corresponda. - Añade un enlace intra-doc
[OtroTipo], luego renombraOtroTipoy confirma quecargo docfalla hasta que arreglas el enlace. - Activa
#![warn(missing_docs)]en la raíz y observa cómo Cargo enumera cada elemento público sin documentar. - Marca una función con
#[doc(hidden)]y comprueba concargo doc --openque desaparece de la web pese a seguir siendopub. - Configura
[package.metadata.docs.rs]conall-features = truey razona qué problema resuelve para un crate con APIs tras features.