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};
pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
#[derive(Clone, Debug, PartialEq)]
pub struct TransportRequest {
pub method: Method,
pub url: String,
pub headers: HashMap<String, String>,
pub body: Option<Vec<u8>>,
pub timeout: Option<Duration>,
pub response_type: ResponseType,
}
impl TransportRequest {
pub fn json_body(&self) -> Option<Value> {
serde_json::from_slice(self.body.as_deref()?).ok()
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct TransportResponse {
pub status: u16,
pub headers: HashMap<String, String>,
pub data: ResponseData,
pub body: Vec<u8>,
}
impl TransportResponse {
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,
}
}
pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.insert(name.into(), value.into());
self
}
}
pub trait Transport: Send + Sync {
fn execute<'a>(&'a self, request: TransportRequest)
-> BoxFuture<'a, Result<TransportResponse>>;
}
#[derive(Clone, Debug, Default)]
pub struct ReqwestTransport {
client: reqwest::Client,
}
impl ReqwestTransport {
pub fn new() -> Self {
Self::default()
}
pub fn with_client(client: reqwest::Client) -> Self {
Self { client }
}
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}"))
}
}
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")
}
}