Skip to main content

exa_async/
client.rs

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