use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{MappedMutexGuard, Mutex, MutexGuard};
use crate::auth::Auth;
use crate::config::Config;
use crate::error::{Error, Result};
mod auth_header;
mod builder;
mod call;
mod source;
mod transport;
mod url;
pub use builder::ClientBuilder;
pub use call::Call;
pub(crate) use source::CredentialSource;
pub use transport::{StreamLines, WIRE_TARGET};
pub(crate) use url::render_template_path;
use url::{build_url_for_target, render_template_template};
#[derive(Debug, Clone)]
pub enum RequestTarget {
Template {
path: String,
path_params: HashMap<String, String>,
query: Vec<(String, String)>,
},
RawUrl(String),
}
impl Default for RequestTarget {
fn default() -> Self {
Self::Template {
path: String::new(),
path_params: HashMap::new(),
query: Vec::new(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct RequestOptions {
pub method: String,
pub target: RequestTarget,
pub headers: Vec<String>,
pub data: String,
pub auth_type: String,
pub username: String,
pub no_auth: bool,
pub trace: bool,
}
pub const DEFAULT_TIMEOUT_SECS: u64 = 30;
#[derive(Debug, Clone, Default)]
pub(crate) struct CallOptions {
pub(crate) auth_type: String,
pub(crate) username: String,
pub(crate) trace: bool,
pub(crate) no_auth: bool,
pub(crate) timeout: Option<Duration>,
pub(crate) pagination_token: String,
}
#[derive(Debug, Clone)]
pub struct MultipartOptions {
pub request: RequestOptions,
pub form_fields: std::collections::HashMap<String, String>,
pub file_field: String,
pub file_path: String,
pub file_name: String,
pub file_data: Vec<u8>,
}
#[derive(Clone)]
pub struct Client {
inner: Arc<Inner>,
}
pub const DEFAULT_USER_AGENT: &str = concat!("xdk-rs/", env!("CARGO_PKG_VERSION"));
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RateLimit {
pub limit: Option<u32>,
pub remaining: Option<u32>,
pub reset_at: Option<u64>,
}
impl RateLimit {
fn from_headers(headers: &reqwest::header::HeaderMap) -> Option<Self> {
fn number<T: std::str::FromStr>(
headers: &reqwest::header::HeaderMap,
name: &str,
) -> Option<T> {
headers.get(name)?.to_str().ok()?.trim().parse().ok()
}
let window = Self {
limit: number(headers, "x-rate-limit-limit"),
remaining: number(headers, "x-rate-limit-remaining"),
reset_at: number(headers, "x-rate-limit-reset"),
};
(window.limit.is_some() || window.remaining.is_some() || window.reset_at.is_some())
.then_some(window)
}
}
struct Inner {
base_url: String,
http: reqwest::Client,
credentials: Mutex<CredentialSource>,
timeout: Duration,
user_agent: String,
rate_limit: std::sync::Mutex<Option<RateLimit>>,
}
crate::assert_send_sync!(Client);
impl std::fmt::Debug for Client {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Client")
.field("base_url", &self.inner.base_url)
.field("timeout", &self.inner.timeout)
.field("user_agent", &self.inner.user_agent)
.finish_non_exhaustive()
}
}
impl Client {
pub fn builder() -> ClientBuilder {
ClientBuilder::new()
}
pub fn new(config: &Config, auth: Auth) -> Result<Self> {
Self::with_timeout(config, auth, config.http_timeout_secs)
}
pub fn new_with_user_agent(
config: &Config,
auth: Auth,
user_agent: impl Into<String>,
) -> Result<Self> {
Self::from_source(
config.api_base_url.clone(),
CredentialSource::Store(auth),
Duration::from_secs(config.http_timeout_secs),
user_agent.into(),
)
}
pub fn with_timeout(config: &Config, auth: Auth, timeout_secs: u64) -> Result<Self> {
Self::from_source(
config.api_base_url.clone(),
CredentialSource::Store(auth),
Duration::from_secs(timeout_secs),
DEFAULT_USER_AGENT.to_string(),
)
}
pub(crate) fn from_source(
base_url: String,
credentials: CredentialSource,
timeout: Duration,
user_agent: String,
) -> Result<Self> {
let http = reqwest::Client::builder()
.build()
.map_err(|e| Error::Http(format!("cannot build the HTTP client: {e}")))?;
Ok(Self {
inner: Arc::new(Inner {
base_url,
http,
credentials: Mutex::new(credentials),
timeout,
user_agent,
rate_limit: std::sync::Mutex::new(None),
}),
})
}
#[must_use]
pub fn last_rate_limit(&self) -> Option<RateLimit> {
*self
.inner
.rate_limit
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
pub(crate) fn record_rate_limit(&self, headers: &reqwest::header::HeaderMap) {
if let Some(window) = RateLimit::from_headers(headers) {
*self
.inner
.rate_limit
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(window);
}
}
#[must_use]
pub fn timeout_secs(&self) -> u64 {
self.inner.timeout.as_secs()
}
pub(crate) fn request_timeout(&self) -> Duration {
self.inner.timeout
}
pub(crate) fn http(&self) -> &reqwest::Client {
&self.inner.http
}
pub(crate) async fn credentials(&self) -> MutexGuard<'_, CredentialSource> {
self.inner.credentials.lock().await
}
pub async fn oauth2_flow<F>(
&self,
username: &str,
cancel: tokio_util::sync::CancellationToken,
browser_opener: F,
) -> Result<String>
where
F: Fn(&str) -> std::io::Result<()> + Send + Sync + 'static,
{
let mut auth = self.auth().await?;
auth.oauth2_flow(self.http(), username, cancel, browser_opener)
.await
}
pub async fn remote_oauth2_step2(
&self,
redirect_url: &str,
username: &str,
pending_path: &std::path::Path,
) -> Result<String> {
let mut auth = self.auth().await?;
auth.remote_oauth2_step2(self.http(), redirect_url, username, pending_path)
.await
}
pub async fn auth(&self) -> Result<MappedMutexGuard<'_, Auth>> {
let credentials = self.credentials().await;
MutexGuard::try_map(credentials, CredentialSource::store).map_err(|_| {
Error::validation("this client was built from credentials, not a token store")
})
}
pub fn from_env() -> Result<Self> {
let cfg = Config::new();
Self::new(&cfg, Auth::new(&cfg))
}
pub fn build_url_public(&self, target: &RequestTarget) -> Result<String> {
self.build_url(target)
}
fn build_url(&self, target: &RequestTarget) -> Result<String> {
build_url_for_target(&self.inner.base_url, target)
}
}