Skip to main content

systemprompt_cloud/api_client/
client.rs

1//! `CloudApiClient` constructor + accessors. Lower-level HTTP verbs
2//! live in `methods.rs`; high-level endpoints in `endpoints.rs`.
3
4use reqwest::{Client, StatusCode};
5use serde::de::DeserializeOwned;
6use systemprompt_models::net::{HTTP_CONNECT_TIMEOUT, HTTP_DEFAULT_TIMEOUT};
7
8use super::types::ApiError;
9use crate::error::{CloudError, CloudResult};
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    pub fn new(api_url: &str, token: &str) -> Result<Self, reqwest::Error> {
20        Ok(Self {
21            client: Client::builder()
22                .connect_timeout(HTTP_CONNECT_TIMEOUT)
23                .timeout(HTTP_DEFAULT_TIMEOUT)
24                .build()?,
25            api_url: api_url.to_string(),
26            token: token.to_string(),
27        })
28    }
29
30    #[must_use]
31    pub fn api_url(&self) -> &str {
32        &self.api_url
33    }
34
35    #[must_use]
36    pub fn token(&self) -> &str {
37        &self.token
38    }
39
40    pub(super) async fn handle_response<T: DeserializeOwned>(
41        &self,
42        response: reqwest::Response,
43    ) -> CloudResult<T> {
44        let status = response.status();
45
46        if status == StatusCode::UNAUTHORIZED {
47            return Err(CloudError::Unauthorized);
48        }
49
50        if !status.is_success() {
51            return Err(parse_error_response(status, response).await);
52        }
53
54        response.json().await.map_err(CloudError::from)
55    }
56
57    pub(super) async fn handle_no_content_response(
58        &self,
59        response: reqwest::Response,
60    ) -> CloudResult<()> {
61        let status = response.status();
62        if status == StatusCode::UNAUTHORIZED {
63            return Err(CloudError::Unauthorized);
64        }
65        if status == StatusCode::NO_CONTENT || status.is_success() {
66            return Ok(());
67        }
68        Err(parse_error_response(status, response).await)
69    }
70}
71
72pub(super) async fn parse_error_response(
73    status: StatusCode,
74    response: reqwest::Response,
75) -> CloudError {
76    let error_text = match response.text().await {
77        Ok(t) => t,
78        Err(e) => {
79            tracing::warn!(error = %e, "Failed to read error response body");
80            String::from("<failed to read response body>")
81        },
82    };
83
84    serde_json::from_str::<ApiError>(&error_text).map_or_else(
85        |_| CloudError::HttpStatus {
86            status: status.as_u16(),
87            body: error_text.chars().take(500).collect(),
88        },
89        |parsed| CloudError::ApiError {
90            message: format!("{}: {}", parsed.error.code, parsed.error.message),
91        },
92    )
93}