use reqwest::{Client, Method, RequestBuilder};
use serde::de::DeserializeOwned;
use serde::Serialize;
use crate::config::{Config, Operation};
use crate::error::{self, Error};
#[derive(Debug, Clone)]
pub struct HttpTransport {
client: Client,
stream_client: Client,
config: Config,
}
impl HttpTransport {
pub fn new(config: Config) -> error::Result<Self> {
let mut default_headers = reqwest::header::HeaderMap::new();
default_headers.insert(
reqwest::header::CONTENT_TYPE,
reqwest::header::HeaderValue::from_static("application/json"),
);
for (name, value) in &config.headers {
let header_name = reqwest::header::HeaderName::from_bytes(name.as_bytes())
.map_err(|e| Error::Configuration(format!("invalid header name '{name}': {e}")))?;
let header_value = value.parse().map_err(|e| {
Error::Configuration(format!("invalid header value for '{name}': {e}"))
})?;
default_headers.insert(header_name, header_value);
}
let client = Client::builder()
.timeout(config.timeout())
.default_headers(default_headers.clone())
.build()
.map_err(|e| Error::Configuration(format!("failed to build HTTP client: {e}")))?;
let stream_client = Client::builder()
.connect_timeout(config.timeout())
.default_headers(default_headers)
.build()
.map_err(|e| Error::Configuration(format!("failed to build HTTP client: {e}")))?;
Ok(Self {
client,
stream_client,
config,
})
}
pub async fn request_data<T: DeserializeOwned>(
&self,
method: Method,
path: &str,
operation: Operation,
body: Option<&(impl Serialize + ?Sized)>,
) -> error::Result<T> {
let envelope: crate::admin::types::ApiResponse<T> =
self.request(method, path, operation, body).await?;
Ok(envelope.data)
}
pub async fn request<T: DeserializeOwned>(
&self,
method: Method,
path: &str,
operation: Operation,
body: Option<&(impl Serialize + ?Sized)>,
) -> error::Result<T> {
let url = self
.config
.base_url
.join(path)
.map_err(|e| Error::Configuration(format!("invalid path '{path}': {e}")))?;
let api_key = self.config.api_key_for(operation)?;
let mut req: RequestBuilder = self.client.request(method, url.as_str());
req = req.header("Authorization", api_key);
if let Some(b) = body {
req = req.json(b);
}
let response = req.send().await.map_err(|e| {
if e.is_timeout() {
Error::Network("request timed out".into())
} else if e.is_connect() {
Error::Network(format!("connection failed: {e}"))
} else {
Error::from(e)
}
})?;
let status = response.status().as_u16();
if !response.status().is_success() {
let body_text = response.text().await.unwrap_or_default();
return Err(error::map_status_error(status, body_text));
}
response.json::<T>().await.map_err(Error::from)
}
pub async fn request_no_content(
&self,
method: Method,
path: &str,
operation: Operation,
body: Option<&(impl Serialize + ?Sized)>,
) -> error::Result<()> {
let url = self
.config
.base_url
.join(path)
.map_err(|e| Error::Configuration(format!("invalid path '{path}': {e}")))?;
let api_key = self.config.api_key_for(operation)?;
let mut req = self.client.request(method, url.as_str());
req = req.header("Authorization", api_key);
if let Some(b) = body {
req = req.json(b);
}
let response = req.send().await.map_err(|e| {
if e.is_timeout() {
Error::Network("request timed out".into())
} else if e.is_connect() {
Error::Network(format!("connection failed: {e}"))
} else {
Error::from(e)
}
})?;
let status = response.status().as_u16();
if !response.status().is_success() {
let body_text = response.text().await.unwrap_or_default();
return Err(error::map_status_error(status, body_text));
}
Ok(())
}
pub async fn request_text(
&self,
method: Method,
path: &str,
operation: Operation,
body: Option<&(impl Serialize + ?Sized)>,
) -> error::Result<String> {
let url = self
.config
.base_url
.join(path)
.map_err(|e| Error::Configuration(format!("invalid path '{path}': {e}")))?;
let api_key = self.config.api_key_for(operation)?;
let mut req = self.client.request(method, url.as_str());
req = req.header("Authorization", api_key);
if let Some(b) = body {
req = req.json(b);
}
let response = req.send().await.map_err(|e| {
if e.is_timeout() {
Error::Network("request timed out".into())
} else if e.is_connect() {
Error::Network(format!("connection failed: {e}"))
} else {
Error::from(e)
}
})?;
let status = response.status().as_u16();
if !response.status().is_success() {
let body_text = response.text().await.unwrap_or_default();
return Err(error::map_status_error(status, body_text));
}
response.text().await.map_err(Error::from)
}
pub(crate) async fn request_stream(
&self,
method: Method,
path: &str,
operation: Operation,
body: Option<&(impl Serialize + ?Sized)>,
) -> error::Result<reqwest::Response> {
let url = self
.config
.base_url
.join(path)
.map_err(|e| Error::Configuration(format!("invalid path '{path}': {e}")))?;
let api_key = self.config.api_key_for(operation)?;
let mut req = self.stream_client.request(method, url.as_str());
req = req.header("Authorization", api_key);
req = req.header(reqwest::header::ACCEPT, "text/event-stream");
if let Some(b) = body {
req = req.json(b);
}
let response = req.send().await.map_err(|e| {
if e.is_timeout() {
Error::Network("request timed out".into())
} else if e.is_connect() {
Error::Network(format!("connection failed: {e}"))
} else {
Error::from(e)
}
})?;
let status = response.status().as_u16();
if !response.status().is_success() {
let body_text = response.text().await.unwrap_or_default();
return Err(error::map_status_error(status, body_text));
}
Ok(response)
}
#[must_use]
pub fn config(&self) -> &Config {
&self.config
}
}