use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use reqwest::cookie::Jar;
use tracing::{debug, info};
use super::{APIContextOptions, APIError, APIRequestBuilder, HttpMethod};
#[derive(Debug)]
pub struct APIRequestContext {
client: Arc<reqwest::Client>,
cookie_jar: Arc<Jar>,
options: APIContextOptions,
disposed: Arc<AtomicBool>,
}
impl APIRequestContext {
pub async fn new(options: APIContextOptions) -> Result<Self, APIError> {
info!("Creating standalone APIRequestContext");
let cookie_jar = Arc::new(Jar::default());
let client = Self::build_client(&options, Arc::clone(&cookie_jar))?;
Ok(Self {
client: Arc::new(client),
cookie_jar,
options,
disposed: Arc::new(AtomicBool::new(false)),
})
}
pub(crate) async fn with_shared_cookies(
options: APIContextOptions,
cookie_jar: Arc<Jar>,
) -> Result<Self, APIError> {
debug!("Creating APIRequestContext with shared cookie jar");
let client = Self::build_client(&options, Arc::clone(&cookie_jar))?;
Ok(Self {
client: Arc::new(client),
cookie_jar,
options,
disposed: Arc::new(AtomicBool::new(false)),
})
}
fn build_client(
options: &APIContextOptions,
cookie_jar: Arc<Jar>,
) -> Result<reqwest::Client, APIError> {
let mut builder = reqwest::Client::builder().cookie_provider(cookie_jar);
if let Some(timeout) = options.timeout {
builder = builder.timeout(timeout);
}
if let Some(ref user_agent) = options.user_agent {
builder = builder.user_agent(user_agent);
}
if options.ignore_https_errors {
builder = builder.danger_accept_invalid_certs(true);
}
if let Some(ref proxy_config) = options.proxy {
let mut proxy = reqwest::Proxy::all(&proxy_config.server)
.map_err(|e| APIError::BuildError(format!("Invalid proxy URL: {e}")))?;
if let (Some(username), Some(password)) =
(&proxy_config.username, &proxy_config.password)
{
proxy = proxy.basic_auth(username, password);
}
builder = builder.proxy(proxy);
}
builder
.build()
.map_err(|e| APIError::BuildError(e.to_string()))
}
fn default_headers(&self) -> Vec<(String, String)> {
let mut headers: Vec<(String, String)> = self
.options
.extra_http_headers
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
if let Some(ref creds) = self.options.http_credentials {
use base64::Engine;
let auth_string = format!("{}:{}", creds.username, creds.password);
let encoded = base64::engine::general_purpose::STANDARD.encode(auth_string);
headers.push(("Authorization".to_string(), format!("Basic {encoded}")));
}
headers
}
pub fn get(&self, url: impl Into<String>) -> APIRequestBuilder {
self.request(HttpMethod::Get, url)
}
pub fn post(&self, url: impl Into<String>) -> APIRequestBuilder {
self.request(HttpMethod::Post, url)
}
pub fn put(&self, url: impl Into<String>) -> APIRequestBuilder {
self.request(HttpMethod::Put, url)
}
pub fn patch(&self, url: impl Into<String>) -> APIRequestBuilder {
self.request(HttpMethod::Patch, url)
}
pub fn delete(&self, url: impl Into<String>) -> APIRequestBuilder {
self.request(HttpMethod::Delete, url)
}
pub fn head(&self, url: impl Into<String>) -> APIRequestBuilder {
self.request(HttpMethod::Head, url)
}
pub fn fetch(&self, method: HttpMethod, url: impl Into<String>) -> APIRequestBuilder {
self.request(method, url)
}
fn request(&self, method: HttpMethod, url: impl Into<String>) -> APIRequestBuilder {
let mut builder = APIRequestBuilder::new(
Arc::clone(&self.client),
method,
url,
self.options.base_url.clone(),
self.default_headers(),
);
if self.disposed.load(Ordering::SeqCst) {
builder.set_disposed();
}
builder
}
pub async fn dispose(&self) {
info!("Disposing APIRequestContext");
self.disposed.store(true, Ordering::SeqCst);
}
pub fn is_disposed(&self) -> bool {
self.disposed.load(Ordering::SeqCst)
}
pub fn base_url(&self) -> Option<&str> {
self.options.base_url.as_deref()
}
pub fn cookie_jar(&self) -> &Arc<Jar> {
&self.cookie_jar
}
}
impl Clone for APIRequestContext {
fn clone(&self) -> Self {
Self {
client: Arc::clone(&self.client),
cookie_jar: Arc::clone(&self.cookie_jar),
options: self.options.clone(),
disposed: Arc::clone(&self.disposed),
}
}
}
#[cfg(test)]
mod tests;