1use std::time::Duration;
27
28use reqwest::blocking::{Client, RequestBuilder};
29use serde_json::Value;
30
31use crate::error::{Result, ToriiError};
32
33pub const USER_AGENT: &str = "gitorii-cli";
35
36const REQUEST_TIMEOUT_SECS: u64 = 60;
41
42const CONNECT_TIMEOUT_SECS: u64 = 10;
45
46pub fn make_client() -> Client {
51 Client::builder()
52 .user_agent(USER_AGENT)
53 .timeout(Duration::from_secs(REQUEST_TIMEOUT_SECS))
54 .connect_timeout(Duration::from_secs(CONNECT_TIMEOUT_SECS))
55 .build()
56 .expect("reqwest client build failed")
57}
58
59pub fn send_json(req: RequestBuilder, ctx: &str) -> Result<Value> {
66 let resp = req.send().map_err(|e| ToriiError::Network {
67 provider: ctx.to_string(),
68 message: e.to_string(),
69 })?;
70 let status = resp.status();
71 let body = resp.text().map_err(|e| ToriiError::Network {
72 provider: ctx.to_string(),
73 message: format!("read error: {}", e),
74 })?;
75 let json: Value =
76 serde_json::from_str(&body).unwrap_or_else(|_| serde_json::json!({ "raw_body": body }));
77 if !status.is_success() {
78 let msg = json
79 .get("message")
80 .and_then(|v| v.as_str())
81 .or_else(|| json.get("error").and_then(|v| v.as_str()))
82 .unwrap_or(if body.is_empty() {
83 "(no message)"
84 } else {
85 &body
86 });
87 return Err(ToriiError::PlatformApi {
88 provider: ctx.to_string(),
89 status: status.as_u16(),
90 message: msg.to_string(),
91 });
92 }
93 Ok(json)
94}
95
96pub fn send_empty(req: RequestBuilder, ctx: &str) -> Result<()> {
99 let resp = req.send().map_err(|e| ToriiError::Network {
100 provider: ctx.to_string(),
101 message: e.to_string(),
102 })?;
103 if !resp.status().is_success() {
104 let s = resp.status();
105 let txt = resp.text().unwrap_or_default();
106 return Err(ToriiError::PlatformApi {
107 provider: ctx.to_string(),
108 status: s.as_u16(),
109 message: txt,
110 });
111 }
112 Ok(())
113}
114
115pub fn extract_array<'a>(json: &'a Value, ctx: &str) -> Result<&'a Vec<Value>> {
118 json.as_array()
119 .ok_or_else(|| ToriiError::MalformedResponse {
120 provider: ctx.to_string(),
121 message: format!("expected array body, got: {}", json),
122 })
123}
124
125pub fn send_text(req: RequestBuilder, ctx: &str) -> Result<String> {
132 let resp = req.send().map_err(|e| ToriiError::Network {
133 provider: ctx.to_string(),
134 message: e.to_string(),
135 })?;
136 let status = resp.status();
137 let body = resp.text().map_err(|e| ToriiError::Network {
138 provider: ctx.to_string(),
139 message: format!("read error: {}", e),
140 })?;
141 if !status.is_success() {
142 return Err(ToriiError::PlatformApi {
143 provider: ctx.to_string(),
144 status: status.as_u16(),
145 message: if body.is_empty() {
146 "(empty body)".to_string()
147 } else {
148 body.lines().next().unwrap_or(&body).to_string()
149 },
150 });
151 }
152 Ok(body)
153}
154
155pub fn send_bytes(req: RequestBuilder, ctx: &str) -> Result<Vec<u8>> {
159 let resp = req.send().map_err(|e| ToriiError::Network {
160 provider: ctx.to_string(),
161 message: e.to_string(),
162 })?;
163 let status = resp.status();
164 if !status.is_success() {
165 let body = resp.text().unwrap_or_default();
166 return Err(ToriiError::PlatformApi {
167 provider: ctx.to_string(),
168 status: status.as_u16(),
169 message: if body.is_empty() {
170 "(binary response, empty)".to_string()
171 } else {
172 body
173 },
174 });
175 }
176 let bytes = resp.bytes().map_err(|e| ToriiError::Network {
177 provider: ctx.to_string(),
178 message: format!("read error: {}", e),
179 })?;
180 Ok(bytes.to_vec())
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use crate::error::ToriiError;
187 use httpmock::prelude::*;
188
189 #[test]
190 fn send_json_returns_parsed_body_on_2xx() {
191 let server = MockServer::start();
192 let m = server.mock(|when, then| {
193 when.method(GET).path("/ok");
194 then.status(200)
195 .json_body(serde_json::json!({ "id": 7, "name": "torii" }));
196 });
197 let json = send_json(make_client().get(server.url("/ok")), "Test").unwrap();
198 m.assert();
199 assert_eq!(json["id"], 7);
200 assert_eq!(json["name"], "torii");
201 }
202
203 #[test]
204 fn send_json_maps_non_2xx_to_platform_api_with_status_and_message() {
205 let server = MockServer::start();
206 server.mock(|when, then| {
207 when.method(GET).path("/missing");
208 then.status(404)
209 .json_body(serde_json::json!({ "message": "Not Found" }));
210 });
211 let err = send_json(make_client().get(server.url("/missing")), "Test").unwrap_err();
212 match err {
213 ToriiError::PlatformApi {
214 provider,
215 status,
216 message,
217 } => {
218 assert_eq!(provider, "Test");
219 assert_eq!(status, 404);
220 assert_eq!(message, "Not Found");
221 }
222 other => panic!("expected PlatformApi, got: {other:?}"),
223 }
224 }
225
226 #[test]
227 fn send_json_maps_transport_failure_to_network() {
228 let err = send_json(make_client().get("http://127.0.0.1:1/x"), "Test").unwrap_err();
230 assert!(
231 matches!(err, ToriiError::Network { ref provider, .. } if provider == "Test"),
232 "expected Network, got: {err:?}"
233 );
234 }
235
236 #[test]
237 fn send_empty_ok_on_2xx_platform_api_on_failure() {
238 let server = MockServer::start();
239 server.mock(|when, then| {
240 when.method(POST).path("/del");
241 then.status(204);
242 });
243 server.mock(|when, then| {
244 when.method(POST).path("/forbidden");
245 then.status(403).body("nope");
246 });
247 assert!(send_empty(make_client().post(server.url("/del")), "Test").is_ok());
248 let err = send_empty(make_client().post(server.url("/forbidden")), "Test").unwrap_err();
249 match err {
250 ToriiError::PlatformApi {
251 status, message, ..
252 } => {
253 assert_eq!(status, 403);
254 assert_eq!(message, "nope");
255 }
256 other => panic!("expected PlatformApi, got: {other:?}"),
257 }
258 }
259
260 #[test]
261 fn extract_array_rejects_non_array_as_malformed_response() {
262 let json = serde_json::json!({ "values": [] });
263 let err = extract_array(&json, "Test").unwrap_err();
264 assert!(
265 matches!(err, ToriiError::MalformedResponse { ref provider, .. } if provider == "Test"),
266 "expected MalformedResponse, got: {err:?}"
267 );
268 let arr_json = serde_json::json!([1, 2]);
269 assert_eq!(extract_array(&arr_json, "Test").unwrap().len(), 2);
270 }
271
272 #[test]
273 fn send_text_and_send_bytes_return_raw_bodies() {
274 let server = MockServer::start();
275 server.mock(|when, then| {
276 when.method(GET).path("/log");
277 then.status(200).body("line1\nline2\n");
278 });
279 let text = send_text(make_client().get(server.url("/log")), "Test").unwrap();
280 assert_eq!(text, "line1\nline2\n");
281 let bytes = send_bytes(make_client().get(server.url("/log")), "Test").unwrap();
282 assert_eq!(bytes, b"line1\nline2\n");
283 }
284}