La documentación es una parte importante de cualquier proyecto de software y un ciudadano de primera clase en Rust. Hablemos acerca de las herramientas que Rust te proporciona para documentar tus proyectos.
rustdoc
La distribución de Rust incluye una herramienta, rustdoc
, encargada de generar la documentación. rustdoc
es también usada por Cargo a través de cargo doc
.
La documentación puede ser generada de dos formas: desde el código fuente, o desde archivos Markdown.
La principal forma de documentar un proyecto Rust es a través de la anotación del código fuente. Para este propósito, puedes usar comentarios de documentación:
fn main() { /// Construye un nuevo `Rc<T>`. /// /// # Examples /// /// ``` /// use std::rc::Rc; /// /// let cinco = Rc::new(5); /// ``` pub fn new(value: T) -> Rc<T> { // la implementación va aqui } }/// Construye un nuevo `Rc<T>`. /// /// # Examples /// /// ``` /// use std::rc::Rc; /// /// let cinco = Rc::new(5); /// ``` pub fn new(value: T) -> Rc<T> { // la implementación va aqui }
El código anterior genera documentación que luce como esta(ingles). He dejado la implementación por fuera, con un comentario regular en su lugar. Esa es la primera cosa a resaltar acerca de esta anotación: usa ///
, en vez de //
. El slash triple indica que es un comentario de documentación.
Los comentarios de documentación están escritos en formato Markdown.
Rust mantiene un registro de dichos comentarios, registro que usa al momento de generar la documentación. Esto es importante cuando se documentan cosas como enumeraciones (enums):
fn main() { /// El tipo `Option`. Vea [la documentación a nivel de modulo](../) para mas información. enum Option<T> { /// Ningún valor None /// Algún valor `T` Some(T), } }/// El tipo `Option`. Vea [la documentación a nivel de modulo](../) para mas información. enum Option<T> { /// Ningún valor None /// Algún valor `T` Some(T), }
Lo anterior funciona, pero esto, no:
fn main() { /// El tipo `Option`. Vea [la documentación a nivel de modulo](../) para mas información. enum Option<T> { None, /// Ningún valor Some(T), /// Algún valor `T` } }/// El tipo `Option`. Vea [la documentación a nivel de modulo](../) para mas información. enum Option<T> { None, /// Ningún valor Some(T), /// Algún valor `T` }
Obtendrás un error:
hola.rs:4:1: 4:2 error: expected ident, found `}`
hola.rs:4 }
^
Este desafortunado error es correcto: los comentarios de documentación aplican solo a lo que este después de ellos, y no hay nada después del ultimo comentario.
De cualquier modo, cubramos cada parte de este comentario en detalle:
fn main() { /// Construye un nuevo `Rc<T>`. fn foo() {} }
/// Construye un nuevo `Rc<T>`.
La primera linea de un comentario de documentación debe ser un resumen corto de sus funcionalidad. Una oración. Solo lo básico. De alto nivel.
fn main() { /// /// Otros detalles acerca de la construcción de `Rc<T>`s, quizás describiendo semántica /// complicada, tal vez opciones adicionales, cualquier cosa extra. /// fn foo() {} }/// /// Otros detalles acerca de la construcción de `Rc<T>`s, quizás describiendo semántica /// complicada, tal vez opciones adicionales, cualquier cosa extra. ///
Nuestro ejemplo original solo tenia una linea de resumen, pero si hubiésemos tenido mas cosas que decir, pudimos haber agregado mas explicación en un párrafo nuevo.
/// # Examples
A continuación están las secciones especiales. Estas son indicadas con una cabecera, #
. Hay tres tipos de cabecera que se usan comúnmente. Estos no son sintaxis especial, solo convención, por ahora.
/// # Panics
Malos e irrecuperables usos de una función (e.j. Errores de programación) en Rust son usualmente indicados por pánicos (panics), los cuales matan el hilo actual como mínimo. Si tu función posee un contrato no trivial como este, que es detectado/impuesto por pánicos, documentarlo es muy importante.
fn main() { /// # Failures fn foo() {} }
/// # Failures
Si tu función o método retorna un Result<T, E>
, entonces describir las condiciones bajo las cuales retorna Err(E)
es algo bueno por hacer. Esto es ligeramente menos importante que Panics
, a consecuencia de que es codificado en el sistema de tipos, pero es aun, algo que se recomienda hacer.
/// # Safety
Si tu función es unsafe
(insegura), deberías explicar cuales son las invariantes que deben ser mantenidas por el llamador.
/// # Examples /// /// ``` /// use std::rc::Rc; /// /// let cinco = Rc::new(5); /// ```
Tercero, Examples
, incluye uno o mas ejemplos del uso de tu función o método, y tus usuarios te querrán. Estos ejemplos van dentro de anotaciones de bloques de código, de los cuales hablaremos en un momento, pueden tener mas de una sección:
/// # Examples /// /// Patrones `&str` simples: /// /// ``` /// let v: Vec<&str> = "Mary tenia un corderito".split(' ').collect(); /// assert_eq!(v, vec!["Mary", "tenia", "un", "corderito"]); /// ``` /// /// Patrones mas complejos con lambdas: /// /// ``` /// let v: Vec<&str> = "abc1def2ghi".split(|c: char| c.is_numeric()).collect(); /// assert_eq!(v, vec!["abc", "def", "ghi"]); /// ```
Discutamos los detalles de esos bloques de código.
Para escribir alguna código Rust en un comentario, usa los graves triples:
fn main() { /// ``` /// println!("Hola, mundo"); /// ``` fn foo() {} }/// ``` /// println!("Hola, mundo"); /// ```
Si quieres código que no sea Rust, puedes agregar una anotación:
fn main() { /// ```c /// printf("Hola, mundo\n"); /// ``` fn foo() {} }/// ```c /// printf("Hola, mundo\n"); /// ```
La sintaxis de esta sección sera resaltada de acuerdo al lenguaje que estés mostrando. Si solo estas mostrando texto plano, usa text
.
Acá, es importante elegir la anotación correcta, debido a que rustdoc
la usa de una manera interesante: Puede ser usada para probar tus ejemplos, de tal manera que no se vuelvan obsoletos con el tiempo. Si tienes algún código C pero rustdoc
piensa que es Rust, es porque olvidaste la anotación, rustdoc
se quejara al momento de tratar de generar la documentación.
Discutamos nuestra documentación de ejemplo:
fn main() { /// ``` /// println!("Hola, mundo"); /// ``` fn foo() {} }/// ``` /// println!("Hola, mundo"); /// ```
Notaras que no necesitas una fn main()
o algo mas. rustdoc
agregara un main() automáticamente alrededor de tu código, y en el lugar correcto. Por ejemplo:
/// ``` /// use std::rc::Rc; /// /// let cinco = Rc::new(5); /// ```
Se convertirá en la prueba:
fn main() { use std::rc::Rc; let cinco = Rc::new(5); }fn main() { use std::rc::Rc; let cinco = Rc::new(5); }
He aquí el algoritmo completo que rustdoc
usa para post-procesar los ejemplos:
#![foo]
sobrante es dejado intacto como atributo del crate.unused_variables
, unused_assignments
, unused_mut
, unused_attributes
, y dead_code
. Ejemplos pequeños ocasionalmente disparan estos lints.extern crate
, entonces el extern crate <micrate>;
es insertado.fn main
, el texto es envuelto en fn main() { tu_codigo }
Algunas veces, todo esto no es suficiente. Por ejemplo, todos estos ejemplos de código con ///
de los que hemos estado hablando? El texto plano:
/// Algo de documentación.
# fn foo() {}
Luce diferente a la salida:
fn main() { /// Algo de documentación. fn foo() {} }
/// Algo de documentación.
Si, es correcto: puedes agregar lineas que comiencen con #
, y estas serán eliminadas de la salida, pero serán usadas en la compilación de tu código. Puedes usar esto como ventaja. En este caso, los comentarios de documentación necesitan aplicar a algún tipo de función, entonces si quiero mostrar solo un comentario de documentación, necesito agregar una pequeña definición de función debajo. Al mismo tiempo, esta allí solo para satisfacer al compilador, de manera tal que esconderla hace el ejemplo mas limpio. Puedes usar esta técnica para explicar ejemplos mas largos en detalle, preservando aun la capacidad de tu documentación para ser probada. Por ejemplo, este código:
let x = 5; let y = 6; println!("{}", x + y);
He aquí una explicación, renderizada:
Primero, asignamos a x
el valor de cinco:
let x = 5;
A continuación, asignamos seis a y
:
let y = 6;
Finalmente, imprimimos la suma de x
y y
:
println!("{}", x + y);
He aquí la misma explicación, en texto plano:
Primero, asignamos a
x
el valor de cinco:let x = 5; # let y = 6; # println!("{}", x + y);
A continuación, asignamos seis a
y
:# let x = 5; let y = 6; # println!("{}", x + y);
Finalmente, imprimimos la suma de
x
yy
:# let x = 5; # let y = 6; println!("{}", x + y);
Al repetir todas las partes del ejemplo, puedes asegurarte que tu ejemplo aun compila, mostrando solo las partes relevantes a tu explicación.
He aquí un ejemplo de la documentación a una macro:
/// Panic con un mensaje proporcionado a menos que la expression sea evaluada a true. /// /// # Examples /// /// ``` /// # #[macro_use] extern crate foo; /// # fn main() { /// panic_unless!(1 + 1 == 2, “Las mathematicas estan rotas.”); /// # } /// ``` /// /// ```should_panic /// # #[macro_use] extern crate foo; /// # fn main() { /// panic_unless!(true == false, “Yo estoy roto.”); /// # } /// ``` #[macro_export] macro_rules! panic_unless { ($condition:expr, $($rest:expr),+) => ({ if ! $condition { panic!($($rest),+); } }); } fn main() {}/// Panic con un mensaje proporcionado a menos que la expression sea evaluada a true. /// /// # Examples /// /// ``` /// # #[macro_use] extern crate foo; /// # fn main() { /// panic_unless!(1 + 1 == 2, “Las mathematicas estan rotas.”); /// # } /// ``` /// /// ```should_panic /// # #[macro_use] extern crate foo; /// # fn main() { /// panic_unless!(true == false, “Yo estoy roto.”); /// # } /// ``` #[macro_export] macro_rules! panic_unless { ($condition:expr, $($rest:expr),+) => ({ if ! $condition { panic!($($rest),+); } }); }
Notaras tres cosas: necesitamos agregar nuestro propia linea extern crate
, de tal manera que podamos agregar el atributo #[macro_use]
. Segundo, necesitaremos agregar nuestra propia main()
. Finalmente, un uso juicioso de #
para comentar esas dos cosas, de manera que nos se muestren en la salida.
Para correr las pruebas puedes:
$ rustdoc --test ruta/a/mi/crate/root.rs
# ó
$ cargo test
Correcto, cargo test
prueba la documentación embebida también. Sin embargo, cargo test
, no probara crates binarios, solo bibliotecas. Esto debido a la forma en la que rustdoc
funciona: enlaza con la biblioteca a ser probada, pero en el caso de un binario, no hay nada a lo cual enlazar.
Hay unas pocas anotaciones mas que son útiles para ayudar a rustdoc
a hacer la cosa correcta cuando pruebas tu código:
/// ```ignore /// fn foo() { /// ```
La directiva ignore
le dice a Rust que ignore el codigo. Esta es la forma que casi nunca querrás, pues es la mas genérica. En su lugar, considera el anotar con text
de no ser codigo, o usar #
s para obtener un ejemplo funcional que solo muestra la parte que te interesa.
/// ```should_panic /// assert!(false); /// ```
should_panic
le dice a rustdoc
que el código debe compilar correctamente, pero sin la necesidad de pasar una prueba de manera satisfactoria.
/// ```no_run /// loop { /// println!("Hola, mundo"); /// } /// ```
El atributo no_run
compilara tu código, pero no lo ejecutara. Esto es importante para ejemplos como "He aquí como iniciar un servicio de red," el cual debes asegurarte que compile, pero podría causar un ciclo infinito!
Rust posee otro tipo de comentario de documentación, //!
. Este comentario no documenta el siguiente item, este comenta el item que lo encierra. En otras palabras:
mod foo { //! Esta es documentation para el modulo `foo`. //! //! # Examples // ... }
Es aquí en donde veras //!
usado mas a menudo: para documentación de módulos. Si tienes un modulo en foo.rs
, frecuentemente al a abrir su código veras esto:
//! Un modulo para usar `foo`s. //! //! El modulo `foo` contiene un monton de funcionalidad bla bla bla
Echa un vistazo a el RFC 505 para un listado completo de convenciones acerca del estilo y formato de la documentación (ingles)
Todo este comportamiento funciona en archivos no Rust también. Debido a que los comentarios son escritos en Markdown, frecuentemente son archivos .md
.
Cuando escribes documentación en archivos Markdown, no necesitas prefijar la documentación con comentarios. Por ejemplo:
fn main() { /// # Examples /// /// ``` /// use std::rc::Rc; /// /// let cinco = Rc::new(5); /// ``` fn foo() {} }/// # Examples /// /// ``` /// use std::rc::Rc; /// /// let cinco = Rc::new(5); /// ```
es solo
# Examples
```
use std::rc::Rc;
let cinco = Rc::new(5);
```
cuando esta en un archivo Markdown. Solo hay un detalle, los archivos markdown necesitan tener un titulo como este:
% Titulo
Esta es la documentación de ejemplo
Esta linea %
deber estar ubicada en la primera linea del archivo.
doc
A un nivel mas profundo, los comentarios de documentación son otra forma de escribir atributos de documentación:
fn main() { /// this fn foo() {} #[doc="this"] fn bar() {} }/// this #[doc="this"]
son lo mismo que estos:
fn main() { //! this #![doc="/// this"] }//! this #![doc="/// this"]
No veras frecuentemente este atributo siendo usado para escribir documentación, pero puede ser util cuando se esten cambiando ciertas opciones, o escribiendo una macro.
rustdoc
mostrara la documentación para un re-export publico en ambos lugares:
extern crate foo; pub use foo::bar;
Lo anterior creara documentación para bar dentro de la documentación para el crate foo
, así como la documentación para tu crate. Sera la misma documentación en ambos lugares.
Este comportamiento puede ser suprimido con no_inline
:
extern crate foo; #[doc(no_inline)] pub use foo::bar;
Puedes controlar algunos aspectos de el HTML que rustdoc
genera a través de la versión #![doc]
del atributo:
#![doc(html_logo_url = "http://www.rust-lang.org/logos/rust-logo-128x128-blk-v2.png",
html_favicon_url = "http://www.rust-lang.org/favicon.ico",
html_root_url = "http://doc.rust-lang.org/")]
Esto configura unas pocas opciones, con un logo, favicon, y URL raíz.
rustdoc
también contiene unas pocas opciones en la linea de comandos, para mas personalización:
--html-in-header FILE
: incluye el contenido de FILE al final de la sección <head>...</head>
.--html-before-content FILE
: incluye el contenido de FILE después de <body>
, antes del contenido renderizado (incluyendo la barra de búsqueda).--html-after-content FILE
: incluye el contenido de FILE después de todo el contenido renderizado.El Markdown en los comentarios de documentación es puesto sin procesar en la pagina final. Se cuidadoso con HTML literal:
fn main() { /// <script>alert(document.cookie)</script> fn foo() {} }
/// <script>alert(document.cookie)</script>