Skip to main content

singularity_cli/
client.rs

1use anyhow::{Context, Result, bail};
2use reqwest::StatusCode;
3use reqwest::blocking::Client;
4use serde::Serialize;
5use serde::de::DeserializeOwned;
6
7pub struct ApiClient {
8    base_url: String,
9    token: String,
10    http: Client,
11}
12
13impl ApiClient {
14    pub fn new(token: String) -> Self {
15        Self {
16            base_url: "https://api.singularity-app.com".to_string(),
17            token,
18            http: Client::new(),
19        }
20    }
21
22    #[allow(dead_code)]
23    pub fn with_base_url(token: String, base_url: String) -> Self {
24        Self {
25            base_url,
26            token,
27            http: Client::new(),
28        }
29    }
30
31    fn url(&self, path: &str) -> String {
32        format!("{}{}", self.base_url, path)
33    }
34
35    pub fn get<T: DeserializeOwned>(&self, path: &str, query: &[(&str, String)]) -> Result<T> {
36        let resp = self
37            .http
38            .get(self.url(path))
39            .bearer_auth(&self.token)
40            .query(query)
41            .send()
42            .context("request failed")?;
43
44        Self::handle_response(resp)
45    }
46
47    pub fn post<T: DeserializeOwned, B: Serialize>(&self, path: &str, body: &B) -> Result<T> {
48        let resp = self
49            .http
50            .post(self.url(path))
51            .bearer_auth(&self.token)
52            .json(body)
53            .send()
54            .context("request failed")?;
55
56        Self::handle_response(resp)
57    }
58
59    pub fn patch<T: DeserializeOwned, B: Serialize>(&self, path: &str, body: &B) -> Result<T> {
60        let resp = self
61            .http
62            .patch(self.url(path))
63            .bearer_auth(&self.token)
64            .json(body)
65            .send()
66            .context("request failed")?;
67
68        Self::handle_response(resp)
69    }
70
71    pub fn delete(&self, path: &str) -> Result<()> {
72        let resp = self
73            .http
74            .delete(self.url(path))
75            .bearer_auth(&self.token)
76            .send()
77            .context("request failed")?;
78
79        let status = resp.status();
80        if status.is_success() {
81            Ok(())
82        } else {
83            let body = resp.text().unwrap_or_default();
84            bail!("API error ({}): {}", status, body)
85        }
86    }
87
88    fn handle_response<T: DeserializeOwned>(resp: reqwest::blocking::Response) -> Result<T> {
89        let status = resp.status();
90        if status == StatusCode::UNAUTHORIZED {
91            bail!("unauthorized — check your API token");
92        }
93        if !status.is_success() {
94            let body = resp.text().unwrap_or_default();
95            bail!("API error ({}): {}", status, body);
96        }
97        resp.json::<T>().context("failed to parse response")
98    }
99}