Skip to main content

systemprompt_cloud/api_client/
client.rs

1use anyhow::{anyhow, Context, Result};
2use chrono::Utc;
3use reqwest::{Client, StatusCode};
4use serde::de::DeserializeOwned;
5use serde::Serialize;
6use systemprompt_models::modules::ApiPaths;
7
8use super::types::{
9    ActivityRequest, ApiError, CheckoutRequest, CheckoutResponse, ListResponse, Plan, Tenant,
10    UserMeResponse,
11};
12
13#[derive(Debug)]
14pub struct CloudApiClient {
15    pub(super) client: Client,
16    pub(super) api_url: String,
17    pub(super) token: String,
18}
19
20impl CloudApiClient {
21    #[must_use]
22    pub fn new(api_url: &str, token: &str) -> Self {
23        Self {
24            client: Client::new(),
25            api_url: api_url.to_string(),
26            token: token.to_string(),
27        }
28    }
29
30    #[must_use]
31    pub fn api_url(&self) -> &str {
32        &self.api_url
33    }
34
35    #[must_use]
36    pub fn token(&self) -> &str {
37        &self.token
38    }
39
40    pub(super) async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
41        let url = format!("{}{}", self.api_url, path);
42        let response = self
43            .client
44            .get(&url)
45            .header("Authorization", format!("Bearer {}", self.token))
46            .send()
47            .await
48            .context("Failed to connect to API")?;
49
50        self.handle_response(response).await
51    }
52
53    pub(super) async fn post<T: DeserializeOwned, B: Serialize + Sync>(
54        &self,
55        path: &str,
56        body: &B,
57    ) -> Result<T> {
58        let url = format!("{}{}", self.api_url, path);
59        let response = self
60            .client
61            .post(&url)
62            .header("Authorization", format!("Bearer {}", self.token))
63            .json(body)
64            .send()
65            .await
66            .context("Failed to connect to API")?;
67
68        self.handle_response(response).await
69    }
70
71    pub(super) async fn post_no_response<B: Serialize + Sync>(
72        &self,
73        path: &str,
74        body: &B,
75    ) -> Result<()> {
76        let url = format!("{}{}", self.api_url, path);
77        let response = self
78            .client
79            .post(&url)
80            .header("Authorization", format!("Bearer {}", self.token))
81            .json(body)
82            .send()
83            .await
84            .context("Failed to connect to API")?;
85
86        let status = response.status();
87        if status == StatusCode::UNAUTHORIZED {
88            return Err(anyhow!(
89                "Authentication failed. Please run 'systemprompt cloud login' again."
90            ));
91        }
92        if status == StatusCode::NO_CONTENT || status.is_success() {
93            return Ok(());
94        }
95
96        let error_text = response
97            .text()
98            .await
99            .unwrap_or_else(|_| String::from("<failed to read response body>"));
100
101        serde_json::from_str::<ApiError>(&error_text).map_or_else(
102            |_| {
103                Err(anyhow!(
104                    "Request failed with status {}: {}",
105                    status,
106                    error_text.chars().take(500).collect::<String>()
107                ))
108            },
109            |parsed| Err(anyhow!("{}: {}", parsed.error.code, parsed.error.message)),
110        )
111    }
112
113    pub(super) async fn put<T: DeserializeOwned, B: Serialize + Sync>(
114        &self,
115        path: &str,
116        body: &B,
117    ) -> Result<T> {
118        let url = format!("{}{}", self.api_url, path);
119        let response = self
120            .client
121            .put(&url)
122            .header("Authorization", format!("Bearer {}", self.token))
123            .json(body)
124            .send()
125            .await
126            .context("Failed to connect to API")?;
127
128        self.handle_response(response).await
129    }
130
131    pub(super) async fn put_no_content<B: Serialize + Sync>(
132        &self,
133        path: &str,
134        body: &B,
135    ) -> Result<()> {
136        let url = format!("{}{}", self.api_url, path);
137        let response = self
138            .client
139            .put(&url)
140            .header("Authorization", format!("Bearer {}", self.token))
141            .json(body)
142            .send()
143            .await
144            .context("Failed to connect to API")?;
145
146        let status = response.status();
147        if status == StatusCode::UNAUTHORIZED {
148            return Err(anyhow!(
149                "Authentication failed. Please run 'systemprompt cloud login' again."
150            ));
151        }
152        if status == StatusCode::NO_CONTENT || status.is_success() {
153            return Ok(());
154        }
155
156        let error_text = response
157            .text()
158            .await
159            .unwrap_or_else(|_| String::from("<failed to read response body>"));
160
161        serde_json::from_str::<ApiError>(&error_text).map_or_else(
162            |_| {
163                Err(anyhow!(
164                    "Request failed with status {}: {}",
165                    status,
166                    error_text.chars().take(500).collect::<String>()
167                ))
168            },
169            |parsed| Err(anyhow!("{}: {}", parsed.error.code, parsed.error.message)),
170        )
171    }
172
173    pub(super) async fn delete(&self, path: &str) -> Result<()> {
174        let url = format!("{}{}", self.api_url, path);
175        let response = self
176            .client
177            .delete(&url)
178            .header("Authorization", format!("Bearer {}", self.token))
179            .send()
180            .await
181            .context("Failed to connect to API")?;
182
183        let status = response.status();
184
185        if status == StatusCode::UNAUTHORIZED {
186            return Err(anyhow!(
187                "Authentication failed. Please run 'systemprompt cloud login' again."
188            ));
189        }
190
191        if status == StatusCode::NO_CONTENT {
192            return Ok(());
193        }
194
195        if !status.is_success() {
196            let error_text = response
197                .text()
198                .await
199                .unwrap_or_else(|_| String::from("<failed to read response body>"));
200
201            return serde_json::from_str::<ApiError>(&error_text).map_or_else(
202                |_| {
203                    Err(anyhow!(
204                        "Request failed with status {}: {}",
205                        status,
206                        error_text.chars().take(500).collect::<String>()
207                    ))
208                },
209                |parsed| Err(anyhow!("{}: {}", parsed.error.code, parsed.error.message)),
210            );
211        }
212
213        Ok(())
214    }
215
216    pub(super) async fn post_empty<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
217        let url = format!("{}{}", self.api_url, path);
218        let response = self
219            .client
220            .post(&url)
221            .header("Authorization", format!("Bearer {}", self.token))
222            .send()
223            .await
224            .context("Failed to connect to API")?;
225
226        self.handle_response(response).await
227    }
228
229    async fn handle_response<T: DeserializeOwned>(&self, response: reqwest::Response) -> Result<T> {
230        let status = response.status();
231
232        if status == StatusCode::UNAUTHORIZED {
233            return Err(anyhow!(
234                "Authentication failed. Please run 'systemprompt cloud login' again."
235            ));
236        }
237
238        if !status.is_success() {
239            let error_text = response
240                .text()
241                .await
242                .unwrap_or_else(|_| String::from("<failed to read response body>"));
243
244            return serde_json::from_str::<ApiError>(&error_text).map_or_else(
245                |_| {
246                    Err(anyhow!(
247                        "Request failed with status {}: {}",
248                        status,
249                        error_text.chars().take(500).collect::<String>()
250                    ))
251                },
252                |parsed| Err(anyhow!("{}: {}", parsed.error.code, parsed.error.message)),
253            );
254        }
255
256        response
257            .json()
258            .await
259            .context("Failed to parse API response")
260    }
261
262    pub async fn get_user(&self) -> Result<UserMeResponse> {
263        self.get(ApiPaths::AUTH_ME).await
264    }
265
266    pub async fn list_tenants(&self) -> Result<Vec<Tenant>> {
267        let response: ListResponse<Tenant> = self.get(ApiPaths::CLOUD_TENANTS).await?;
268        Ok(response.data)
269    }
270
271    pub async fn get_plans(&self) -> Result<Vec<Plan>> {
272        let plans: Vec<Plan> = self.get(ApiPaths::CLOUD_CHECKOUT_PLANS).await?;
273        Ok(plans)
274    }
275
276    pub async fn create_checkout(
277        &self,
278        price_id: &str,
279        region: &str,
280        redirect_uri: Option<&str>,
281    ) -> Result<CheckoutResponse> {
282        let request = CheckoutRequest {
283            price_id: price_id.to_string(),
284            region: region.to_string(),
285            redirect_uri: redirect_uri.map(String::from),
286        };
287        self.post(ApiPaths::CLOUD_CHECKOUT, &request).await
288    }
289
290    pub async fn report_activity(&self, event_type: &str, user_id: &str) -> Result<()> {
291        let request = ActivityRequest {
292            event: event_type.to_string(),
293            timestamp: Utc::now().to_rfc3339(),
294            data: super::types::ActivityData {
295                user_id: user_id.to_string(),
296            },
297        };
298        self.post_no_response(ApiPaths::CLOUD_ACTIVITY, &request)
299            .await
300    }
301}