Skip to main content

romm_api/client/
request.rs

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