rs_puff/
client.rs

1use crate::{Error, Namespace, NamespacesResponse, Result};
2
3const DEFAULT_BASE_URL: &str = "https://api.turbopuffer.com";
4
5#[derive(Debug, Clone, Default, serde::Serialize)]
6pub struct NamespacesParams {
7    #[serde(skip_serializing_if = "Option::is_none")]
8    pub prefix: Option<String>,
9
10    #[serde(skip_serializing_if = "Option::is_none")]
11    pub cursor: Option<String>,
12
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pub page_size: Option<u32>,
15}
16
17pub struct Client {
18    pub(crate) api_key: String,
19    pub(crate) base_url: String,
20    pub(crate) http: reqwest::Client,
21}
22
23impl Client {
24    pub fn new(api_key: impl Into<String>) -> Self {
25        Self {
26            api_key: api_key.into(),
27            base_url: DEFAULT_BASE_URL.to_string(),
28            http: reqwest::Client::new(),
29        }
30    }
31
32    pub fn with_region(api_key: impl Into<String>, region: &str) -> Self {
33        let base_url = format!("https://{}.turbopuffer.com", region);
34        Self {
35            api_key: api_key.into(),
36            base_url,
37            http: reqwest::Client::new(),
38        }
39    }
40
41    pub fn with_base_url(api_key: impl Into<String>, base_url: impl Into<String>) -> Self {
42        Self {
43            api_key: api_key.into(),
44            base_url: base_url.into(),
45            http: reqwest::Client::new(),
46        }
47    }
48
49    pub fn from_env() -> Result<Self> {
50        let api_key = std::env::var("TURBOPUFFER_API_KEY")
51            .map_err(|_| Error::Api {
52                status: 0,
53                message: "TURBOPUFFER_API_KEY not set".to_string(),
54            })?;
55
56        let base_url = std::env::var("TURBOPUFFER_REGION")
57            .map(|r| format!("https://{}.turbopuffer.com", r))
58            .unwrap_or_else(|_| DEFAULT_BASE_URL.to_string());
59
60        Ok(Self {
61            api_key,
62            base_url,
63            http: reqwest::Client::new(),
64        })
65    }
66
67    pub fn namespace(&self, name: impl Into<String>) -> Namespace<'_> {
68        Namespace::new(self, name.into())
69    }
70
71    pub async fn namespaces(&self, params: NamespacesParams) -> Result<NamespacesResponse> {
72        let mut query_parts = Vec::new();
73        if let Some(ref prefix) = params.prefix {
74            query_parts.push(format!("prefix={}", prefix));
75        }
76        if let Some(ref cursor) = params.cursor {
77            query_parts.push(format!("cursor={}", cursor));
78        }
79        if let Some(page_size) = params.page_size {
80            query_parts.push(format!("page_size={}", page_size));
81        }
82
83        let path = if query_parts.is_empty() {
84            "/v1/namespaces".to_string()
85        } else {
86            format!("/v1/namespaces?{}", query_parts.join("&"))
87        };
88
89        self.request_no_body(reqwest::Method::GET, &path).await
90    }
91
92    pub(crate) async fn request<T, R>(&self, method: reqwest::Method, path: &str, body: Option<&T>) -> Result<R>
93    where
94        T: serde::Serialize + ?Sized,
95        R: serde::de::DeserializeOwned,
96    {
97        let url = format!("{}{}", self.base_url, path);
98
99        let mut req = self.http
100            .request(method, &url)
101            .header("Authorization", format!("Bearer {}", self.api_key))
102            .header("Content-Type", "application/json");
103
104        if let Some(body) = body {
105            req = req.json(body);
106        }
107
108        let resp = req.send().await?;
109        let status = resp.status();
110
111        if !status.is_success() {
112            let message = resp.text().await.unwrap_or_default();
113            return Err(Error::Api {
114                status: status.as_u16(),
115                message,
116            });
117        }
118
119        let result = resp.json().await?;
120        Ok(result)
121    }
122
123    pub(crate) async fn request_no_body<R>(&self, method: reqwest::Method, path: &str) -> Result<R>
124    where
125        R: serde::de::DeserializeOwned,
126    {
127        self.request::<(), R>(method, path, None).await
128    }
129}