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}