cellstate/entity/
client.rs

1//! Async API client for entity commands.
2//!
3//! Wraps `reqwest::Client` with session-derived auth headers and
4//! provides typed GET/POST/PATCH/DELETE helpers.
5
6use anyhow::Result;
7use reqwest::Client;
8use serde::{de::DeserializeOwned, Serialize};
9use std::time::Duration;
10
11/// Async HTTP client preconfigured with base URL and auth headers.
12pub struct ApiClient {
13    client: Client,
14    base_url: String,
15    api_key: Option<String>,
16    tenant_id: Option<String>,
17}
18
19impl ApiClient {
20    /// Create a new API client.
21    ///
22    /// Resolution priority for base_url and api_key is handled by the caller
23    /// (flag > env > session > default).
24    pub fn new(base_url: String, api_key: Option<String>, tenant_id: Option<String>) -> Self {
25        let client = Client::builder()
26            .timeout(Duration::from_secs(30))
27            .build()
28            .expect("failed to build HTTP client");
29        Self {
30            client,
31            base_url,
32            api_key,
33            tenant_id,
34        }
35    }
36
37    /// Build a request with common headers applied.
38    fn request(&self, method: reqwest::Method, path: &str) -> reqwest::RequestBuilder {
39        let url = format!("{}{}", self.base_url, path);
40        let mut req = self.client.request(method, &url);
41        if let Some(key) = &self.api_key {
42            req = req.header("x-api-key", key);
43        }
44        if let Some(tid) = &self.tenant_id {
45            req = req.header("x-tenant-id", tid);
46        }
47        req
48    }
49
50    /// GET with typed deserialization.
51    pub async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
52        let resp = self.request(reqwest::Method::GET, path).send().await?;
53        let status = resp.status();
54        if !status.is_success() {
55            let body = resp.text().await.unwrap_or_default();
56            anyhow::bail!("API error {status}: {body}");
57        }
58        Ok(resp.json().await?)
59    }
60
61    /// GET returning raw JSON value.
62    pub async fn get_raw(&self, path: &str) -> Result<serde_json::Value> {
63        self.get(path).await
64    }
65
66    /// POST with typed request and response.
67    pub async fn post<T: DeserializeOwned, B: Serialize>(&self, path: &str, body: &B) -> Result<T> {
68        let resp = self
69            .request(reqwest::Method::POST, path)
70            .json(body)
71            .send()
72            .await?;
73        let status = resp.status();
74        if !status.is_success() {
75            let body = resp.text().await.unwrap_or_default();
76            anyhow::bail!("API error {status}: {body}");
77        }
78        Ok(resp.json().await?)
79    }
80
81    /// POST returning raw JSON value.
82    pub async fn post_raw<B: Serialize>(&self, path: &str, body: &B) -> Result<serde_json::Value> {
83        self.post(path, body).await
84    }
85
86    /// POST with no request body, typed response.
87    pub async fn post_no_body<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
88        let resp = self.request(reqwest::Method::POST, path).send().await?;
89        let status = resp.status();
90        if !status.is_success() {
91            let body = resp.text().await.unwrap_or_default();
92            anyhow::bail!("API error {status}: {body}");
93        }
94        Ok(resp.json().await?)
95    }
96
97    /// POST with no request body, raw JSON response.
98    pub async fn post_no_body_raw(&self, path: &str) -> Result<serde_json::Value> {
99        self.post_no_body(path).await
100    }
101
102    /// PATCH with typed request and response.
103    pub async fn patch<T: DeserializeOwned, B: Serialize>(
104        &self,
105        path: &str,
106        body: &B,
107    ) -> Result<T> {
108        let resp = self
109            .request(reqwest::Method::PATCH, path)
110            .json(body)
111            .send()
112            .await?;
113        let status = resp.status();
114        if !status.is_success() {
115            let body = resp.text().await.unwrap_or_default();
116            anyhow::bail!("API error {status}: {body}");
117        }
118        Ok(resp.json().await?)
119    }
120
121    /// DELETE with no response body.
122    pub async fn delete(&self, path: &str) -> Result<()> {
123        let resp = self.request(reqwest::Method::DELETE, path).send().await?;
124        let status = resp.status();
125        if !status.is_success() {
126            let body = resp.text().await.unwrap_or_default();
127            anyhow::bail!("API error {status}: {body}");
128        }
129        Ok(())
130    }
131}