apibrasil 1.0.0

SDK oficial Rust da plataforma APIBrasil: WhatsApp, SMS, consultas de CPF/CNPJ, veiculos, CEP, correios, pagamentos PIX/boleto e mais.
Documentation
//! Hierarquia de erros da SDK.
//!
//! Toda chamada devolve [`Result<T>`], e a falha carrega a categoria em
//! [`Error::kind`] — o equivalente Rust às subclasses de erro das demais
//! SDKs da plataforma.

use std::collections::HashMap;
use std::error::Error as StdError;
use std::fmt;
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};

use serde_json::Value;

use super::types::{Json, ResponseData};

/// Resultado de qualquer chamada da SDK.
pub type Result<T> = std::result::Result<T, Error>;

/// Categoria de uma falha da SDK.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ErrorKind {
    /// Falha genérica da API.
    Api,
    /// Falha de rede: a requisição pode não ter chegado ao servidor.
    Network,
    /// Tempo limite excedido. A requisição pode ter sido processada, por
    /// isso a SDK nunca faz retry automático — evita duplicar
    /// cobranças/envios.
    Timeout,
    /// HTTP 400/422: payload inválido.
    Validation,
    /// HTTP 401: Bearer Token ausente, inválido ou expirado.
    Authentication,
    /// HTTP 402: saldo/créditos insuficientes.
    InsufficientBalance,
    /// HTTP 403: sem permissão (ex: API exige conta PJ).
    Permission,
    /// HTTP 404/410: recurso não encontrado ou desativado.
    NotFound,
    /// HTTP 429: rate limit atingido.
    RateLimit,
    /// HTTP 5xx: erro interno do gateway/provedor.
    Server,
}

impl ErrorKind {
    /// Informa se a categoria é de falha antes da resposta — rede ou
    /// tempo limite.
    pub const fn is_network(self) -> bool {
        matches!(self, ErrorKind::Network | ErrorKind::Timeout)
    }

    /// Nome da categoria, como usado nas demais SDKs.
    pub const fn as_str(self) -> &'static str {
        match self {
            ErrorKind::Api => "api",
            ErrorKind::Network => "network",
            ErrorKind::Timeout => "timeout",
            ErrorKind::Validation => "validation",
            ErrorKind::Authentication => "authentication",
            ErrorKind::InsufficientBalance => "insufficient_balance",
            ErrorKind::Permission => "permission",
            ErrorKind::NotFound => "not_found",
            ErrorKind::RateLimit => "rate_limit",
            ErrorKind::Server => "server",
        }
    }
}

impl fmt::Display for ErrorKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_str())
    }
}

/// Erro devolvido por todas as chamadas da SDK.
///
/// ```no_run
/// # use apibrasil::{ApiBrasil, ErrorKind};
/// # use serde_json::json;
/// # async fn exemplo(api: ApiBrasil) {
/// match api.consulta().cpf(json!({ "cpf": "00000000000" })).await {
///     Ok(consulta) => println!("{:?}", consulta.data()),
///     Err(error) => match error.kind() {
///         ErrorKind::InsufficientBalance => println!("Recarregue seus créditos"),
///         ErrorKind::RateLimit => println!("Aguarde {:?}", error.retry_after),
///         _ => println!("{} (HTTP {:?})", error, error.status),
///     },
/// }
/// # }
/// ```
#[derive(Clone)]
pub struct Error {
    /// Categoria da falha.
    pub kind: ErrorKind,
    /// Mensagem de erro — a da API quando disponível.
    pub message: String,
    /// Status HTTP retornado pela API (ex: 401, 402, 404).
    pub status: Option<u16>,
    /// Código de erro retornado pela API (ex: `NOT_FOUND`).
    pub code: Option<String>,
    /// Corpo completo da resposta de erro, quando existir.
    pub response: Option<Json>,
    /// Espera sugerida pelo servidor (header `Retry-After`), preenchida
    /// em HTTP 429.
    pub retry_after: Option<Duration>,
    source: Option<Arc<dyn StdError + Send + Sync>>,
}

impl Error {
    /// Cria um erro da categoria informada.
    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
        Self {
            kind,
            message: message.into(),
            status: None,
            code: None,
            response: None,
            retry_after: None,
            source: None,
        }
    }

    /// Falha de rede — nenhuma resposta recebida.
    pub fn network(message: impl Into<String>) -> Self {
        Self::new(ErrorKind::Network, message)
    }

    /// Falha por tempo limite excedido.
    pub fn timeout(message: impl Into<String>) -> Self {
        Self::new(ErrorKind::Timeout, message)
    }

    /// Falha de validação (payload inválido).
    pub fn validation(message: impl Into<String>) -> Self {
        Self::new(ErrorKind::Validation, message)
    }

    /// Falha de autenticação.
    pub fn authentication(message: impl Into<String>) -> Self {
        Self::new(ErrorKind::Authentication, message)
    }

    /// Anexa o erro original que causou esta falha.
    pub fn with_source(mut self, source: impl StdError + Send + Sync + 'static) -> Self {
        self.source = Some(Arc::new(source));
        self
    }

    /// Anexa o corpo da resposta de erro.
    pub fn with_response(mut self, response: Json) -> Self {
        self.response = Some(response);
        self
    }

    /// Categoria da falha.
    pub const fn kind(&self) -> ErrorKind {
        self.kind
    }

    /// Falha genérica da API.
    pub fn is_api(&self) -> bool {
        self.kind == ErrorKind::Api
    }

    /// Falha antes da resposta — rede ou tempo limite.
    pub fn is_network(&self) -> bool {
        self.kind.is_network()
    }

    /// Tempo limite excedido.
    pub fn is_timeout(&self) -> bool {
        self.kind == ErrorKind::Timeout
    }

    /// HTTP 400/422 — payload inválido.
    pub fn is_validation(&self) -> bool {
        self.kind == ErrorKind::Validation
    }

    /// HTTP 401 — token ausente, inválido ou expirado.
    pub fn is_authentication(&self) -> bool {
        self.kind == ErrorKind::Authentication
    }

    /// HTTP 402 — saldo/créditos insuficientes.
    pub fn is_insufficient_balance(&self) -> bool {
        self.kind == ErrorKind::InsufficientBalance
    }

    /// HTTP 403 — sem permissão.
    pub fn is_permission(&self) -> bool {
        self.kind == ErrorKind::Permission
    }

    /// HTTP 404/410 — não encontrado ou desativado.
    pub fn is_not_found(&self) -> bool {
        self.kind == ErrorKind::NotFound
    }

    /// HTTP 429 — rate limit atingido.
    pub fn is_rate_limit(&self) -> bool {
        self.kind == ErrorKind::RateLimit
    }

    /// HTTP 5xx — erro interno do gateway/provedor.
    pub fn is_server(&self) -> bool {
        self.kind == ErrorKind::Server
    }

    /// Mapeia um status HTTP + corpo de erro para o erro adequado,
    /// replicando a hierarquia de erros das demais SDKs.
    pub fn from_api(status: u16, data: &ResponseData, headers: &HashMap<String, String>) -> Self {
        let body = data.to_value();

        Self {
            kind: kind_for_status(status),
            message: extract_message(status, &body),
            status: Some(status),
            code: extract_code(&body),
            response: Some(body),
            retry_after: if kind_for_status(status) == ErrorKind::RateLimit {
                parse_retry_after(headers, SystemTime::now())
            } else {
                None
            },
            source: None,
        }
    }
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.message)?;
        if let Some(status) = self.status {
            write!(f, " (HTTP {status})")?;
        }
        if let Some(code) = &self.code {
            write!(f, " [{code}]")?;
        }
        Ok(())
    }
}

impl fmt::Debug for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Error")
            .field("kind", &self.kind)
            .field("message", &self.message)
            .field("status", &self.status)
            .field("code", &self.code)
            .field("retry_after", &self.retry_after)
            .field("response", &self.response)
            .finish()
    }
}

impl StdError for Error {
    fn source(&self) -> Option<&(dyn StdError + 'static)> {
        self.source
            .as_ref()
            .map(|source| &**source as &(dyn StdError + 'static))
    }
}

fn kind_for_status(status: u16) -> ErrorKind {
    match status {
        400 | 422 => ErrorKind::Validation,
        401 => ErrorKind::Authentication,
        402 => ErrorKind::InsufficientBalance,
        403 => ErrorKind::Permission,
        404 | 410 => ErrorKind::NotFound,
        429 => ErrorKind::RateLimit,
        status if status >= 500 => ErrorKind::Server,
        _ => ErrorKind::Api,
    }
}

fn extract_message(status: u16, body: &Value) -> String {
    for key in ["message", "error"] {
        if let Some(message) = body.get(key).and_then(Value::as_str) {
            if !message.is_empty() {
                return message.to_string();
            }
        }
    }
    format!("A API respondeu com HTTP {status}.")
}

fn extract_code(body: &Value) -> Option<String> {
    Some(body.get("code")?.as_str()?.to_string())
}

/// Lê o header `Retry-After` — segundos ou data HTTP (IMF-fixdate).
pub fn parse_retry_after(headers: &HashMap<String, String>, now: SystemTime) -> Option<Duration> {
    let raw = header(headers, "retry-after")?.trim();
    if raw.is_empty() {
        return None;
    }

    if let Ok(seconds) = raw.parse::<f64>() {
        if seconds <= 0.0 {
            return None;
        }
        return Some(Duration::from_secs_f64(seconds));
    }

    let at = parse_http_date(raw)?;
    let now = now.duration_since(UNIX_EPOCH).ok()?.as_secs() as i64;
    let delta = at - now;
    if delta > 0 {
        Some(Duration::from_secs(delta as u64))
    } else {
        None
    }
}

/// Lê um header sem diferenciar maiúsculas de minúsculas.
pub fn header<'a>(headers: &'a HashMap<String, String>, name: &str) -> Option<&'a str> {
    headers
        .iter()
        .find(|(key, _)| key.eq_ignore_ascii_case(name))
        .map(|(_, value)| value.as_str())
}

/// Converte uma data HTTP (`Wed, 21 Oct 2015 07:28:00 GMT`) em segundos
/// desde a época Unix.
fn parse_http_date(raw: &str) -> Option<i64> {
    // "Wed, 21 Oct 2015 07:28:00 GMT"
    let raw = raw.split_once(", ").map(|(_, rest)| rest).unwrap_or(raw);
    let mut parts = raw.split_whitespace();

    let day: i64 = parts.next()?.parse().ok()?;
    let month = match parts.next()? {
        "Jan" => 1,
        "Feb" => 2,
        "Mar" => 3,
        "Apr" => 4,
        "May" => 5,
        "Jun" => 6,
        "Jul" => 7,
        "Aug" => 8,
        "Sep" => 9,
        "Oct" => 10,
        "Nov" => 11,
        "Dec" => 12,
        _ => return None,
    };
    let year: i64 = parts.next()?.parse().ok()?;

    let mut clock = parts.next()?.split(':');
    let hour: i64 = clock.next()?.parse().ok()?;
    let minute: i64 = clock.next()?.parse().ok()?;
    let second: i64 = clock.next()?.parse().ok()?;

    Some(days_from_civil(year, month, day) * 86_400 + hour * 3_600 + minute * 60 + second)
}

/// Dias desde 1970-01-01 (algoritmo `days_from_civil` de Howard Hinnant).
fn days_from_civil(year: i64, month: i64, day: i64) -> i64 {
    let year = if month <= 2 { year - 1 } else { year };
    let era = if year >= 0 { year } else { year - 399 } / 400;
    let year_of_era = year - era * 400;
    let day_of_year = (153 * (if month > 2 { month - 3 } else { month + 9 }) + 2) / 5 + day - 1;
    let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
    era * 146_097 + day_of_era - 719_468
}