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
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}