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
//! Camada de transporte HTTP plugável.
//!
//! A implementação padrão é [`ReqwestTransport`]; injete a sua para usar
//! proxies corporativos, instrumentação ou mocks de teste.

use std::collections::HashMap;
use std::fmt;
use std::future::Future;
use std::pin::Pin;
use std::time::Duration;

use serde_json::Value;

use super::errors::{Error, Result};
use super::types::{Method, ResponseData, ResponseType};

/// Future devolvido por [`Transport::execute`].
pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;

/// Requisição entregue à camada de transporte.
#[derive(Clone, Debug, PartialEq)]
pub struct TransportRequest {
    /// Verbo HTTP da requisição.
    pub method: Method,
    /// URL absoluta, já com query string.
    pub url: String,
    /// Headers da requisição, incluindo os de autenticação.
    pub headers: HashMap<String, String>,
    /// Body já serializado (JSON). `None` quando não há corpo.
    pub body: Option<Vec<u8>>,
    /// Tempo limite desta requisição.
    pub timeout: Option<Duration>,
    /// Como o corpo da resposta deve ser decodificado.
    pub response_type: ResponseType,
}

impl TransportRequest {
    /// Devolve o body decodificado como JSON, quando houver.
    pub fn json_body(&self) -> Option<Value> {
        serde_json::from_slice(self.body.as_deref()?).ok()
    }
}

/// Resposta devolvida pela camada de transporte.
#[derive(Clone, Debug, PartialEq)]
pub struct TransportResponse {
    /// Status HTTP devolvido pelo servidor.
    pub status: u16,
    /// Headers da resposta, com os nomes em minúsculas.
    pub headers: HashMap<String, String>,
    /// Corpo já decodificado conforme o [`ResponseType`] pedido.
    pub data: ResponseData,
    /// Bytes crus da resposta.
    pub body: Vec<u8>,
}

impl TransportResponse {
    /// Monta uma resposta com status e corpo JSON — atalho para testes e
    /// transportes customizados.
    pub fn json(status: u16, data: Value) -> Self {
        let body = serde_json::to_vec(&data).unwrap_or_default();
        Self {
            status,
            headers: HashMap::new(),
            data: ResponseData::Value(data),
            body,
        }
    }

    /// Adiciona um header à resposta.
    pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
        self.headers.insert(name.into(), value.into());
        self
    }
}

/// Camada de transporte HTTP da SDK.
///
/// Contrato: devolve a resposta para **qualquer** status HTTP; devolve
/// erro ([`ErrorKind::Network`](super::errors::ErrorKind::Network) ou
/// [`Timeout`](super::errors::ErrorKind::Timeout)) apenas quando não
/// houve resposta.
///
/// ```
/// use apibrasil::core::transport::{BoxFuture, Transport, TransportRequest, TransportResponse};
/// use apibrasil::Result;
/// use serde_json::json;
///
/// struct MeuTransporte;
///
/// impl Transport for MeuTransporte {
///     fn execute<'a>(&'a self, request: TransportRequest) -> BoxFuture<'a, Result<TransportResponse>> {
///         Box::pin(async move {
///             let _ = request.url;
///             Ok(TransportResponse::json(200, json!({ "ok": true })))
///         })
///     }
/// }
/// ```
pub trait Transport: Send + Sync {
    /// Executa a requisição HTTP.
    fn execute<'a>(&'a self, request: TransportRequest)
        -> BoxFuture<'a, Result<TransportResponse>>;
}

/// Transporte padrão, baseado no [`reqwest`].
///
/// Informe o seu [`reqwest::Client`] para configurar proxy, TLS, pool de
/// conexões etc.
#[derive(Clone, Debug, Default)]
pub struct ReqwestTransport {
    client: reqwest::Client,
}

impl ReqwestTransport {
    /// Cria o transporte padrão.
    pub fn new() -> Self {
        Self::default()
    }

    /// Cria o transporte sobre um [`reqwest::Client`] já configurado.
    pub fn with_client(client: reqwest::Client) -> Self {
        Self { client }
    }

    /// Devolve o cliente HTTP em uso.
    pub fn client(&self) -> &reqwest::Client {
        &self.client
    }
}

impl Transport for ReqwestTransport {
    fn execute<'a>(
        &'a self,
        request: TransportRequest,
    ) -> BoxFuture<'a, Result<TransportResponse>> {
        Box::pin(async move {
            let method = reqwest::Method::from_bytes(request.method.as_str().as_bytes()).map_err(
                |error| {
                    Error::network(format!(
                        "Verbo HTTP inválido em {} {}: {error}",
                        request.method, request.url
                    ))
                    .with_source(error)
                },
            )?;

            let mut builder = self.client.request(method, &request.url);
            for (name, value) in &request.headers {
                builder = builder.header(name, value);
            }
            if let Some(timeout) = request.timeout {
                builder = builder.timeout(timeout);
            }
            if let Some(body) = request.body.clone() {
                builder = builder.body(body);
            }

            let response = builder.send().await.map_err(|error| {
                transport_error(&error, &request, "Falha de rede", "Tempo limite excedido")
            })?;

            let status = response.status().as_u16();
            let headers = response
                .headers()
                .iter()
                .map(|(name, value)| {
                    (
                        name.as_str().to_ascii_lowercase(),
                        value.to_str().unwrap_or_default().to_string(),
                    )
                })
                .collect();

            let raw = response.bytes().await.map_err(|error| {
                transport_error(
                    &error,
                    &request,
                    "Falha ao ler a resposta",
                    "Tempo limite excedido ao ler a resposta",
                )
            })?;
            let raw = raw.to_vec();

            Ok(TransportResponse {
                status,
                headers,
                data: decode_body(&raw, request.response_type),
                body: raw,
            })
        })
    }
}

fn transport_error(
    error: &reqwest::Error,
    request: &TransportRequest,
    network_prefix: &str,
    timeout_prefix: &str,
) -> Error {
    let target = format!("{} {}", request.method, request.url);
    if error.is_timeout() {
        Error::timeout(format!("{timeout_prefix} em {target}: {error}"))
    } else {
        Error::network(format!("{network_prefix} em {target}: {error}"))
    }
}

/// Decodifica o corpo cru conforme o [`ResponseType`]: JSON vira
/// [`Value`], corpos não-JSON viram texto e [`ResponseType::Bytes`]
/// devolve os próprios bytes.
pub fn decode_body(raw: &[u8], response_type: ResponseType) -> ResponseData {
    if response_type == ResponseType::Bytes {
        return ResponseData::Bytes(raw.to_vec());
    }
    if raw.is_empty() {
        return ResponseData::Value(Value::Null);
    }

    match serde_json::from_slice::<Value>(raw) {
        Ok(value) => ResponseData::Value(value),
        Err(_) => ResponseData::Value(Value::String(String::from_utf8_lossy(raw).into_owned())),
    }
}

impl fmt::Debug for dyn Transport {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("Transport")
    }
}