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,
};
pub const DEFAULT_BASE_URL: &str = "https://gateway.apibrasil.io/api/v2";
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
pub const USER_AGENT: &str = "APIBRASIL/SDK-RUST";
#[derive(Default)]
struct Tokens {
bearer: Option<String>,
device: Option<String>,
}
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 {
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),
}),
}
}
pub fn base_url(&self) -> &str {
&self.base_url
}
pub fn url(&self, path: &str) -> String {
join_url(&self.base_url, path)
}
pub fn transport(&self) -> &Arc<dyn Transport> {
&self.transport
}
pub fn retry(&self) -> &RetryConfig {
&self.retry
}
pub fn timeout(&self) -> Duration {
self.timeout
}
pub fn secret_key(&self) -> Option<String> {
self.secret_key.clone()
}
pub fn bearer_token(&self) -> Option<String> {
self.tokens.read().expect("tokens").bearer.clone()
}
pub fn set_bearer_token(&self, token: Option<String>) {
self.tokens.write().expect("tokens").bearer = token.filter(|t| !t.is_empty());
}
pub fn device_token(&self) -> Option<String> {
self.tokens.read().expect("tokens").device.clone()
}
pub fn set_device_token(&self, token: Option<String>) {
self.tokens.write().expect("tokens").device = token.filter(|t| !t.is_empty());
}
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(),
}
}
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) => {
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);
}
}
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())
}
pub async fn get(&self, path: &str, options: &RequestOptions) -> Result<Json> {
self.request_json(Method::Get, path, None, options).await
}
pub async fn post(
&self,
path: &str,
body: Option<Json>,
options: &RequestOptions,
) -> Result<Json> {
self.request_json(Method::Post, path, body, options).await
}
pub async fn put(
&self,
path: &str,
body: Option<Json>,
options: &RequestOptions,
) -> Result<Json> {
self.request_json(Method::Put, path, body, options).await
}
pub async fn patch(
&self,
path: &str,
body: Option<Json>,
options: &RequestOptions,
) -> Result<Json> {
self.request_json(Method::Patch, path, body, options).await
}
pub async fn delete(
&self,
path: &str,
body: Option<Json>,
options: &RequestOptions,
) -> Result<Json> {
self.request_json(Method::Delete, path, body, options).await
}
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())
}
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)
}),
}
}