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
//! Cliente HTTP interno da SDK.
//!
//! Injeta os headers de autenticação da plataforma (`Authorization:
//! Bearer`, `DeviceToken`, `SecretKey`), aplica retry com backoff,
//! dispara os hooks de observabilidade e converte falhas em [`Error`].

use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::time::{Duration, Instant};

use serde::de::DeserializeOwned;
use serde_json::Value;

use super::errors::{Error, Result};
use super::retry::{sleep, RetryConfig};
use super::transport::{ReqwestTransport, Transport, TransportRequest};
use super::types::{
    build_query_string, decode_value, join_url, Config, Hooks, Json, Method, RequestHookInfo,
    RequestOptions, ResponseData, ResponseHookInfo, ResponseType, RetryHookInfo,
};

/// Base da API.
pub const DEFAULT_BASE_URL: &str = "https://gateway.apibrasil.io/api/v2";
/// Tempo limite das requisições.
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
/// Identificação da SDK nas requisições.
pub const USER_AGENT: &str = "APIBRASIL/SDK-RUST";

#[derive(Default)]
struct Tokens {
    bearer: Option<String>,
    device: Option<String>,
}

/// Cliente HTTP compartilhado por todos os serviços.
///
/// É seguro para uso concorrente: use `Arc<HttpClient>` e um
/// [`HttpClient::set_bearer_token`] vale para todos os serviços.
pub struct HttpClient {
    base_url: String,
    timeout: Duration,
    headers: HashMap<String, String>,
    secret_key: Option<String>,
    transport: Arc<dyn Transport>,
    retry: RetryConfig,
    hooks: Option<Arc<dyn Hooks>>,
    tokens: RwLock<Tokens>,
}

impl HttpClient {
    /// Cria o cliente HTTP resolvendo a configuração com as variáveis de
    /// ambiente — os campos informados em `config` têm prioridade.
    ///
    /// Credenciais vazias contam como ausentes: informar `""` é a forma de
    /// desligar o que veio do ambiente.
    pub fn new(config: Config) -> Self {
        let resolved = Config::from_env().merge(config);

        Self {
            base_url: present(resolved.base_url).unwrap_or_else(|| DEFAULT_BASE_URL.to_string()),
            timeout: resolved.timeout.unwrap_or(DEFAULT_TIMEOUT),
            headers: resolved.headers,
            secret_key: present(resolved.secret_key),
            transport: resolved
                .transport
                .unwrap_or_else(|| Arc::new(ReqwestTransport::new())),
            retry: resolved.retry.unwrap_or_default(),
            hooks: resolved.hooks,
            tokens: RwLock::new(Tokens {
                bearer: present(resolved.bearer_token),
                device: present(resolved.device_token),
            }),
        }
    }

    /// Base da API em uso.
    pub fn base_url(&self) -> &str {
        &self.base_url
    }

    /// Monta a URL completa de um caminho.
    pub fn url(&self, path: &str) -> String {
        join_url(&self.base_url, path)
    }

    /// Transporte HTTP em uso.
    pub fn transport(&self) -> &Arc<dyn Transport> {
        &self.transport
    }

    /// Política de retry em uso.
    pub fn retry(&self) -> &RetryConfig {
        &self.retry
    }

    /// Tempo limite padrão das requisições.
    pub fn timeout(&self) -> Duration {
        self.timeout
    }

    /// SecretKey configurada (usada em `Devices::store`).
    pub fn secret_key(&self) -> Option<String> {
        self.secret_key.clone()
    }

    /// Bearer Token atual.
    pub fn bearer_token(&self) -> Option<String> {
        self.tokens.read().expect("tokens").bearer.clone()
    }

    /// Define/atualiza o Bearer Token — `None` remove a autenticação.
    pub fn set_bearer_token(&self, token: Option<String>) {
        self.tokens.write().expect("tokens").bearer = token.filter(|t| !t.is_empty());
    }

    /// DeviceToken atual.
    pub fn device_token(&self) -> Option<String> {
        self.tokens.read().expect("tokens").device.clone()
    }

    /// Define/atualiza o DeviceToken — `None` remove o header.
    pub fn set_device_token(&self, token: Option<String>) {
        self.tokens.write().expect("tokens").device = token.filter(|t| !t.is_empty());
    }

    /// Configuração atual do cliente, já resolvida com o ambiente e com
    /// os tokens em vigor.
    pub fn config(&self) -> Config {
        Config {
            bearer_token: self.bearer_token(),
            device_token: self.device_token(),
            secret_key: self.secret_key.clone(),
            base_url: Some(self.base_url.clone()),
            timeout: Some(self.timeout),
            headers: self.headers.clone(),
            transport: Some(self.transport.clone()),
            retry: Some(self.retry.clone()),
            hooks: self.hooks.clone(),
        }
    }

    /// Executa uma requisição e devolve o corpo já decodificado.
    pub async fn execute(
        &self,
        method: Method,
        path: &str,
        body: Option<Json>,
        options: &RequestOptions,
    ) -> Result<ResponseData> {
        let headers = self.build_headers(options);
        let target = format!(
            "{}{}",
            self.url(path),
            build_query_string(options.query.as_ref())
        );

        let payload = serialize_body(body.as_ref())?;
        let timeout = options.timeout.unwrap_or(self.timeout);
        let max_attempts = self.retry.max_attempts();

        let mut attempt = 0u32;
        loop {
            if let Some(hooks) = &self.hooks {
                hooks.on_request(&RequestHookInfo {
                    method,
                    url: &target,
                    headers: &headers,
                    body: body.as_ref(),
                    attempt,
                });
            }

            let started_at = Instant::now();
            let result = self
                .transport
                .execute(TransportRequest {
                    method,
                    url: target.clone(),
                    headers: headers.clone(),
                    body: payload.clone(),
                    timeout: Some(timeout),
                    response_type: options.response_type,
                })
                .await;

            let response = match result {
                Ok(response) => response,
                Err(error) => {
                    // Timeouts nunca são refeitos: a requisição pode ter
                    // sido processada e o retry duplicaria
                    // cobranças/envios.
                    let retryable = error.is_network() && !error.is_timeout();
                    if retryable && attempt + 1 < max_attempts {
                        let delay = self.retry.backoff_delay(attempt);
                        attempt += 1;
                        self.notify_retry(method, &target, attempt, delay, &error.message);
                        sleep(delay).await;
                        continue;
                    }
                    return Err(error);
                }
            };

            if let Some(hooks) = &self.hooks {
                hooks.on_response(&ResponseHookInfo {
                    method,
                    url: &target,
                    status: response.status,
                    duration: started_at.elapsed(),
                    attempt,
                });
            }

            if response.status >= 400 {
                let error = Error::from_api(response.status, &response.data, &response.headers);
                if self.retry.retries_status(response.status) && attempt + 1 < max_attempts {
                    let delay = error
                        .retry_after
                        .unwrap_or_else(|| self.retry.backoff_delay(attempt));
                    attempt += 1;
                    let reason = format!("HTTP {}", response.status);
                    self.notify_retry(method, &target, attempt, delay, &reason);
                    sleep(delay).await;
                    continue;
                }
                return Err(error);
            }

            return Ok(response.data);
        }
    }

    /// Executa a requisição e devolve o corpo como objeto JSON.
    pub async fn request_json(
        &self,
        method: Method,
        path: &str,
        body: Option<Json>,
        options: &RequestOptions,
    ) -> Result<Json> {
        Ok(self
            .execute(method, path, body, options)
            .await?
            .into_json_object())
    }

    /// `GET path`.
    pub async fn get(&self, path: &str, options: &RequestOptions) -> Result<Json> {
        self.request_json(Method::Get, path, None, options).await
    }

    /// `POST path`.
    pub async fn post(
        &self,
        path: &str,
        body: Option<Json>,
        options: &RequestOptions,
    ) -> Result<Json> {
        self.request_json(Method::Post, path, body, options).await
    }

    /// `PUT path`.
    pub async fn put(
        &self,
        path: &str,
        body: Option<Json>,
        options: &RequestOptions,
    ) -> Result<Json> {
        self.request_json(Method::Put, path, body, options).await
    }

    /// `PATCH path`.
    pub async fn patch(
        &self,
        path: &str,
        body: Option<Json>,
        options: &RequestOptions,
    ) -> Result<Json> {
        self.request_json(Method::Patch, path, body, options).await
    }

    /// `DELETE path`.
    pub async fn delete(
        &self,
        path: &str,
        body: Option<Json>,
        options: &RequestOptions,
    ) -> Result<Json> {
        self.request_json(Method::Delete, path, body, options).await
    }

    /// Baixa o corpo cru (PDF de boleto, imagens...).
    pub async fn bytes(
        &self,
        method: Method,
        path: &str,
        body: Option<Json>,
        options: &RequestOptions,
    ) -> Result<Vec<u8>> {
        let mut options = options.clone();
        options.response_type = ResponseType::Bytes;

        Ok(self
            .execute(method, path, body, &options)
            .await?
            .into_bytes())
    }

    /// Executa a requisição e decodifica a resposta em `T`.
    pub async fn request_into<T: DeserializeOwned>(
        &self,
        method: Method,
        path: &str,
        body: Option<Json>,
        options: &RequestOptions,
    ) -> Result<T> {
        decode_value(
            self.execute(method, path, body, options)
                .await?
                .into_value(),
        )
    }

    fn build_headers(&self, options: &RequestOptions) -> HashMap<String, String> {
        let mut headers = HashMap::with_capacity(6 + self.headers.len() + options.headers.len());
        headers.insert("Content-Type".to_string(), "application/json".to_string());
        headers.insert("Accept".to_string(), "application/json".to_string());
        headers.insert("User-Agent".to_string(), USER_AGENT.to_string());

        for (name, value) in &self.headers {
            headers.insert(name.clone(), value.clone());
        }

        if let Some(token) = options.bearer_token.clone().or_else(|| self.bearer_token()) {
            headers.insert("Authorization".to_string(), format!("Bearer {token}"));
        }
        if let Some(token) = options.device_token.clone().or_else(|| self.device_token()) {
            headers.insert("DeviceToken".to_string(), token);
        }
        if let Some(key) = &options.secret_key {
            headers.insert("SecretKey".to_string(), key.clone());
        }

        for (name, value) in &options.headers {
            headers.insert(name.clone(), value.clone());
        }

        headers
    }

    fn notify_retry(&self, method: Method, url: &str, attempt: u32, delay: Duration, reason: &str) {
        if let Some(hooks) = &self.hooks {
            hooks.on_retry(&RetryHookInfo {
                method,
                url,
                attempt,
                delay,
                reason,
            });
        }
    }
}

impl std::fmt::Debug for HttpClient {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("HttpClient")
            .field("base_url", &self.base_url)
            .field("timeout", &self.timeout)
            .field("retry", &self.retry)
            .field("authenticated", &self.bearer_token().is_some())
            .field("device", &self.device_token().is_some())
            .finish()
    }
}

fn present(value: Option<String>) -> Option<String> {
    value.filter(|value| !value.is_empty())
}

fn serialize_body(body: Option<&Json>) -> Result<Option<Vec<u8>>> {
    match body {
        None | Some(Value::Null) => Ok(None),
        Some(value) => serde_json::to_vec(value).map(Some).map_err(|error| {
            Error::validation(format!(
                "Não foi possível serializar o corpo da requisição: {error}"
            ))
            .with_source(error)
        }),
    }
}