Skip to main content

systemprompt_cloud/api_client/
client.rs

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