Documentação Rust: rustdoc e Doc Tests | Rust Brasil

Guia completo de documentação em Rust: rustdoc, doc comments, doc tests, exemplos inline e publicação no docs.rs.

Introdução

Documentação em Rust não é um luxo, é uma funcionalidade de primeira classe da linguagem. O sistema de documentação do Rust, rustdoc, é integrado diretamente ao compilador e ao Cargo, permitindo que seus exemplos de código sejam compilados e testados automaticamente. Isso significa que sua documentação nunca fica desatualizada — se o código no exemplo não compila, o teste falha.

Este artigo cobre desde a sintaxe básica de documentação até recursos avançados como doc tests, publicação no docs.rs e criação de guias com mdBook. Se você mantém uma biblioteca Rust, dominar a documentação é essencial para a adoção pelo ecossistema.

O Problema: Código sem Documentação (ou com Documentação Falha)

Não Faça Isso: Funções Públicas sem Documentação

// ERRADO: API pública sem nenhuma documentação
pub fn calcular(dados: &[f64], modo: u8) -> Option<f64> {
    match modo {
        0 => Some(dados.iter().sum::<f64>() / dados.len() as f64),
        1 => {
            let mut sorted = dados.to_vec();
            sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
            Some(sorted[sorted.len() / 2])
        }
        _ => None,
    }
}
// Perguntas que ficam sem resposta:
// - O que é "modo"? Quais valores são válidos?
// - O que acontece se dados estiver vazio?
// - Quando retorna None?

Não Faça Isso: Documentação Desatualizada

/// Calcula a soma de todos os elementos.
///
/// # Exemplo
///
/// ```
/// let resultado = meu_crate::calcular(&[1.0, 2.0, 3.0]);
/// assert_eq!(resultado, 6.0);
/// ```
// A assinatura mudou mas a documentação não foi atualizada:
pub fn calcular(dados: &[f64], modo: u8) -> Option<f64> {
    // ... implementação
    # None
}
// O doc test vai FALHAR porque a assinatura não confere!
// Isso é uma VANTAGEM do Rust — a documentação quebrada é detectada.

A Solução: Documentação Idiomática com Doc Tests

/// vs //! — Tipos de Comentários de Documentação

//! # Minha Biblioteca de Estatística
//!
//! Esta crate fornece funções para cálculos estatísticos básicos.
//!
//! ## Exemplo Rápido
//!
//! ```
//! use minha_lib::media;
//!
//! let dados = vec![1.0, 2.0, 3.0, 4.0, 5.0];
//! assert_eq!(media(&dados), Some(3.0));
//! ```
//!
//! ## Módulos
//!
//! - [`basico`] — Média, mediana, moda
//! - [`avancado`] — Desvio padrão, correlação

/// Calcula a média aritmética de uma fatia de números.
///
/// Retorna `None` se a fatia estiver vazia, evitando divisão por zero.
///
/// # Argumentos
///
/// * `dados` — Fatia de valores `f64` para calcular a média
///
/// # Exemplos
///
/// ```
/// use minha_lib::media;
///
/// // Caso normal
/// assert_eq!(media(&[1.0, 2.0, 3.0]), Some(2.0));
///
/// // Fatia vazia retorna None
/// assert_eq!(media(&[]), None);
///
/// // Um único elemento
/// assert_eq!(media(&[42.0]), Some(42.0));
/// ```
///
/// # Performance
///
/// Complexidade O(n) onde n é o tamanho da fatia.
pub fn media(dados: &[f64]) -> Option<f64> {
    if dados.is_empty() {
        return None;
    }
    Some(dados.iter().sum::<f64>() / dados.len() as f64)
}

A diferença:

  • /// documenta o item seguinte (função, struct, enum, etc.)
  • //! documenta o item que contém (crate, módulo)

Doc Tests: Exemplos que Compilam

Todo bloco de código na documentação é um teste automático:

/// Divide dois números com verificação de divisão por zero.
///
/// # Exemplos
///
/// ```
/// use minha_lib::dividir;
///
/// assert_eq!(dividir(10.0, 2.0), Ok(5.0));
/// assert!(dividir(1.0, 0.0).is_err());
/// ```
///
/// # Errors
///
/// Retorna `Err` com mensagem descritiva se o divisor for zero.
///
/// ```
/// use minha_lib::dividir;
///
/// let erro = dividir(1.0, 0.0).unwrap_err();
/// assert_eq!(erro, "Divisão por zero");
/// ```
pub fn dividir(a: f64, b: f64) -> Result<f64, &'static str> {
    if b == 0.0 {
        return Err("Divisão por zero");
    }
    Ok(a / b)
}

Execute os doc tests com:

cargo test --doc

Seções Padrão de Documentação

A comunidade Rust usa seções convencionais. Siga este padrão:

/// Breve descrição em uma linha.
///
/// Descrição mais longa com detalhes sobre o comportamento,
/// pressupostos e uso esperado.
///
/// # Argumentos
///
/// * `param1` — Descrição do primeiro parâmetro
/// * `param2` — Descrição do segundo parâmetro
///
/// # Exemplos
///
/// ```
/// // Exemplo de uso básico
/// ```
///
/// # Errors
///
/// Descreva quando a função retorna `Err`.
///
/// # Panics
///
/// Descreva quando a função pode entrar em panic (se aplicável).
///
/// # Safety
///
/// Para funções `unsafe`, descreva as invariantes que o chamador deve garantir.
pub fn minha_funcao(param1: &str, param2: u32) -> Result<String, MyError> {
    // ...
    # Ok(String::new())
}

# #[derive(Debug)]
# struct MyError;

Documentando Structs e Enums

/// Representa uma conexão com um banco de dados PostgreSQL.
///
/// # Exemplos
///
/// ```no_run
/// use minha_lib::DatabaseConfig;
///
/// let config = DatabaseConfig::builder()
///     .host("localhost")
///     .porta(5432)
///     .banco("meu_app")
///     .build()
///     .expect("Configuração inválida");
/// ```
pub struct DatabaseConfig {
    /// Hostname ou IP do servidor de banco de dados.
    pub host: String,

    /// Porta de conexão (padrão: 5432).
    pub porta: u16,

    /// Nome do banco de dados.
    pub banco: String,

    /// Número máximo de conexões simultâneas no pool.
    ///
    /// O valor padrão é 10. Valores acima de 100 não são recomendados
    /// para a maioria das aplicações.
    pub max_conexoes: u32,
}

/// Tipos de erro que podem ocorrer durante operações no banco de dados.
///
/// # Exemplos
///
/// ```
/// use minha_lib::DatabaseError;
///
/// fn tratar_erro(err: DatabaseError) {
///     match err {
///         DatabaseError::Conexao(msg) => eprintln!("Falha na conexão: {msg}"),
///         DatabaseError::Query(msg) => eprintln!("Query falhou: {msg}"),
///         DatabaseError::Timeout => eprintln!("Operação expirou"),
///     }
/// }
/// ```
pub enum DatabaseError {
    /// Falha ao estabelecer conexão com o banco de dados.
    Conexao(String),

    /// Erro durante a execução de uma query SQL.
    Query(String),

    /// A operação excedeu o tempo limite configurado.
    Timeout,
}

Recursos Avançados de Doc Tests

/// Função que demonstra recursos avançados de doc tests.
///
/// Exemplo que deve compilar mas não executar (útil para código de rede):
///
/// ```no_run
/// use std::net::TcpListener;
/// let listener = TcpListener::bind("0.0.0.0:8080").unwrap();
/// ```
///
/// Exemplo que deve falhar na compilação (para demonstrar o que NÃO fazer):
///
/// ```compile_fail
/// let x: i32 = "não é um número";
/// ```
///
/// Exemplo que deve entrar em panic:
///
/// ```should_panic
/// # fn exemplo() {
/// panic!("Este panic é esperado");
/// # }
/// # exemplo();
/// ```
///
/// Código oculto na documentação (setup que não é relevante):
///
/// ```
/// # fn setup() -> Vec<i32> { vec![1, 2, 3] }
/// # let dados = setup();
/// // O leitor vê apenas isso:
/// let soma: i32 = dados.iter().sum();
/// assert_eq!(soma, 6);
/// ```
pub fn funcao_exemplo() {}

Atributos de Documentação

// Negar warnings de documentação ausente (recomendado para bibliotecas)
#![deny(missing_docs)]

// Habilitar features de documentação
#![doc = include_str!("../README.md")]

/// Função com link para outra documentação.
///
/// Veja também [`outra_funcao`] e o módulo [`utils`].
///
/// Para detalhes sobre o formato, consulte [`std::fmt`].
pub fn minha_funcao() {}

/// Outra função documentada.
pub fn outra_funcao() {}

/// Módulo de utilitários.
pub mod utils {
    //! Funções utilitárias diversas.

    /// Formata um número como moeda brasileira.
    ///
    /// ```
    /// # // Em um doc test real, seria: use minha_lib::utils::formatar_brl;
    /// fn formatar_brl(valor: f64) -> String {
    ///     format!("R$ {:.2}", valor)
    /// }
    /// assert_eq!(formatar_brl(1500.5), "R$ 1500.50");
    /// ```
    pub fn formatar_brl(valor: f64) -> String {
        format!("R$ {:.2}", valor)
    }
}

Guia Passo a Passo: Documentação Profissional

Passo 1: Gerar Documentação Localmente

# Gerar e abrir no navegador
cargo doc --open

# Incluir dependências privadas
cargo doc --document-private-items

# Verificar links quebrados
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps

Passo 2: Configurar Cargo.toml para docs.rs

# Cargo.toml
[package]
name = "minha-lib"
version = "1.0.0"
description = "Biblioteca de estatística para Rust"
documentation = "https://docs.rs/minha-lib"
repository = "https://github.com/usuario/minha-lib"
readme = "README.md"
license = "MIT"
keywords = ["estatística", "matemática", "cálculo"]
categories = ["mathematics", "science"]

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

Passo 3: Badges no README

<!-- No README.md -->
[![Crates.io](https://img.shields.io/crates/v/minha-lib)](https://crates.io/crates/minha-lib)
[![docs.rs](https://docs.rs/minha-lib/badge.svg)](https://docs.rs/minha-lib)
[![CI](https://github.com/usuario/minha-lib/workflows/CI/badge.svg)](https://github.com/usuario/minha-lib/actions)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

Passo 4: mdBook para Guias Longos

Para documentação que vai além do rustdoc (tutoriais, guias, arquitetura):

cargo install mdbook

# Criar estrutura
mdbook init docs

# Editar docs/src/SUMMARY.md
<!-- docs/src/SUMMARY.md -->
# Summary

- [Introdução](./introducao.md)
- [Início Rápido](./inicio-rapido.md)
- [Guia do Usuário](./guia/README.md)
  - [Configuração](./guia/configuracao.md)
  - [Uso Básico](./guia/uso-basico.md)
  - [Recursos Avançados](./guia/avancado.md)
- [Referência da API](./api.md)
- [FAQ](./faq.md)
# Construir e servir
mdbook serve docs --open

Passo 5: Integrar Doc Tests no CI

# .github/workflows/ci.yml
- name: Doc tests
  run: cargo test --doc

- name: Verificar documentação
  run: RUSTDOCFLAGS="-D warnings" cargo doc --no-deps

Armadilhas Comuns

1. Esquecer # use nos Doc Tests

/// ```
/// // ERRADO: Não vai compilar porque falta o `use`
/// let resultado = minha_funcao(42);
/// ```
///
/// ```
/// // CORRETO: Inclua o import (pode ser oculto com #)
/// # use minha_lib::minha_funcao;
/// let resultado = minha_funcao(42);
/// # assert_eq!(resultado, 84);
/// ```
pub fn minha_funcao(x: i32) -> i32 {
    x * 2
}
/// Veja [`outra_funcao`] para mais detalhes.
///
/// // Se `outra_funcao` não existir, rustdoc dará warning.
/// // Com RUSTDOCFLAGS="-D warnings", vira erro.
pub fn minha_funcao() {}

3. Exemplos Que Não Demonstram o Uso Real

/// # Exemplos
///
/// ```
/// // ERRADO: Exemplo trivial que não ajuda
/// use minha_lib::ordenar;
/// let mut v = vec![1, 2, 3];
/// ordenar(&mut v);
/// ```
///
/// ```
/// // CORRETO: Mostra um caso de uso real
/// use minha_lib::ordenar;
///
/// let mut notas = vec![7.5, 9.0, 6.8, 8.3, 5.5];
/// ordenar(&mut notas);
/// assert_eq!(notas, vec![5.5, 6.8, 7.5, 8.3, 9.0]);
/// ```
pub fn ordenar(dados: &mut Vec<f64>) {
    dados.sort_by(|a, b| a.partial_cmp(b).unwrap());
}

4. Documentação Apenas em Inglês

Se o público-alvo inclui falantes de português, considere documentação bilíngue:

/// Calcula o fatorial de um número.
///
/// Calculates the factorial of a number.
///
/// # Exemplos / Examples
///
/// ```
/// # fn fatorial(n: u64) -> u64 { if n <= 1 { 1 } else { n * fatorial(n - 1) } }
/// assert_eq!(fatorial(5), 120);
/// assert_eq!(fatorial(0), 1);
/// ```
pub fn fatorial(n: u64) -> u64 {
    if n <= 1 { 1 } else { n * fatorial(n - 1) }
}

Exemplo do Mundo Real: Biblioteca com Documentação Completa

//! # minha_lib — Utilitários de Texto para Português Brasileiro
//!
//! Esta biblioteca fornece funções para manipulação de texto com suporte
//! a particularidades do português brasileiro.
//!
//! ## Início Rápido
//!
//! ```
//! use minha_lib::TextoBR;
//!
//! let texto = TextoBR::new("São Paulo é a maior cidade do Brasil.");
//! assert_eq!(texto.contar_palavras(), 8);
//! assert!(texto.contem_acentos());
//! ```
//!
//! ## Features
//!
//! - Contagem de palavras com suporte a hifens e apóstrofos
//! - Detecção de acentos e caracteres especiais do português
//! - Normalização de texto (remoção de acentos)

#![deny(missing_docs)]

/// Wrapper para operações em texto brasileiro.
///
/// Fornece métodos otimizados para análise de texto em português.
///
/// # Exemplos
///
/// ```
/// use minha_lib::TextoBR;
///
/// let texto = TextoBR::new("Olá, mundo!");
/// assert_eq!(texto.contar_palavras(), 2);
/// assert!(texto.contem_acentos());
/// assert_eq!(texto.como_str(), "Olá, mundo!");
/// ```
pub struct TextoBR {
    conteudo: String,
}

impl TextoBR {
    /// Cria uma nova instância a partir de uma string.
    ///
    /// # Argumentos
    ///
    /// * `texto` — O texto em português para analisar
    ///
    /// # Exemplos
    ///
    /// ```
    /// use minha_lib::TextoBR;
    ///
    /// let t = TextoBR::new("Bom dia!");
    /// assert_eq!(t.como_str(), "Bom dia!");
    /// ```
    pub fn new(texto: &str) -> Self {
        TextoBR {
            conteudo: texto.to_string(),
        }
    }

    /// Retorna a referência ao conteúdo interno.
    pub fn como_str(&self) -> &str {
        &self.conteudo
    }

    /// Conta o número de palavras no texto.
    ///
    /// Palavras são delimitadas por espaços em branco. Pontuação
    /// adjacente não é contada como parte da palavra.
    ///
    /// # Exemplos
    ///
    /// ```
    /// use minha_lib::TextoBR;
    ///
    /// assert_eq!(TextoBR::new("Uma frase simples").contar_palavras(), 3);
    /// assert_eq!(TextoBR::new("").contar_palavras(), 0);
    /// assert_eq!(TextoBR::new("   ").contar_palavras(), 0);
    /// ```
    pub fn contar_palavras(&self) -> usize {
        self.conteudo.split_whitespace().count()
    }

    /// Verifica se o texto contém caracteres acentuados do português.
    ///
    /// Detecta: á, à, â, ã, é, ê, í, ó, ô, õ, ú, ü, ç (maiúsculas e minúsculas).
    ///
    /// # Exemplos
    ///
    /// ```
    /// use minha_lib::TextoBR;
    ///
    /// assert!(TextoBR::new("café").contem_acentos());
    /// assert!(TextoBR::new("ação").contem_acentos());
    /// assert!(!TextoBR::new("hello world").contem_acentos());
    /// ```
    pub fn contem_acentos(&self) -> bool {
        self.conteudo.chars().any(|c| {
            matches!(
                c,
                'á' | 'à' | 'â' | 'ã' | 'é' | 'ê' | 'í' | 'ó' | 'ô' | 'õ' | 'ú' | 'ü' | 'ç'
                | 'Á' | 'À' | 'Â' | 'Ã' | 'É' | 'Ê' | 'Í' | 'Ó' | 'Ô' | 'Õ' | 'Ú' | 'Ü' | 'Ç'
            )
        })
    }
}

Checklist de Documentação

  1. #![deny(missing_docs)] — Exija documentação em itens públicos
  2. Doc tests em toda função pública — Exemplos que compilam
  3. Seções padronizadas — Arguments, Examples, Errors, Panics
  4. cargo test --doc no CI — Nunca deixe doc tests quebrarem
  5. Links internos [item] — Navegação entre itens da API
  6. RUSTDOCFLAGS="-D warnings" — Links quebrados viram erros
  7. #[doc = include_str!("../README.md")] — README como documentação do crate
  8. docs.rs metadata — Configure features e flags para docs.rs
  9. Badges — Crates.io, docs.rs, CI status
  10. mdBook para guias — Documentação longa fora do rustdoc

Veja Também