use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use crate::error::{Error, Result};
use crate::options::APIRequestOptions;
use crate::types::Headers;
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct ApiRequestContextOptions {
pub base_url: Option<String>,
pub user_agent: Option<String>,
pub timeout: Option<Duration>,
pub ignore_https_errors: bool,
pub extra_http_headers: Headers,
}
impl ApiRequestContextOptions {
pub fn new() -> Self {
Self::default()
}
pub fn base_url(mut self, url: impl Into<String>) -> Self {
self.base_url = Some(url.into());
self
}
pub fn user_agent(mut self, ua: impl Into<String>) -> Self {
self.user_agent = Some(ua.into());
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn ignore_https_errors(mut self, ignore: bool) -> Self {
self.ignore_https_errors = ignore;
self
}
pub fn extra_http_headers(mut self, headers: Headers) -> Self {
self.extra_http_headers = headers;
self
}
pub fn extra_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.extra_http_headers.insert(name.into(), value.into());
self
}
}
#[derive(Clone)]
pub struct APIRequestContext {
client: reqwest::Client,
default_headers: Headers,
options: ApiRequestContextOptions,
}
impl APIRequestContext {
pub fn new(default_headers: Headers) -> Self {
let mut options = ApiRequestContextOptions::default();
options.extra_http_headers = default_headers;
Self::new_with_options(options)
}
pub fn new_with_options(options: ApiRequestContextOptions) -> Self {
let mut builder = reqwest::Client::builder();
if options.ignore_https_errors {
builder = builder.danger_accept_invalid_certs(true);
}
if let Some(timeout) = options.timeout {
builder = builder.timeout(timeout);
}
if let Some(user_agent) = options.user_agent.as_deref() {
builder = builder.user_agent(user_agent);
}
let client = builder
.build()
.unwrap_or_else(|_| reqwest::Client::new());
let default_headers = options.extra_http_headers.clone();
Self {
client,
default_headers,
options,
}
}
pub fn default_headers(&self) -> &Headers {
&self.default_headers
}
pub fn options(&self) -> &ApiRequestContextOptions {
&self.options
}
pub async fn get(&self, url: &str, options: Option<APIRequestOptions>) -> Result<APIResponse> {
self.send(reqwest::Method::GET, url, options).await
}
pub async fn post(&self, url: &str, options: Option<APIRequestOptions>) -> Result<APIResponse> {
self.send(reqwest::Method::POST, url, options).await
}
pub async fn put(&self, url: &str, options: Option<APIRequestOptions>) -> Result<APIResponse> {
self.send(reqwest::Method::PUT, url, options).await
}
pub async fn patch(&self, url: &str, options: Option<APIRequestOptions>) -> Result<APIResponse> {
self.send(reqwest::Method::PATCH, url, options).await
}
pub async fn delete(
&self,
url: &str,
options: Option<APIRequestOptions>,
) -> Result<APIResponse> {
self.send(reqwest::Method::DELETE, url, options).await
}
pub async fn head(&self, url: &str, options: Option<APIRequestOptions>) -> Result<APIResponse> {
self.send(reqwest::Method::HEAD, url, options).await
}
fn resolve_url(&self, url: &str) -> String {
let Some(base) = self.options.base_url.as_deref() else {
return url.to_string();
};
if url.starts_with("http://") || url.starts_with("https://") {
return url.to_string();
}
let base = base.trim_end_matches('/');
let path = if url.starts_with('/') {
url.to_string()
} else {
format!("/{url}")
};
format!("{base}{path}")
}
async fn send(
&self,
method: reqwest::Method,
url: &str,
options: Option<APIRequestOptions>,
) -> Result<APIResponse> {
let options = options.unwrap_or_default();
let url = self.resolve_url(url);
let mut builder = self.client.request(method, &url);
for (k, v) in &self.default_headers {
builder = builder.header(k.as_str(), v.as_str());
}
if let Some(headers) = options.headers.as_ref() {
for (k, v) in headers {
builder = builder.header(k.as_str(), v.as_str());
}
}
if let Some(params) = options.params.as_ref() {
builder = builder.query(¶ms);
}
if let Some(data) = options.data.as_ref() {
builder = builder.json(data);
} else if let Some(form) = options.form.as_ref() {
builder = builder.form(form);
}
if let Some(timeout_ms) = options.timeout {
builder = builder.timeout(Duration::from_millis(timeout_ms.max(0.0) as u64));
}
let resp = builder
.send()
.await
.map_err(|e| Error::Http(format!("request failed: {e}")))?;
let url = resp.url().to_string();
let status = resp.status().as_u16();
let mut headers: Headers = HashMap::with_capacity(resp.headers().len());
for (name, value) in resp.headers().iter() {
let key = name.as_str().to_ascii_lowercase();
let val = match value.to_str() {
Ok(s) => s.to_string(),
Err(_) => {
String::from_utf8_lossy(value.as_bytes()).into_owned()
}
};
headers.insert(key, val);
}
let body = resp
.bytes()
.await
.map_err(|e| Error::Http(format!("failed to read body: {e}")))?;
Ok(APIResponse {
url,
status,
headers,
body: Arc::from(body.as_ref()),
})
}
}
#[derive(Debug, Clone)]
pub struct APIResponse {
url: String,
status: u16,
headers: Headers,
body: Arc<[u8]>,
}
impl APIResponse {
pub fn url(&self) -> &str {
&self.url
}
pub fn status(&self) -> u16 {
self.status
}
pub fn ok(&self) -> bool {
(200..300).contains(&self.status)
}
pub fn headers(&self) -> &Headers {
&self.headers
}
pub async fn body(&self) -> Result<Vec<u8>> {
Ok(self.body.to_vec())
}
pub async fn text(&self) -> Result<String> {
Ok(String::from_utf8_lossy(&self.body).into_owned())
}
pub async fn json(&self) -> Result<serde_json::Value> {
serde_json::from_slice(&self.body).map_err(Into::into)
}
}