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.text().await.unwrap_or_default();
113        let error: Result<ApiError, _> = serde_json::from_str(&error_text);
114
115        match error {
116            Ok(parsed) => Err(anyhow!("{}: {}", parsed.error.code, parsed.error.message)),
117            Err(_) => Err(anyhow!(
118                "Request failed with status {}: {}",
119                status,
120                error_text.chars().take(500).collect::<String>()
121            )),
122        }
123    }
124
125    pub(super) async fn delete(&self, path: &str) -> Result<()> {
126        let url = format!("{}{}", self.api_url, path);
127        let response = self
128            .client
129            .delete(&url)
130            .header("Authorization", format!("Bearer {}", self.token))
131            .send()
132            .await
133            .context("Failed to connect to API")?;
134
135        let status = response.status();
136
137        if status == StatusCode::UNAUTHORIZED {
138            return Err(anyhow!(
139                "Authentication failed. Please run 'systemprompt cloud login' again."
140            ));
141        }
142
143        if status == StatusCode::NO_CONTENT {
144            return Ok(());
145        }
146
147        if !status.is_success() {
148            let error_text = response.text().await.unwrap_or_default();
149            let error: Result<ApiError, _> = serde_json::from_str(&error_text);
150
151            return match error {
152                Ok(parsed) => Err(anyhow!("{}: {}", parsed.error.code, parsed.error.message)),
153                Err(_) => Err(anyhow!(
154                    "Request failed with status {}: {}",
155                    status,
156                    error_text.chars().take(500).collect::<String>()
157                )),
158            };
159        }
160
161        Ok(())
162    }
163
164    pub(super) async fn post_empty<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
165        let url = format!("{}{}", self.api_url, path);
166        let response = self
167            .client
168            .post(&url)
169            .header("Authorization", format!("Bearer {}", self.token))
170            .send()
171            .await
172            .context("Failed to connect to API")?;
173
174        self.handle_response(response).await
175    }
176
177    async fn handle_response<T: DeserializeOwned>(&self, response: reqwest::Response) -> Result<T> {
178        let status = response.status();
179
180        if status == StatusCode::UNAUTHORIZED {
181            return Err(anyhow!(
182                "Authentication failed. Please run 'systemprompt cloud login' again."
183            ));
184        }
185
186        if !status.is_success() {
187            let error_text = response.text().await.unwrap_or_default();
188
189            // Try to parse as our expected error format
190            let error: Result<ApiError, _> = serde_json::from_str(&error_text);
191
192            match error {
193                Ok(parsed) => {
194                    return Err(anyhow!("{}: {}", parsed.error.code, parsed.error.message));
195                },
196                Err(_parse_error) => {
197                    // Parsing failed - show raw response for debugging
198                    return Err(anyhow!(
199                        "Request failed with status {}: {}",
200                        status,
201                        error_text.chars().take(500).collect::<String>()
202                    ));
203                },
204            }
205        }
206
207        response
208            .json()
209            .await
210            .context("Failed to parse API response")
211    }
212
213    pub async fn get_user(&self) -> Result<UserMeResponse> {
214        self.get(ApiPaths::AUTH_ME).await
215    }
216
217    pub async fn list_tenants(&self) -> Result<Vec<Tenant>> {
218        let response: ListResponse<Tenant> = self.get(ApiPaths::CLOUD_TENANTS).await?;
219        Ok(response.data)
220    }
221
222    pub async fn get_plans(&self) -> Result<Vec<Plan>> {
223        let plans: Vec<Plan> = self.get(ApiPaths::CLOUD_CHECKOUT_PLANS).await?;
224        Ok(plans)
225    }
226
227    pub async fn create_checkout(
228        &self,
229        price_id: &str,
230        region: &str,
231        redirect_uri: Option<&str>,
232    ) -> Result<CheckoutResponse> {
233        let request = CheckoutRequest {
234            price_id: price_id.to_string(),
235            region: region.to_string(),
236            redirect_uri: redirect_uri.map(String::from),
237        };
238        self.post(ApiPaths::CLOUD_CHECKOUT, &request).await
239    }
240}