Skip to main content

romm_cli/client/
request.rs

1use anyhow::{anyhow, Result};
2use reqwest::header::HeaderMap;
3use reqwest::Method;
4use serde_json::Value;
5use std::time::Instant;
6
7use crate::endpoints::Endpoint;
8
9use super::response::{decode_json_response_body, read_error_response_text, romm_api_error};
10use super::RommClient;
11
12impl RommClient {
13    /// Executes a typed [`Endpoint`] and returns its deserialized output.
14    pub async fn call<E>(&self, ep: &E) -> anyhow::Result<E::Output>
15    where
16        E: Endpoint,
17        E::Output: serde::de::DeserializeOwned,
18    {
19        let method = ep.method();
20        let path = ep.path();
21        let query = ep.query();
22        let body = ep.body();
23
24        let value = self.request_json(method, &path, &query, body).await?;
25        let output = serde_json::from_value(value)
26            .map_err(|e| anyhow!("failed to decode response for {} {}: {}", method, path, e))?;
27
28        Ok(output)
29    }
30
31    /// Low-level helper that issues an HTTP request and returns a raw JSON [`Value`].
32    pub async fn request_json(
33        &self,
34        method: &str,
35        path: &str,
36        query: &[(String, String)],
37        body: Option<Value>,
38    ) -> Result<Value> {
39        self.request_json_with_headers(method, path, query, body, self.build_headers()?)
40            .await
41    }
42
43    pub async fn request_json_unauthenticated(
44        &self,
45        method: &str,
46        path: &str,
47        query: &[(String, String)],
48        body: Option<Value>,
49    ) -> Result<Value> {
50        self.request_json_with_headers(method, path, query, body, HeaderMap::new())
51            .await
52    }
53
54    async fn request_json_with_headers(
55        &self,
56        method: &str,
57        path: &str,
58        query: &[(String, String)],
59        body: Option<Value>,
60        headers: HeaderMap,
61    ) -> Result<Value> {
62        let url = format!(
63            "{}/{}",
64            self.base_url.trim_end_matches('/'),
65            path.trim_start_matches('/')
66        );
67
68        let http_method = Method::from_bytes(method.as_bytes())
69            .map_err(|_| anyhow!("invalid HTTP method: {method}"))?;
70
71        let query_refs: Vec<(&str, &str)> = query
72            .iter()
73            .map(|(k, v)| (k.as_str(), v.as_str()))
74            .collect();
75
76        let mut req = self
77            .http
78            .request(http_method, &url)
79            .headers(headers)
80            .query(&query_refs);
81
82        if let Some(body) = body {
83            req = req.json(&body);
84        }
85
86        let t0 = Instant::now();
87        let resp = req
88            .send()
89            .await
90            .map_err(|e| anyhow!("request error: {e}"))?;
91
92        let status = resp.status();
93        if self.verbose {
94            let keys: Vec<&str> = query.iter().map(|(k, _)| k.as_str()).collect();
95            tracing::info!(
96                "[romm-cli] {} {} query_keys={:?} -> {} ({}ms)",
97                method,
98                path,
99                keys,
100                status.as_u16(),
101                t0.elapsed().as_millis()
102            );
103        }
104        if !status.is_success() {
105            let body = read_error_response_text(resp).await;
106            return Err(romm_api_error(status, &body));
107        }
108
109        let bytes = resp
110            .bytes()
111            .await
112            .map_err(|e| anyhow!("read response body: {e}"))?;
113
114        Ok(decode_json_response_body(&bytes))
115    }
116
117    /// Authenticated GET returning raw bytes.
118    pub async fn get_bytes(&self, path: &str, query: &[(String, String)]) -> Result<Vec<u8>> {
119        let url = format!(
120            "{}/{}",
121            self.base_url.trim_end_matches('/'),
122            path.trim_start_matches('/')
123        );
124        let headers = self.build_headers()?;
125        let query_refs: Vec<(&str, &str)> = query
126            .iter()
127            .map(|(k, v)| (k.as_str(), v.as_str()))
128            .collect();
129        let resp = self
130            .http
131            .get(&url)
132            .headers(headers)
133            .query(&query_refs)
134            .send()
135            .await
136            .map_err(|e| anyhow!("GET {path}: {e}"))?;
137        let status = resp.status();
138        if !status.is_success() {
139            let body = read_error_response_text(resp).await;
140            return Err(romm_api_error(status, &body));
141        }
142        Ok(resp.bytes().await?.to_vec())
143    }
144
145    /// POST returning raw bytes.
146    pub async fn post_bytes(
147        &self,
148        path: &str,
149        query: &[(String, String)],
150        json_body: Option<Value>,
151    ) -> Result<Vec<u8>> {
152        let url = format!(
153            "{}/{}",
154            self.base_url.trim_end_matches('/'),
155            path.trim_start_matches('/')
156        );
157        let headers = self.build_headers()?;
158        let query_refs: Vec<(&str, &str)> = query
159            .iter()
160            .map(|(k, v)| (k.as_str(), v.as_str()))
161            .collect();
162        let mut req = self.http.post(&url).headers(headers).query(&query_refs);
163        if let Some(b) = json_body {
164            req = req.json(&b);
165        }
166        let resp = req.send().await.map_err(|e| anyhow!("POST {path}: {e}"))?;
167        let status = resp.status();
168        if !status.is_success() {
169            let body = read_error_response_text(resp).await;
170            return Err(romm_api_error(status, &body));
171        }
172        Ok(resp.bytes().await?.to_vec())
173    }
174}