1use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE, USER_AGENT};
4use reqwest::{Client, Method, Response, StatusCode};
5use serde::de::DeserializeOwned;
6use serde::Serialize;
7use std::sync::Arc;
8use std::time::Duration;
9
10use crate::config::{Auth, ClientConfig, TenantContext};
11use crate::error::{Error, Result};
12
13struct ClientInner {
15 http: Client,
16 config: ClientConfig,
17 auth: Auth,
18 tenant: TenantContext,
19}
20
21#[derive(Clone)]
23pub struct EdgeQuakeClient {
24 inner: Arc<ClientInner>,
25}
26
27impl EdgeQuakeClient {
28 pub fn builder() -> ClientBuilder {
30 ClientBuilder::default()
31 }
32
33 pub fn documents(&self) -> crate::resources::documents::DocumentsResource<'_> {
36 crate::resources::documents::DocumentsResource { client: self }
37 }
38
39 pub fn graph(&self) -> crate::resources::graph::GraphResource<'_> {
40 crate::resources::graph::GraphResource { client: self }
41 }
42
43 pub fn entities(&self) -> crate::resources::entities::EntitiesResource<'_> {
44 crate::resources::entities::EntitiesResource { client: self }
45 }
46
47 pub fn relationships(&self) -> crate::resources::relationships::RelationshipsResource<'_> {
48 crate::resources::relationships::RelationshipsResource { client: self }
49 }
50
51 pub fn query(&self) -> crate::resources::query::QueryResource<'_> {
52 crate::resources::query::QueryResource { client: self }
53 }
54
55 pub fn chat(&self) -> crate::resources::chat::ChatResource<'_> {
56 crate::resources::chat::ChatResource { client: self }
57 }
58
59 pub fn auth(&self) -> crate::resources::auth::AuthResource<'_> {
60 crate::resources::auth::AuthResource { client: self }
61 }
62
63 pub fn users(&self) -> crate::resources::users::UsersResource<'_> {
64 crate::resources::users::UsersResource { client: self }
65 }
66
67 pub fn api_keys(&self) -> crate::resources::api_keys::ApiKeysResource<'_> {
68 crate::resources::api_keys::ApiKeysResource { client: self }
69 }
70
71 pub fn tenants(&self) -> crate::resources::tenants::TenantsResource<'_> {
72 crate::resources::tenants::TenantsResource { client: self }
73 }
74
75 pub fn conversations(&self) -> crate::resources::conversations::ConversationsResource<'_> {
76 crate::resources::conversations::ConversationsResource { client: self }
77 }
78
79 pub fn folders(&self) -> crate::resources::folders::FoldersResource<'_> {
80 crate::resources::folders::FoldersResource { client: self }
81 }
82
83 pub fn tasks(&self) -> crate::resources::tasks::TasksResource<'_> {
84 crate::resources::tasks::TasksResource { client: self }
85 }
86
87 pub fn pipeline(&self) -> crate::resources::pipeline::PipelineResource<'_> {
88 crate::resources::pipeline::PipelineResource { client: self }
89 }
90
91 pub fn costs(&self) -> crate::resources::costs::CostsResource<'_> {
92 crate::resources::costs::CostsResource { client: self }
93 }
94
95 pub fn chunks(&self) -> crate::resources::chunks::ChunksResource<'_> {
96 crate::resources::chunks::ChunksResource { client: self }
97 }
98
99 pub fn provenance(&self) -> crate::resources::provenance::ProvenanceResource<'_> {
100 crate::resources::provenance::ProvenanceResource { client: self }
101 }
102
103 pub fn models(&self) -> crate::resources::models::ModelsResource<'_> {
104 crate::resources::models::ModelsResource { client: self }
105 }
106
107 pub fn workspaces(&self) -> crate::resources::workspaces::WorkspacesResource<'_> {
108 crate::resources::workspaces::WorkspacesResource { client: self }
109 }
110
111 pub fn health(&self) -> crate::resources::health::HealthResource<'_> {
112 crate::resources::health::HealthResource { client: self }
113 }
114
115 pub fn pdf(&self) -> crate::resources::pdf::PdfResource<'_> {
116 crate::resources::pdf::PdfResource { client: self }
117 }
118
119 pub fn lineage(&self) -> crate::resources::lineage::LineageResource<'_> {
120 crate::resources::lineage::LineageResource { client: self }
121 }
122
123 pub fn settings(&self) -> crate::resources::settings::SettingsResource<'_> {
124 crate::resources::settings::SettingsResource { client: self }
125 }
126
127 pub(crate) fn url(&self, path: &str) -> Result<url::Url> {
131 let base = &self.inner.config.base_url;
132 let full = if path.starts_with('/') {
133 format!("{}{}", base.trim_end_matches('/'), path)
134 } else {
135 format!("{}/{}", base.trim_end_matches('/'), path)
136 };
137 url::Url::parse(&full).map_err(Error::Url)
138 }
139
140 pub(crate) async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
142 self.request(Method::GET, path, Option::<&()>::None).await
143 }
144
145 pub(crate) async fn post<B: Serialize, T: DeserializeOwned>(
147 &self,
148 path: &str,
149 body: Option<&B>,
150 ) -> Result<T> {
151 self.request(Method::POST, path, body).await
152 }
153
154 pub(crate) async fn put<B: Serialize, T: DeserializeOwned>(
156 &self,
157 path: &str,
158 body: Option<&B>,
159 ) -> Result<T> {
160 self.request(Method::PUT, path, body).await
161 }
162
163 pub(crate) async fn patch<B: Serialize, T: DeserializeOwned>(
165 &self,
166 path: &str,
167 body: Option<&B>,
168 ) -> Result<T> {
169 self.request(Method::PATCH, path, body).await
170 }
171
172 pub(crate) async fn delete<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
174 self.request(Method::DELETE, path, Option::<&()>::None).await
175 }
176
177 pub(crate) async fn delete_no_content(&self, path: &str) -> Result<()> {
179 let resp = self.send_with_retry(Method::DELETE, path, Option::<&()>::None).await?;
180 let status = resp.status();
181 if status.is_success() {
182 Ok(())
183 } else {
184 Err(Error::from_response(resp).await)
185 }
186 }
187
188 pub(crate) async fn get_raw(&self, path: &str) -> Result<Vec<u8>> {
190 let resp = self.send_with_retry(Method::GET, path, Option::<&()>::None).await?;
191 let status = resp.status();
192 if status.is_success() {
193 resp.bytes().await.map(|b| b.to_vec()).map_err(Error::Network)
194 } else {
195 Err(Error::from_response(resp).await)
196 }
197 }
198
199 pub(crate) async fn post_no_content<B: Serialize>(
201 &self,
202 path: &str,
203 body: Option<&B>,
204 ) -> Result<()> {
205 let resp = self.send_with_retry(Method::POST, path, body).await?;
206 let status = resp.status();
207 if status.is_success() {
208 Ok(())
209 } else {
210 Err(Error::from_response(resp).await)
211 }
212 }
213
214 async fn request<B: Serialize, T: DeserializeOwned>(
216 &self,
217 method: Method,
218 path: &str,
219 body: Option<&B>,
220 ) -> Result<T> {
221 let resp = self.send_with_retry(method, path, body).await?;
222 let status = resp.status();
223 if status.is_success() {
224 let bytes = resp.bytes().await.map_err(Error::Network)?;
225 serde_json::from_slice(&bytes).map_err(Error::Json)
226 } else {
227 Err(Error::from_response(resp).await)
228 }
229 }
230
231 async fn send_with_retry<B: Serialize>(
233 &self,
234 method: Method,
235 path: &str,
236 body: Option<&B>,
237 ) -> Result<Response> {
238 let max_retries = self.inner.config.max_retries;
239 let backoff = self.inner.config.retry_backoff;
240 let mut last_err: Option<Error> = None;
241
242 for attempt in 0..=max_retries {
243 if attempt > 0 {
244 let wait = backoff * 2u32.saturating_pow(attempt - 1);
245 tokio::time::sleep(wait).await;
246 }
247
248 match self.send_once(method.clone(), path, body).await {
249 Ok(resp) => {
250 if (resp.status() == StatusCode::TOO_MANY_REQUESTS
252 || resp.status().is_server_error())
253 && attempt < max_retries {
254 last_err = Some(Error::from_response(resp).await);
255 continue;
256 }
257 return Ok(resp);
258 }
259 Err(e) if e.is_retryable() && attempt < max_retries => {
260 last_err = Some(e);
261 continue;
262 }
263 Err(e) => return Err(e),
264 }
265 }
266
267 Err(last_err.unwrap_or(Error::Config("max retries exhausted".into())))
268 }
269
270 async fn send_once<B: Serialize>(
272 &self,
273 method: Method,
274 path: &str,
275 body: Option<&B>,
276 ) -> Result<Response> {
277 let url = self.url(path)?;
278 let mut req = self.inner.http.request(method, url);
279
280 match &self.inner.auth {
282 Auth::None => {}
283 Auth::ApiKey(key) => {
284 req = req.header("X-API-Key", key.as_str());
285 }
286 Auth::Bearer(token) => {
287 req = req.header(
288 AUTHORIZATION,
289 format!("Bearer {}", token),
290 );
291 }
292 }
293
294 if let Some(tid) = &self.inner.tenant.tenant_id {
296 req = req.header("X-Tenant-ID", tid.as_str());
297 }
298 if let Some(uid) = &self.inner.tenant.user_id {
299 req = req.header("X-User-ID", uid.as_str());
300 }
301 if let Some(wid) = &self.inner.tenant.workspace_id {
302 req = req.header("X-Workspace-ID", wid.as_str());
303 }
304
305 if let Some(b) = body {
307 req = req.json(b);
308 }
309
310 req.send().await.map_err(Error::Network)
311 }
312
313 pub(crate) async fn raw_get(&self, path: &str) -> Result<Response> {
315 self.send_with_retry(Method::GET, path, Option::<&()>::None).await
316 }
317
318 pub fn base_url(&self) -> &str {
320 &self.inner.config.base_url
321 }
322}
323
324pub struct ClientBuilder {
328 config: ClientConfig,
329 auth: Auth,
330 tenant: TenantContext,
331}
332
333impl Default for ClientBuilder {
334 fn default() -> Self {
335 Self {
336 config: ClientConfig::default(),
337 auth: Auth::None,
338 tenant: TenantContext::default(),
339 }
340 }
341}
342
343impl ClientBuilder {
344 pub fn base_url(mut self, url: impl Into<String>) -> Self {
345 self.config.base_url = url.into();
346 self
347 }
348
349 pub fn api_key(mut self, key: impl Into<String>) -> Self {
350 self.auth = Auth::ApiKey(key.into());
351 self
352 }
353
354 pub fn bearer_token(mut self, token: impl Into<String>) -> Self {
355 self.auth = Auth::Bearer(token.into());
356 self
357 }
358
359 pub fn tenant_id(mut self, id: impl Into<String>) -> Self {
360 self.tenant.tenant_id = Some(id.into());
361 self
362 }
363
364 pub fn user_id(mut self, id: impl Into<String>) -> Self {
365 self.tenant.user_id = Some(id.into());
366 self
367 }
368
369 pub fn workspace_id(mut self, id: impl Into<String>) -> Self {
370 self.tenant.workspace_id = Some(id.into());
371 self
372 }
373
374 pub fn timeout(mut self, d: Duration) -> Self {
375 self.config.timeout = d;
376 self
377 }
378
379 pub fn connect_timeout(mut self, d: Duration) -> Self {
380 self.config.connect_timeout = d;
381 self
382 }
383
384 pub fn max_retries(mut self, n: u32) -> Self {
385 self.config.max_retries = n;
386 self
387 }
388
389 pub fn user_agent(mut self, ua: impl Into<String>) -> Self {
390 self.config.user_agent = ua.into();
391 self
392 }
393
394 pub fn build(self) -> Result<EdgeQuakeClient> {
396 let _ = url::Url::parse(&self.config.base_url)
398 .map_err(|_| Error::Config(format!("invalid base_url: {}", self.config.base_url)))?;
399
400 let mut headers = HeaderMap::new();
401 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
402 headers.insert(
403 USER_AGENT,
404 HeaderValue::from_str(&self.config.user_agent)
405 .unwrap_or_else(|_| HeaderValue::from_static("edgequake-rust-sdk/0.1.0")),
406 );
407
408 let http = Client::builder()
409 .timeout(self.config.timeout)
410 .connect_timeout(self.config.connect_timeout)
411 .default_headers(headers)
412 .build()
413 .map_err(Error::Network)?;
414
415 Ok(EdgeQuakeClient {
416 inner: Arc::new(ClientInner {
417 http,
418 config: self.config,
419 auth: self.auth,
420 tenant: self.tenant,
421 }),
422 })
423 }
424}
425
426impl std::fmt::Debug for EdgeQuakeClient {
427 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
428 f.debug_struct("EdgeQuakeClient")
429 .field("base_url", &self.inner.config.base_url)
430 .finish()
431 }
432}