Skip to main content

exa_async/
client.rs

1use backon::{ExponentialBuilder, Retryable};
2use serde::{Serialize, de::DeserializeOwned};
3
4use crate::{config::Config, error::ExaError, retry};
5
6/// Exa API client
7///
8/// The client is generic over a [`Config`] implementation that provides authentication
9/// and API configuration.
10#[derive(Debug, Clone)]
11pub struct Client<C: Config> {
12    http: reqwest::Client,
13    config: C,
14    backoff: ExponentialBuilder,
15}
16
17impl Client<crate::config::ExaConfig> {
18    /// Creates a new client with default configuration
19    ///
20    /// Uses environment variables for authentication:
21    /// - `EXA_API_KEY` for API key authentication
22    /// - `EXA_BASE_URL` for custom API base URL
23    #[must_use]
24    pub fn new() -> Self {
25        Self::with_config(crate::config::ExaConfig::new())
26    }
27}
28
29impl<C: Config + Default> Default for Client<C> {
30    fn default() -> Self {
31        Self::with_config(C::default())
32    }
33}
34
35impl<C: Config> Client<C> {
36    /// Creates a new client with the given configuration.
37    ///
38    /// # Panics
39    ///
40    /// Panics if the reqwest client cannot be built.
41    #[must_use]
42    pub fn with_config(config: C) -> Self {
43        Self {
44            http: reqwest::Client::builder()
45                .connect_timeout(std::time::Duration::from_secs(5))
46                .timeout(std::time::Duration::from_secs(60))
47                .build()
48                .expect("reqwest client"),
49            config,
50            backoff: retry::default_backoff_builder(),
51        }
52    }
53
54    /// Replaces the HTTP client with a custom one
55    #[must_use]
56    pub fn with_http_client(mut self, http: reqwest::Client) -> Self {
57        self.http = http;
58        self
59    }
60
61    /// Replaces the backoff configuration for retry logic
62    #[must_use]
63    pub const fn with_backoff(mut self, backoff: ExponentialBuilder) -> Self {
64        self.backoff = backoff;
65        self
66    }
67
68    /// Returns a reference to the client's configuration
69    #[must_use]
70    pub const fn config(&self) -> &C {
71        &self.config
72    }
73
74    pub(crate) async fn post<I, O>(&self, path: &str, body: I) -> Result<O, ExaError>
75    where
76        I: Serialize + Send + Sync,
77        O: DeserializeOwned,
78    {
79        let mk = || async {
80            let headers = self.config.headers()?;
81            Ok(self
82                .http
83                .post(self.config.url(path))
84                .headers(headers)
85                .query(&self.config.query())
86                .json(&body)
87                .build()?)
88        };
89        self.execute(mk).await
90    }
91
92    async fn execute<O, M, Fut>(&self, mk: M) -> Result<O, ExaError>
93    where
94        O: DeserializeOwned,
95        M: Fn() -> Fut + Send + Sync,
96        Fut: core::future::Future<Output = Result<reqwest::Request, ExaError>> + Send,
97    {
98        // Validate auth before any request
99        self.config.validate_auth()?;
100
101        let bytes = self.execute_raw(mk).await?;
102        let resp: O =
103            serde_json::from_slice(&bytes).map_err(|e| crate::error::map_deser(&e, &bytes))?;
104        Ok(resp)
105    }
106
107    async fn execute_raw<M, Fut>(&self, mk: M) -> Result<bytes::Bytes, ExaError>
108    where
109        M: Fn() -> Fut + Send + Sync,
110        Fut: core::future::Future<Output = Result<reqwest::Request, ExaError>> + Send,
111    {
112        let http_client = self.http.clone();
113
114        (|| async {
115            let request = mk().await?;
116            let response = http_client
117                .execute(request)
118                .await
119                .map_err(ExaError::Reqwest)?;
120
121            let status = response.status();
122            let bytes = response.bytes().await.map_err(ExaError::Reqwest)?;
123
124            if status.is_success() {
125                return Ok(bytes);
126            }
127
128            Err(crate::error::deserialize_api_error(status, &bytes))
129        })
130        .retry(self.backoff)
131        .when(ExaError::is_retryable)
132        .await
133    }
134}