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