Rust

Documentação e Doc Tests — Escrevendo Documentação que Não Fica Desatualizada Já leu

16 min de leitura

Documentação e Doc Tests — Escrevendo Documentação que Não Fica Desatualizada
Rust — Artigo #38 Documentação e Doc Tests — Escrevendo Documentação que Não Fica Desatualizada Por Prof. Dr. Marcelo Fontes | Série: Dominando Rust em 1 Ano Do

Rust — Artigo #38

Documentação e Doc Tests — Escrevendo Documentação que Não Fica Desatualizada

Por Prof. Dr. Marcelo Fontes | Série: Dominando Rust em 1 Ano


Documentação desatualizada é pior que documentação ausente. Um comentário que mente sobre o comportamento do código induz erros com mais eficiência que a ausência de qualquer orientação. Rust resolve esse problema com uma abordagem elegante: a documentação é código, e o código é testado.

Doc tests — exemplos de código dentro da documentação — são compilados e executados pelo cargo test. Se o comportamento da função mudar e o exemplo quebrar, o teste falha. A documentação não pode mentir sobre código que está sendo testado.


A ferramenta: rustdoc

rustdoc gera documentação HTML a partir de comentários especiais. Está integrado ao Cargo:

# Gera documentação do projeto atual
cargo doc

# Gera e abre no navegador
cargo doc --open

# Inclui documentação das dependências
cargo doc --document-private-items

# Executa apenas os doc tests
cargo test --doc

Comentários de documentação

// Comentário normal — não aparece na documentação

/// Comentário de documentação — aparece na documentação do ITEM SEGUINTE
/// Suporta Markdown completo.

//! Comentário de documentação do MÓDULO OU CRATE atual
//! Usado no topo de lib.rs ou mod.rs

/// # Seções Markdown
///
/// ## Exemplo
///
/// ```rust
/// let x = 42;
/// assert_eq!(x, 42);
/// ```
///
/// ## Panics
/// Descreve quando a função entra em pânico.
///
/// ## Errors
/// Descreve os erros que podem ser retornados.
///
/// ## Safety
/// Para funções `unsafe` — explica os invariantes.
fn exemplo() {}

Documentando uma biblioteca completa

//! # Calculadora de Intervalos
//!
//! Esta biblioteca fornece tipos para trabalhar com intervalos
//! numéricos de forma segura e expressiva.
//!
//! ## Uso Rápido
//!
//! ```rust
//! use calc_intervalos::Intervalo;
//!
//! let intervalo = Intervalo::novo(1.0, 10.0).unwrap();
//! assert!(intervalo.contem(5.0));
//! assert!(!intervalo.contem(15.0));
//! println!("Comprimento: {}", intervalo.comprimento());
//! ```
//!
//! ## Feature Flags
//!
//! - `serde` — Serialização/deserialização com Serde

/// Representa um intervalo fechado [início, fim].
///
/// Um intervalo fechado inclui seus dois extremos.
/// Garante em tempo de construção que `início <= fim`.
///
/// # Exemplos
///
/// ```rust
/// use calc_intervalos::Intervalo;
///
/// let i = Intervalo::novo(0.0, 1.0).unwrap();
/// assert_eq!(i.inicio(), 0.0);
/// assert_eq!(i.fim(), 1.0);
/// ```
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Intervalo {
    inicio: f64,
    fim: f64,
}

impl Intervalo {
    /// Cria um novo intervalo [início, fim].
    ///
    /// # Argumentos
    ///
    /// * `inicio` — Extremo inferior do intervalo (inclusivo)
    /// * `fim` — Extremo superior do intervalo (inclusivo)
    ///
    /// # Retorna
    ///
    /// `Some(Intervalo)` se `inicio <= fim`, `None` caso contrário.
    ///
    /// # Exemplos
    ///
    /// ```rust
    /// use calc_intervalos::Intervalo;
    ///
    /// // Intervalo válido
    /// let i = Intervalo::novo(1.0, 5.0);
    /// assert!(i.is_some());
    ///
    /// // Intervalo degenerado (ponto único) — válido
    /// let ponto = Intervalo::novo(3.0, 3.0);
    /// assert!(ponto.is_some());
    ///
    /// // Intervalo inválido
    /// let invalido = Intervalo::novo(5.0, 1.0);
    /// assert!(invalido.is_none());
    /// ```
    pub fn novo(inicio: f64, fim: f64) -> Option<Self> {
        if inicio <= fim {
            Some(Intervalo { inicio, fim })
        } else {
            None
        }
    }

    /// Retorna o extremo inferior do intervalo.
    ///
    /// # Exemplos
    ///
    /// ```rust
    /// # use calc_intervalos::Intervalo;
    /// let i = Intervalo::novo(2.0, 8.0).unwrap();
    /// assert_eq!(i.inicio(), 2.0);
    /// ```
    pub fn inicio(&self) -> f64 { self.inicio }

    /// Retorna o extremo superior do intervalo.
    ///
    /// # Exemplos
    ///
    /// ```rust
    /// # use calc_intervalos::Intervalo;
    /// let i = Intervalo::novo(2.0, 8.0).unwrap();
    /// assert_eq!(i.fim(), 8.0);
    /// ```
    pub fn fim(&self) -> f64 { self.fim }

    /// Retorna o comprimento (medida) do intervalo.
    ///
    /// O comprimento é definido como `fim - início`.
    ///
    /// # Exemplos
    ///
    /// ```rust
    /// # use calc_intervalos::Intervalo;
    /// let i = Intervalo::novo(2.0, 7.0).unwrap();
    /// assert_eq!(i.comprimento(), 5.0);
    ///
    /// // Intervalo degenerado tem comprimento zero
    /// let ponto = Intervalo::novo(3.0, 3.0).unwrap();
    /// assert_eq!(ponto.comprimento(), 0.0);
    /// ```
    pub fn comprimento(&self) -> f64 {
        self.fim - self.inicio
    }

    /// Verifica se um valor está contido no intervalo.
    ///
    /// Um valor `x` está contido se `inicio <= x <= fim`.
    ///
    /// # Exemplos
    ///
    /// ```rust
    /// # use calc_intervalos::Intervalo;
    /// let i = Intervalo::novo(1.0, 10.0).unwrap();
    ///
    /// // Interior
    /// assert!(i.contem(5.0));
    ///
    /// // Extremos são incluídos (intervalo fechado)
    /// assert!(i.contem(1.0));
    /// assert!(i.contem(10.0));
    ///
    /// // Exterior
    /// assert!(!i.contem(0.0));
    /// assert!(!i.contem(11.0));
    /// ```
    pub fn contem(&self, valor: f64) -> bool {
        valor >= self.inicio && valor <= self.fim
    }

    /// Calcula a interseção com outro intervalo.
    ///
    /// Retorna `Some(intervalo)` se os intervalos se sobrepõem,
    /// `None` se são disjuntos.
    ///
    /// # Exemplos
    ///
    /// ```rust
    /// # use calc_intervalos::Intervalo;
    /// let a = Intervalo::novo(1.0, 5.0).unwrap();
    /// let b = Intervalo::novo(3.0, 8.0).unwrap();
    ///
    /// let intersecao = a.intersectar(&b).unwrap();
    /// assert_eq!(intersecao.inicio(), 3.0);
    /// assert_eq!(intersecao.fim(), 5.0);
    ///
    /// // Sem sobreposição
    /// let c = Intervalo::novo(6.0, 9.0).unwrap();
    /// assert!(a.intersectar(&c).is_none());
    /// ```
    pub fn intersectar(&self, outro: &Intervalo) -> Option<Intervalo> {
        let inicio = self.inicio.max(outro.inicio);
        let fim    = self.fim.min(outro.fim);
        Intervalo::novo(inicio, fim)
    }

    /// Retorna o menor intervalo que contém ambos os intervalos.
    ///
    /// Também conhecido como "hull" ou "union convexa".
    ///
    /// # Exemplos
    ///
    /// ```rust
    /// # use calc_intervalos::Intervalo;
    /// let a = Intervalo::novo(1.0, 3.0).unwrap();
    /// let b = Intervalo::novo(5.0, 8.0).unwrap();
    ///
    /// let envoltoria = a.envoltoria(&b);
    /// assert_eq!(envoltoria.inicio(), 1.0);
    /// assert_eq!(envoltoria.fim(), 8.0);
    /// ```
    pub fn envoltoria(&self, outro: &Intervalo) -> Intervalo {
        Intervalo {
            inicio: self.inicio.min(outro.inicio),
            fim:    self.fim.max(outro.fim),
        }
    }

    /// Escala o intervalo por um fator em torno do seu centro.
    ///
    /// # Panics
    ///
    /// Entra em pânico se `fator` for negativo.
    ///
    /// # Exemplos
    ///
    /// ```rust
    /// # use calc_intervalos::Intervalo;
    /// let i = Intervalo::novo(0.0, 10.0).unwrap();
    ///
    /// // Dobra o tamanho (mantém o centro em 5.0)
    /// let dobrado = i.escalar(2.0);
    /// assert_eq!(dobrado.inicio(), -5.0);
    /// assert_eq!(dobrado.fim(), 15.0);
    /// ```
    ///
    /// ```rust,should_panic
    /// # use calc_intervalos::Intervalo;
    /// let i = Intervalo::novo(0.0, 10.0).unwrap();
    /// i.escalar(-1.0); // Panic!
    /// ```
    pub fn escalar(&self, fator: f64) -> Intervalo {
        assert!(fator >= 0.0, "Fator de escala não pode ser negativo");
        let centro = (self.inicio + self.fim) / 2.0;
        let metade = self.comprimento() / 2.0 * fator;
        Intervalo {
            inicio: centro - metade,
            fim:    centro + metade,
        }
    }
}

impl std::fmt::Display for Intervalo {
    /// Formata o intervalo como `[início, fim]`.
    ///
    /// # Exemplos
    ///
    /// ```rust
    /// # use calc_intervalos::Intervalo;
    /// let i = Intervalo::novo(1.0, 5.0).unwrap();
    /// assert_eq!(format!("{}", i), "[1, 5]");
    /// ```
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "[{}, {}]", self.inicio, self.fim)
    }
}

Anatomia de um doc test

/// Demonstra as diferentes formas de escrever doc tests.
///
/// ## Código oculto com `#`
///
/// Linhas prefixadas com `#` são compiladas mas não aparecem
/// na documentação renderizada — útil para setup:
///
/// ```rust
/// # use minha_crate::MinhaStruct;  // oculto na doc, necessário para compilar
/// # let config = MinhaStruct::nova();  // oculto
/// let resultado = config.processar("dados"); // visível
/// assert_eq!(resultado, "DADOS");  // visível
/// ```
///
/// ## Testes que devem falhar em pânico
///
/// ```rust,should_panic
/// # use minha_crate::Intervalo;
/// let i = Intervalo::novo(0.0, 1.0).unwrap();
/// i.escalar(-2.0); // deve causar panic
/// ```
///
/// ## Código que não deve compilar
///
/// ```rust,compile_fail
/// let x: u32 = "não sou um número"; // erro de tipo
/// ```
///
/// ## Código sem execução (apenas exibição)
///
/// ```rust,no_run
/// // Este código compilaria mas não queremos executar em teste
/// // (ex: requer hardware, servidor externo, etc.)
/// std::thread::sleep(std::time::Duration::from_secs(3600));
/// ```
///
/// ## Outros idiomas (não testados)
///
/// ```bash
/// cargo run --release
/// ```
///
/// ```toml
/// [dependencies]
/// minha_crate = "1"
/// ```
///
/// ```text
/// Saída esperada:
/// resultado = 42
/// ```
pub fn documentar_exemplos() {}

Testando erros — documentando com ?

Doc tests em funções que retornam Result precisam de um wrapper:

/// Parseia uma data no formato `YYYY-MM-DD`.
///
/// # Erros
///
/// Retorna `Err` se o formato for inválido ou a data não existir.
///
/// # Exemplos
///
/// ```rust
/// # use minha_crate::parsear_data;
/// // O tipo de retorno fn() -> Result<_, _> permite usar ?
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let data = parsear_data("2024-03-15")?;
/// assert_eq!(data.ano, 2024);
/// assert_eq!(data.mes, 3);
/// assert_eq!(data.dia, 15);
/// # Ok(())
/// # }
/// ```
pub fn parsear_data(s: &str) -> Result<Data, ErroData> {
    let partes: Vec<&str> = s.split('-').collect();
    if partes.len() != 3 {
        return Err(ErroData::FormatoInvalido);
    }
    Ok(Data {
        ano: partes[0].parse().map_err(|_| ErroData::FormatoInvalido)?,
        mes: partes[1].parse().map_err(|_| ErroData::FormatoInvalido)?,
        dia: partes[2].parse().map_err(|_| ErroData::FormatoInvalido)?,
    })
}

#[derive(Debug, PartialEq)]
pub struct Data { pub ano: u32, pub mes: u32, pub dia: u32 }

#[derive(Debug)]
pub enum ErroData { FormatoInvalido }

impl std::fmt::Display for ErroData {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "Formato de data inválido")
    }
}

impl std::error::Error for ErroData {}

Documentação de módulos e organização

//! # Módulo de Autenticação
//!
//! Fornece primitivas para autenticação de usuários.
//!
//! ## Fluxo típico
//!
//! ```rust,no_run
//! # use minha_crate::auth::{Autenticador, Credenciais};
//! let auth = Autenticador::novo("minha_chave_secreta");
//!
//! // Login
//! let token = auth.login(Credenciais {
//!     email: "user@exemplo.com".to_string(),
//!     senha: "senha_secreta".to_string(),
//! }).unwrap();
//!
//! // Verificação em rotas protegidas
//! let usuario = auth.verificar(&token).unwrap();
//! println!("Usuário: {}", usuario.nome);
//! ```

pub mod auth {
    /// Gerencia autenticação e geração de tokens.
    ///
    /// Veja o [guia de autenticação](crate::auth) para exemplos completos.
    pub struct Autenticador {
        chave: String,
    }

    impl Autenticador {
        /// Cria um autenticador com a chave secreta fornecida.
        ///
        /// # Segurança
        ///
        /// A chave deve ter pelo menos 32 bytes de entropia.
        /// Use `openssl rand -hex 32` para gerar uma chave adequada.
        ///
        /// # Exemplos
        ///
        /// ```rust
        /// # use minha_crate::auth::Autenticador;
        /// let auth = Autenticador::novo("chave_de_pelo_menos_32_caracteres_aqui");
        /// ```
        pub fn novo(chave: &str) -> Self {
            Autenticador { chave: chave.to_string() }
        }
    }
}

Links em documentação

/// Processa um [`Pedido`] e retorna um [`Resultado`].
///
/// Veja também:
/// - [`Pedido::novo`] para criar pedidos
/// - [`Resultado::sucesso`] para interpretar o retorno
/// - [`crate::erros::ErroPedido`] para tratamento de erros
/// - [documentação do protocolo](https://protocolo.exemplo.com)
///
/// # Exemplos
///
/// Para uso avançado, veja [`ProcessadorAvancado`].
pub fn processar(pedido: Pedido) -> Resultado {
    todo!()
}

/// Um pedido de processamento.
///
/// Relacionado: [`processar`]
pub struct Pedido { pub id: u64 }
impl Pedido {
    /// Cria um novo pedido com o ID fornecido.
    pub fn novo(id: u64) -> Self { Pedido { id } }
}

pub struct Resultado { pub sucesso: bool }
impl Resultado {
    /// Retorna `true` se o processamento foi bem-sucedido.
    pub fn sucesso(&self) -> bool { self.sucesso }
}

pub struct ProcessadorAvancado;

Atributos de documentação úteis

/// Item depreciado — use [`nova_funcao`] no lugar.
///
/// # Exemplos
///
/// ```rust,no_run
/// # use minha_crate::funcao_antiga;
/// funcao_antiga(); // gera aviso de depreciação
/// ```
#[deprecated(since = "2.0.0", note = "Use nova_funcao() no lugar")]
pub fn funcao_antiga() {}

/// Substituto moderno de [`funcao_antiga`].
pub fn nova_funcao() {}

// Oculta item da documentação pública mas mantém acessível
#[doc(hidden)]
pub fn detalhe_interno() {}

// Inclui conteúdo de arquivo markdown externo
#[doc = include_str!("../CHANGELOG.md")]
pub struct ChangelogDummy;

// Alias para uso em links
/// Veja [`TipoAliasado`](crate::TipoOriginal).
pub type TipoAliasado = crate::TipoOriginal;
pub struct TipoOriginal;

Um projeto completo: biblioteca bem documentada

Vamos construir uma pequena biblioteca de estatísticas com documentação exemplar:

//! # estatistica-rs
//!
//! Funções estatísticas simples para análise de dados numéricos.
//!
//! ## Exemplo Completo
//!
//! ```rust
//! use estatistica_rs::{Amostra, resumo};
//!
//! let dados = vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
//! let amostra = Amostra::nova(dados).unwrap();
//!
//! println!("Média:   {}", amostra.media());
//! println!("Mediana: {}", amostra.mediana());
//! println!("DP:      {:.4}", amostra.desvio_padrao());
//!
//! // Saída:
//! // Média:   5
//! // Mediana: 4.5
//! // DP:      1.8708
//! ```

/// Erros possíveis ao trabalhar com amostras.
#[derive(Debug, PartialEq)]
pub enum ErroAmostra {
    /// A amostra não contém nenhum dado.
    Vazia,
    /// O percentil solicitado está fora do intervalo `[0.0, 100.0]`.
    PercentilInvalido(f64),
}

impl std::fmt::Display for ErroAmostra {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            ErroAmostra::Vazia =>
                write!(f, "Amostra vazia"),
            ErroAmostra::PercentilInvalido(p) =>
                write!(f, "Percentil inválido: {p} (deve estar em [0, 100])"),
        }
    }
}

impl std::error::Error for ErroAmostra {}

/// Uma amostra de dados numéricos.
///
/// Mantém os dados ordenados internamente para cálculos eficientes
/// de mediana e percentis.
///
/// # Exemplos
///
/// ```rust
/// # use estatistica_rs::Amostra;
/// let a = Amostra::nova(vec![3.0, 1.0, 4.0, 1.0, 5.0]).unwrap();
/// assert_eq!(a.n(), 5);
/// assert_eq!(a.media(), 2.8);
/// ```
#[derive(Debug, Clone)]
pub struct Amostra {
    dados: Vec<f64>,       // ordenados
    soma: f64,
}

impl Amostra {
    /// Cria uma nova amostra a partir de um vetor de valores.
    ///
    /// Os dados são ordenados internamente.
    ///
    /// # Erros
    ///
    /// Retorna [`ErroAmostra::Vazia`] se o vetor estiver vazio.
    ///
    /// # Exemplos
    ///
    /// ```rust
    /// # use estatistica_rs::{Amostra, ErroAmostra};
    /// assert!(Amostra::nova(vec![1.0, 2.0, 3.0]).is_ok());
    /// assert_eq!(Amostra::nova(vec![]).unwrap_err(), ErroAmostra::Vazia);
    /// ```
    pub fn nova(mut dados: Vec<f64>) -> Result<Self, ErroAmostra> {
        if dados.is_empty() {
            return Err(ErroAmostra::Vazia);
        }
        dados.sort_by(|a, b| a.partial_cmp(b).unwrap());
        let soma = dados.iter().sum();
        Ok(Amostra { dados, soma })
    }

    /// Retorna o número de observações.
    ///
    /// # Exemplos
    ///
    /// ```rust
    /// # use estatistica_rs::Amostra;
    /// let a = Amostra::nova(vec![1.0, 2.0, 3.0, 4.0]).unwrap();
    /// assert_eq!(a.n(), 4);
    /// ```
    pub fn n(&self) -> usize { self.dados.len() }

    /// Calcula a média aritmética.
    ///
    /// `média = Σxᵢ / n`
    ///
    /// # Exemplos
    ///
    /// ```rust
    /// # use estatistica_rs::Amostra;
    /// let a = Amostra::nova(vec![1.0, 2.0, 3.0, 4.0, 5.0]).unwrap();
    /// assert_eq!(a.media(), 3.0);
    /// ```
    pub fn media(&self) -> f64 {
        self.soma / self.n() as f64
    }

    /// Calcula a mediana (valor central da distribuição).
    ///
    /// Para `n` ímpar: elemento central.
    /// Para `n` par: média dos dois elementos centrais.
    ///
    /// # Exemplos
    ///
    /// ```rust
    /// # use estatistica_rs::Amostra;
    /// // n ímpar
    /// let a = Amostra::nova(vec![1.0, 3.0, 5.0]).unwrap();
    /// assert_eq!(a.mediana(), 3.0);
    ///
    /// // n par
    /// let b = Amostra::nova(vec![1.0, 3.0, 5.0, 7.0]).unwrap();
    /// assert_eq!(b.mediana(), 4.0);
    /// ```
    pub fn mediana(&self) -> f64 {
        let n = self.n();
        if n % 2 == 1 {
            self.dados[n / 2]
        } else {
            (self.dados[n/2 - 1] + self.dados[n/2]) / 2.0
        }
    }

    /// Calcula a variância populacional.
    ///
    /// `σ² = Σ(xᵢ - μ)² / n`
    ///
    /// Para variância amostral (divisão por n-1), use [`variancia_amostral`](Self::variancia_amostral).
    ///
    /// # Exemplos
    ///
    /// ```rust
    /// # use estatistica_rs::Amostra;
    /// let a = Amostra::nova(vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]).unwrap();
    /// assert!((a.variancia() - 3.5).abs() < 1e-10);
    /// ```
    pub fn variancia(&self) -> f64 {
        let media = self.media();
        self.dados.iter()
            .map(|&x| (x - media).powi(2))
            .sum::<f64>() / self.n() as f64
    }

    /// Calcula a variância amostral (divisão por n-1).
    ///
    /// Use quando os dados representam uma amostra de uma
    /// população maior (correção de Bessel).
    ///
    /// # Panics
    ///
    /// Entra em pânico se `n == 1` (divisão por zero).
    ///
    /// # Exemplos
    ///
    /// ```rust
    /// # use estatistica_rs::Amostra;
    /// let a = Amostra::nova(vec![2.0, 4.0, 6.0]).unwrap();
    /// assert!((a.variancia_amostral() - 4.0).abs() < 1e-10);
    /// ```
    pub fn variancia_amostral(&self) -> f64 {
        assert!(self.n() > 1, "Variância amostral requer n > 1");
        let media = self.media();
        self.dados.iter()
            .map(|&x| (x - media).powi(2))
            .sum::<f64>() / (self.n() - 1) as f64
    }

    /// Calcula o desvio padrão populacional (raiz da variância).
    ///
    /// # Exemplos
    ///
    /// ```rust
    /// # use estatistica_rs::Amostra;
    /// let a = Amostra::nova(vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]).unwrap();
    /// let dp = a.desvio_padrao();
    /// assert!((dp - 1.8708286933869707).abs() < 1e-10);
    /// ```
    pub fn desvio_padrao(&self) -> f64 {
        self.variancia().sqrt()
    }

    /// Calcula um percentil da amostra.
    ///
    /// Usa interpolação linear (método recomendado pelo NIST).
    ///
    /// # Argumentos
    ///
    /// * `p` — Percentil em `[0.0, 100.0]`
    ///
    /// # Erros
    ///
    /// Retorna [`ErroAmostra::PercentilInvalido`] se `p < 0` ou `p > 100`.
    ///
    /// # Exemplos
    ///
    /// ```rust
    /// # use estatistica_rs::Amostra;
    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// let a = Amostra::nova(vec![1.0, 2.0, 3.0, 4.0, 5.0]).unwrap();
    ///
    /// // Mediana = percentil 50
    /// assert_eq!(a.percentil(50.0)?, a.mediana());
    ///
    /// // Mínimo e máximo
    /// assert_eq!(a.percentil(0.0)?, 1.0);
    /// assert_eq!(a.percentil(100.0)?, 5.0);
    /// # Ok(())
    /// # }
    /// ```
    pub fn percentil(&self, p: f64) -> Result<f64, ErroAmostra> {
        if p < 0.0 || p > 100.0 {
            return Err(ErroAmostra::PercentilInvalido(p));
        }

        let n = self.n();
        if n == 1 { return Ok(self.dados[0]); }

        let indice = p / 100.0 * (n - 1) as f64;
        let i = indice.floor() as usize;
        let fracao = indice - i as f64;

        if i + 1 >= n {
            Ok(self.dados[n - 1])
        } else {
            Ok(self.dados[i] * (1.0 - fracao) + self.dados[i + 1] * fracao)
        }
    }

    /// Retorna o valor mínimo da amostra.
    ///
    /// # Exemplos
    ///
    /// ```rust
    /// # use estatistica_rs::Amostra;
    /// let a = Amostra::nova(vec![5.0, 2.0, 8.0, 1.0]).unwrap();
    /// assert_eq!(a.minimo(), 1.0);
    /// ```
    pub fn minimo(&self) -> f64 { self.dados[0] }

    /// Retorna o valor máximo da amostra.
    ///
    /// # Exemplos
    ///
    /// ```rust
    /// # use estatistica_rs::Amostra;
    /// let a = Amostra::nova(vec![5.0, 2.0, 8.0, 1.0]).unwrap();
    /// assert_eq!(a.maximo(), 8.0);
    /// ```
    pub fn maximo(&self) -> f64 { *self.dados.last().unwrap() }

    /// Retorna os cinco números do resumo estatístico.
    ///
    /// O resumo consiste em: (mínimo, Q1, mediana, Q3, máximo).
    ///
    /// # Exemplos
    ///
    /// ```rust
    /// # use estatistica_rs::Amostra;
    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// let a = Amostra::nova(vec![1.0,2.0,3.0,4.0,5.0,6.0,7.0]).unwrap();
    /// let (min, q1, med, q3, max) = a.cinco_numeros()?;
    ///
    /// assert_eq!(min, 1.0);
    /// assert_eq!(med, 4.0);
    /// assert_eq!(max, 7.0);
    /// assert!(q1 < med && med < q3);
    /// # Ok(())
    /// # }
    /// ```
    pub fn cinco_numeros(&self) -> Result<(f64,f64,f64,f64,f64), ErroAmostra> {
        Ok((
            self.minimo(),
            self.percentil(25.0)?,
            self.mediana(),
            self.percentil(75.0)?,
            self.maximo(),
        ))
    }
}

/// Imprime um resumo estatístico formatado para uma amostra.
///
/// # Exemplos
///
/// ```rust
/// # use estatistica_rs::{Amostra, resumo};
/// let a = Amostra::nova(vec![1.0,2.0,3.0,4.0,5.0]).unwrap();
/// resumo(&a); // imprime tabela de resumo
/// ```
pub fn resumo(amostra: &Amostra) {
    println!("── Resumo Estatístico ──────────────");
    println!("  n        : {}", amostra.n());
    println!("  Média    : {:.4}", amostra.media());
    println!("  Mediana  : {:.4}", amostra.mediana());
    println!("  Desvio P : {:.4}", amostra.desvio_padrao());
    println!("  Mínimo   : {:.4}", amostra.minimo());
    println!("  Máximo   : {:.4}", amostra.maximo());
    if let Ok((_, q1, _, q3, _)) = amostra.cinco_numeros() {
        println!("  Q1 / Q3  : {:.4} / {:.4}", q1, q3);
        println!("  IQR      : {:.4}", q3 - q1);
    }
    println!("────────────────────────────────────");
}

fn main() {
    let dados = vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
    let amostra = Amostra::nova(dados).unwrap();
    resumo(&amostra);
}

Execute os doc tests:

cargo test --doc
# running 14 tests
# test src/lib.rs - Amostra::cinco_numeros ... ok
# test src/lib.rs - Amostra::desvio_padrao ... ok
# test src/lib.rs - Amostra::media ... ok
# ...
# test result: ok. 14 passed; 0 failed

Boas práticas de documentação

Documente o "porquê", não apenas o "o quê". O nome da função já diz o que ela faz. A documentação deve explicar quando usá-la, quais são as alternativas, e quais são as armadilhas.

Todo item público merece documentação. Em crates publicados no crates.io, itens sem doc geram aviso do #![warn(missing_docs)]. Ative esse lint em bibliotecas públicas.

Exemplos devem ser auto-contidos e realistas. Use # para ocultar boilerplate, mas garanta que o exemplo compilado reflita uso real.

Documente os casos de erro com a mesma atenção que os casos felizes. A seção # Errors é frequentemente a mais valiosa para quem usa a biblioteca.

Use cargo doc --open durante o desenvolvimento. Ver a documentação renderizada revela problemas que não são visíveis no código-fonte.


Fontes e leituras recomendadas

  • "rustdoc book" — guia oficial completo — https://doc.rust-lang.org/rustdoc/
  • "Rust API Guidelines — Documentation" — convenções para documentação pública — https://rust-lang.github.io/api-guidelines/documentation.html
  • #![warn(missing_docs)] — lint que força documentação em itens públicos
  • "The Importance of Comments" — Jon Gjengset — filosofia de documentação — https://www.youtube.com/watch?v=8j-9wAV0tGw
  • Exemplo excelente: documentação do std::vec::Vec — https://doc.rust-lang.org/std/vec/struct.Vec.html

Artigo #38 de 52 | Série: Dominando Rust em 1 Ano Próximo → Artigo #39: Gerenciamento de Dependências e Publicação de Crates — Do projeto local ao crates.io


Comentários

Mais em Rust

Projetos Integradores — Construindo Sistemas Completos do Zero
Projetos Integradores — Construindo Sistemas Completos do Zero

Rust — Artigo #51 Projetos Integradores — Construindo Sistemas Completos do Z...

Funções, Expressões e Como Rust Pensa Diferente sobre Retorno de Valores
Funções, Expressões e Como Rust Pensa Diferente sobre Retorno de Valores

Se voc&ecirc; vem de Python, JavaScript ou Java, j&aacute; sabe o que &eacute...

Borrowing e Referências — Usando sem Possuir
Borrowing e Referências — Usando sem Possuir

&nbsp; No artigo anterior, aprendemos que ownership resolve o problema do ge...