Skip to main content

videosdk/
builder.rs

1//! Client construction.
2
3use std::sync::{Arc, Mutex};
4use std::time::Duration;
5
6use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
7
8use crate::client::{
9    default_backoff, BackoffFn, Client, ClientInner, Overrides, TokenCache, DEFAULT_BASE_URL,
10};
11use crate::error::{Error, Result};
12use crate::token::decode_token;
13
14pub(crate) const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
15pub(crate) const DEFAULT_MAX_RETRIES: u32 = 2;
16
17/// Overrides applied to every token the client mints for its own requests.
18#[derive(Debug, Clone, Default)]
19pub struct TokenOptions {
20    /// Permission claims. Defaults to `["allow_join", "allow_mod"]`.
21    pub permissions: Option<Vec<String>>,
22    /// Role claims. Defaults to `["crawler"]`.
23    pub roles: Option<Vec<String>>,
24    /// The token version. Defaults to `2`.
25    pub version: Option<i64>,
26    /// The token lifetime. Defaults to 24 hours.
27    pub expires_in: Option<Duration>,
28}
29
30/// The resolved client configuration.
31#[derive(Clone)]
32pub(crate) struct Config {
33    pub(crate) api_key: Option<String>,
34    pub(crate) secret: Option<String>,
35    pub(crate) max_retries: u32,
36    /// A zero duration disables the per-request timeout.
37    pub(crate) timeout: Duration,
38    pub(crate) headers: HeaderMap,
39    pub(crate) token_options: TokenOptions,
40}
41
42/// Builds a [`Client`].
43///
44/// Requires either a static token, or both an API key and a secret. Credentials
45/// fall back to the `VIDEOSDK_API_KEY` and `VIDEOSDK_SECRET` environment
46/// variables, and the endpoint to `VIDEOSDK_API_ENDPOINT`.
47///
48/// ```no_run
49/// # fn main() -> Result<(), videosdk::Error> {
50/// let client = videosdk::Client::builder()
51///     .api_key("my-key")
52///     .secret("my-secret")
53///     .max_retries(3)
54///     .build()?;
55/// # Ok(())
56/// # }
57/// ```
58#[derive(Default)]
59pub struct ClientBuilder {
60    api_key: Option<String>,
61    secret: Option<String>,
62    base_url: Option<String>,
63    token: Option<String>,
64    http_client: Option<reqwest::Client>,
65    max_retries: Option<u32>,
66    timeout: Option<Duration>,
67    headers: Vec<(String, String)>,
68    token_options: TokenOptions,
69    backoff: Option<BackoffFn>,
70}
71
72impl ClientBuilder {
73    /// Starts a new builder.
74    pub fn new() -> Self {
75        Self::default()
76    }
77
78    /// Sets the project API key. Falls back to `VIDEOSDK_API_KEY`.
79    pub fn api_key(mut self, api_key: impl Into<String>) -> Self {
80        self.api_key = Some(api_key.into());
81        self
82    }
83
84    /// Sets the project secret. Falls back to `VIDEOSDK_SECRET`.
85    pub fn secret(mut self, secret: impl Into<String>) -> Self {
86        self.secret = Some(secret.into());
87        self
88    }
89
90    /// Sets the API endpoint. Falls back to `VIDEOSDK_API_ENDPOINT`, then to
91    /// [`DEFAULT_BASE_URL`].
92    pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
93        self.base_url = Some(base_url.into());
94        self
95    }
96
97    /// Uses a pre-generated static token. Disables automatic token refresh
98    /// unless an API key and secret are also supplied.
99    pub fn token(mut self, token: impl Into<String>) -> Self {
100        self.token = Some(token.into());
101        self
102    }
103
104    /// Supplies a preconfigured [`reqwest::Client`], e.g. with a proxy or a
105    /// custom connection pool.
106    pub fn http_client(mut self, http_client: reqwest::Client) -> Self {
107        self.http_client = Some(http_client);
108        self
109    }
110
111    /// Sets how many times a failed request is retried. Defaults to 2, giving
112    /// up to 3 total attempts.
113    pub fn max_retries(mut self, max_retries: u32) -> Self {
114        self.max_retries = Some(max_retries);
115        self
116    }
117
118    /// Sets the per-request timeout. Defaults to 30 seconds; [`Duration::ZERO`]
119    /// disables it.
120    ///
121    /// This applies to each attempt individually, so a request that exhausts its
122    /// retries can take longer than this in total.
123    pub fn request_timeout(mut self, timeout: Duration) -> Self {
124        self.timeout = Some(timeout);
125        self
126    }
127
128    /// Adds a header sent with every request.
129    pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
130        self.headers.push((name.into(), value.into()));
131        self
132    }
133
134    /// Overrides the claims of the tokens the client mints for its own requests.
135    pub fn token_options(mut self, token_options: TokenOptions) -> Self {
136        self.token_options = token_options;
137        self
138    }
139
140    /// Replaces the retry backoff.
141    ///
142    /// The default is exponential — `min(2^attempt seconds, 8s)` plus up to
143    /// 249 ms of jitter — where `attempt` is the zero-based retry counter.
144    pub fn backoff(mut self, backoff: impl Fn(u32) -> Duration + Send + Sync + 'static) -> Self {
145        self.backoff = Some(Arc::new(backoff));
146        self
147    }
148
149    /// Builds the client, resolving credentials from the environment where they
150    /// were not set explicitly.
151    pub fn build(self) -> Result<Client> {
152        let api_key = self.api_key.or_else(|| env_var("VIDEOSDK_API_KEY"));
153        let secret = self.secret.or_else(|| env_var("VIDEOSDK_SECRET"));
154        let base_url = self
155            .base_url
156            .or_else(|| env_var("VIDEOSDK_API_ENDPOINT"))
157            .unwrap_or_else(|| DEFAULT_BASE_URL.to_string());
158
159        if self.token.is_none() && (api_key.is_none() || secret.is_none()) {
160            return Err(Error::config(
161                "Client::builder() requires either a token, or both an API key and secret \
162                 (via .api_key()/.secret(), or the VIDEOSDK_API_KEY / VIDEOSDK_SECRET env vars)",
163            ));
164        }
165
166        let mut headers = HeaderMap::new();
167        for (name, value) in self.headers {
168            let name = HeaderName::try_from(name.as_str())
169                .map_err(|_| Error::config(format!("invalid header name: {name:?}")))?;
170            let value = HeaderValue::try_from(value.as_str())
171                .map_err(|_| Error::config(format!("invalid value for header {name:?}")))?;
172            headers.insert(name, value);
173        }
174
175        let http = match self.http_client {
176            Some(http) => http,
177            None => reqwest::Client::builder()
178                .build()
179                .map_err(|e| Error::config(format!("failed to build the HTTP client: {e}")))?,
180        };
181
182        let can_refresh = api_key.is_some() && secret.is_some();
183
184        // A static token seeds the cache; decoding its `exp` is best-effort.
185        let mut cache = TokenCache::default();
186        if let Some(token) = self.token {
187            cache.expires_at = decode_token(&token).ok().and_then(|c| c.expires_at);
188            cache.token = Some(token);
189        }
190
191        let config = Config {
192            api_key,
193            secret,
194            max_retries: self.max_retries.unwrap_or(DEFAULT_MAX_RETRIES),
195            timeout: self.timeout.unwrap_or(DEFAULT_TIMEOUT),
196            headers,
197            token_options: self.token_options,
198        };
199
200        Ok(Client::from_parts(
201            Arc::new(ClientInner {
202                config,
203                http,
204                base_url: base_url.trim_end_matches('/').to_string(),
205                can_refresh,
206                token: Mutex::new(cache),
207                backoff: self.backoff.unwrap_or_else(|| Arc::new(default_backoff)),
208            }),
209            Overrides::default(),
210        ))
211    }
212}
213
214fn env_var(name: &str) -> Option<String> {
215    std::env::var(name).ok().filter(|v| !v.is_empty())
216}