systemprompt_cloud/api_client/
client.rs1use 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 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 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}