Skip to main content

systemprompt_cloud/api_client/
client.rs

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