use std::sync::{Arc, Mutex};
use std::time::Duration;
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use crate::client::{
default_backoff, BackoffFn, Client, ClientInner, Overrides, TokenCache, DEFAULT_BASE_URL,
};
use crate::error::{Error, Result};
use crate::token::decode_token;
pub(crate) const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
pub(crate) const DEFAULT_MAX_RETRIES: u32 = 2;
#[derive(Debug, Clone, Default)]
pub struct TokenOptions {
pub permissions: Option<Vec<String>>,
pub roles: Option<Vec<String>>,
pub version: Option<i64>,
pub expires_in: Option<Duration>,
}
#[derive(Clone)]
pub(crate) struct Config {
pub(crate) api_key: Option<String>,
pub(crate) secret: Option<String>,
pub(crate) max_retries: u32,
pub(crate) timeout: Duration,
pub(crate) headers: HeaderMap,
pub(crate) token_options: TokenOptions,
}
#[derive(Default)]
pub struct ClientBuilder {
api_key: Option<String>,
secret: Option<String>,
base_url: Option<String>,
token: Option<String>,
http_client: Option<reqwest::Client>,
max_retries: Option<u32>,
timeout: Option<Duration>,
headers: Vec<(String, String)>,
token_options: TokenOptions,
backoff: Option<BackoffFn>,
}
impl ClientBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn api_key(mut self, api_key: impl Into<String>) -> Self {
self.api_key = Some(api_key.into());
self
}
pub fn secret(mut self, secret: impl Into<String>) -> Self {
self.secret = Some(secret.into());
self
}
pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = Some(base_url.into());
self
}
pub fn token(mut self, token: impl Into<String>) -> Self {
self.token = Some(token.into());
self
}
pub fn http_client(mut self, http_client: reqwest::Client) -> Self {
self.http_client = Some(http_client);
self
}
pub fn max_retries(mut self, max_retries: u32) -> Self {
self.max_retries = Some(max_retries);
self
}
pub fn request_timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.push((name.into(), value.into()));
self
}
pub fn token_options(mut self, token_options: TokenOptions) -> Self {
self.token_options = token_options;
self
}
pub fn backoff(mut self, backoff: impl Fn(u32) -> Duration + Send + Sync + 'static) -> Self {
self.backoff = Some(Arc::new(backoff));
self
}
pub fn build(self) -> Result<Client> {
let api_key = self.api_key.or_else(|| env_var("VIDEOSDK_API_KEY"));
let secret = self.secret.or_else(|| env_var("VIDEOSDK_SECRET"));
let base_url = self
.base_url
.or_else(|| env_var("VIDEOSDK_API_ENDPOINT"))
.unwrap_or_else(|| DEFAULT_BASE_URL.to_string());
if self.token.is_none() && (api_key.is_none() || secret.is_none()) {
return Err(Error::config(
"Client::builder() requires either a token, or both an API key and secret \
(via .api_key()/.secret(), or the VIDEOSDK_API_KEY / VIDEOSDK_SECRET env vars)",
));
}
let mut headers = HeaderMap::new();
for (name, value) in self.headers {
let name = HeaderName::try_from(name.as_str())
.map_err(|_| Error::config(format!("invalid header name: {name:?}")))?;
let value = HeaderValue::try_from(value.as_str())
.map_err(|_| Error::config(format!("invalid value for header {name:?}")))?;
headers.insert(name, value);
}
let http = match self.http_client {
Some(http) => http,
None => reqwest::Client::builder()
.build()
.map_err(|e| Error::config(format!("failed to build the HTTP client: {e}")))?,
};
let can_refresh = api_key.is_some() && secret.is_some();
let mut cache = TokenCache::default();
if let Some(token) = self.token {
cache.expires_at = decode_token(&token).ok().and_then(|c| c.expires_at);
cache.token = Some(token);
}
let config = Config {
api_key,
secret,
max_retries: self.max_retries.unwrap_or(DEFAULT_MAX_RETRIES),
timeout: self.timeout.unwrap_or(DEFAULT_TIMEOUT),
headers,
token_options: self.token_options,
};
Ok(Client::from_parts(
Arc::new(ClientInner {
config,
http,
base_url: base_url.trim_end_matches('/').to_string(),
can_refresh,
token: Mutex::new(cache),
backoff: self.backoff.unwrap_or_else(|| Arc::new(default_backoff)),
}),
Overrides::default(),
))
}
}
fn env_var(name: &str) -> Option<String> {
std::env::var(name).ok().filter(|v| !v.is_empty())
}