mod auth;
mod builder;
mod discovery;
pub(crate) mod transport;
use std::fmt;
use std::sync::Arc;
use std::time::Duration;
use url::Url;
pub use auth::{Credentials, Session};
pub use builder::ClientBuilder;
pub use discovery::ServerInfo;
pub(crate) use discovery::{Api, Support};
use crate::error::Result;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum ApiVersion {
#[default]
Auto,
V3,
V1,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RetryPolicy {
max_retries: u32,
base_delay: Duration,
max_delay: Duration,
}
impl RetryPolicy {
#[must_use]
pub const fn none() -> Self {
Self {
max_retries: 0,
base_delay: Duration::ZERO,
max_delay: Duration::ZERO,
}
}
#[must_use]
pub const fn new(max_retries: u32, base_delay: Duration, max_delay: Duration) -> Self {
Self {
max_retries,
base_delay,
max_delay,
}
}
pub(crate) fn delay(&self, attempt: u32, retry_after: Option<Duration>) -> Duration {
if let Some(wait) = retry_after {
return wait;
}
let exp = self
.base_delay
.saturating_mul(1u32 << attempt.min(16))
.min(self.max_delay);
exp / 2 + exp.mul_f64(fastrand::f64() / 2.0)
}
}
impl Default for RetryPolicy {
fn default() -> Self {
Self::new(2, Duration::from_millis(300), Duration::from_secs(5))
}
}
#[derive(Clone)]
pub struct Client {
pub(crate) inner: Arc<Inner>,
}
pub(crate) struct Inner {
pub(crate) base: Url,
pub(crate) http: reqwest::Client,
pub(crate) credentials: Credentials,
pub(crate) jwt: tokio::sync::Mutex<Option<auth::Jwt>>,
pub(crate) app: String,
pub(crate) api: ApiVersion,
pub(crate) retry: RetryPolicy,
pub(crate) server: tokio::sync::OnceCell<ServerInfo>,
}
impl Client {
pub fn builder(url: impl AsRef<str>) -> ClientBuilder {
ClientBuilder::new(url.as_ref())
}
pub async fn connect(url: impl AsRef<str>, credentials: Credentials) -> Result<Self> {
Self::builder(url).credentials(credentials).connect().await
}
#[must_use]
pub fn base_url(&self) -> &Url {
&self.inner.base
}
#[must_use]
pub fn app_name(&self) -> &str {
&self.inner.app
}
pub async fn server_info(&self) -> Result<ServerInfo> {
self.inner.server_info().await.cloned()
}
pub async fn session(&self) -> Result<Option<Session>> {
self.inner.session().await
}
}
impl fmt::Debug for Client {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Client")
.field("base_url", &self.inner.base.as_str())
.field("credentials", &self.inner.credentials)
.field("app", &self.inner.app)
.field("api", &self.inner.api)
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn backoff_is_bounded_and_jittered() {
let policy = RetryPolicy::new(5, Duration::from_millis(100), Duration::from_secs(1));
for attempt in 0..10 {
let d = policy.delay(attempt, None);
assert!(d <= Duration::from_secs(1), "{d:?}");
}
assert!(policy.delay(0, None) >= Duration::from_millis(50));
assert_eq!(
policy.delay(3, Some(Duration::from_secs(7))),
Duration::from_secs(7)
);
}
}