use std::time::Duration;
use reqwest::blocking::{Client, RequestBuilder};
use serde_json::Value;
use crate::error::{Result, ToriiError};
pub const USER_AGENT: &str = "gitorii-cli";
const REQUEST_TIMEOUT_SECS: u64 = 60;
const CONNECT_TIMEOUT_SECS: u64 = 10;
pub fn make_client() -> Client {
Client::builder()
.user_agent(USER_AGENT)
.timeout(Duration::from_secs(REQUEST_TIMEOUT_SECS))
.connect_timeout(Duration::from_secs(CONNECT_TIMEOUT_SECS))
.build()
.expect("reqwest client build failed")
}
pub fn send_json(req: RequestBuilder, ctx: &str) -> Result<Value> {
let resp = req.send().map_err(|e| ToriiError::Network {
provider: ctx.to_string(),
message: e.to_string(),
})?;
let status = resp.status();
let body = resp.text().map_err(|e| ToriiError::Network {
provider: ctx.to_string(),
message: format!("read error: {}", e),
})?;
let json: Value =
serde_json::from_str(&body).unwrap_or_else(|_| serde_json::json!({ "raw_body": body }));
if !status.is_success() {
let msg = json
.get("message")
.and_then(|v| v.as_str())
.or_else(|| json.get("error").and_then(|v| v.as_str()))
.unwrap_or(if body.is_empty() {
"(no message)"
} else {
&body
});
return Err(ToriiError::PlatformApi {
provider: ctx.to_string(),
status: status.as_u16(),
message: msg.to_string(),
});
}
Ok(json)
}
pub fn send_empty(req: RequestBuilder, ctx: &str) -> Result<()> {
let resp = req.send().map_err(|e| ToriiError::Network {
provider: ctx.to_string(),
message: e.to_string(),
})?;
if !resp.status().is_success() {
let s = resp.status();
let txt = resp.text().unwrap_or_default();
return Err(ToriiError::PlatformApi {
provider: ctx.to_string(),
status: s.as_u16(),
message: txt,
});
}
Ok(())
}
pub fn extract_array<'a>(json: &'a Value, ctx: &str) -> Result<&'a Vec<Value>> {
json.as_array()
.ok_or_else(|| ToriiError::MalformedResponse {
provider: ctx.to_string(),
message: format!("expected array body, got: {}", json),
})
}
pub fn send_text(req: RequestBuilder, ctx: &str) -> Result<String> {
let resp = req.send().map_err(|e| ToriiError::Network {
provider: ctx.to_string(),
message: e.to_string(),
})?;
let status = resp.status();
let body = resp.text().map_err(|e| ToriiError::Network {
provider: ctx.to_string(),
message: format!("read error: {}", e),
})?;
if !status.is_success() {
return Err(ToriiError::PlatformApi {
provider: ctx.to_string(),
status: status.as_u16(),
message: if body.is_empty() {
"(empty body)".to_string()
} else {
body.lines().next().unwrap_or(&body).to_string()
},
});
}
Ok(body)
}
pub fn send_bytes(req: RequestBuilder, ctx: &str) -> Result<Vec<u8>> {
let resp = req.send().map_err(|e| ToriiError::Network {
provider: ctx.to_string(),
message: e.to_string(),
})?;
let status = resp.status();
if !status.is_success() {
let body = resp.text().unwrap_or_default();
return Err(ToriiError::PlatformApi {
provider: ctx.to_string(),
status: status.as_u16(),
message: if body.is_empty() {
"(binary response, empty)".to_string()
} else {
body
},
});
}
let bytes = resp.bytes().map_err(|e| ToriiError::Network {
provider: ctx.to_string(),
message: format!("read error: {}", e),
})?;
Ok(bytes.to_vec())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::ToriiError;
use httpmock::prelude::*;
#[test]
fn send_json_returns_parsed_body_on_2xx() {
let server = MockServer::start();
let m = server.mock(|when, then| {
when.method(GET).path("/ok");
then.status(200)
.json_body(serde_json::json!({ "id": 7, "name": "torii" }));
});
let json = send_json(make_client().get(server.url("/ok")), "Test").unwrap();
m.assert();
assert_eq!(json["id"], 7);
assert_eq!(json["name"], "torii");
}
#[test]
fn send_json_maps_non_2xx_to_platform_api_with_status_and_message() {
let server = MockServer::start();
server.mock(|when, then| {
when.method(GET).path("/missing");
then.status(404)
.json_body(serde_json::json!({ "message": "Not Found" }));
});
let err = send_json(make_client().get(server.url("/missing")), "Test").unwrap_err();
match err {
ToriiError::PlatformApi {
provider,
status,
message,
} => {
assert_eq!(provider, "Test");
assert_eq!(status, 404);
assert_eq!(message, "Not Found");
}
other => panic!("expected PlatformApi, got: {other:?}"),
}
}
#[test]
fn send_json_maps_transport_failure_to_network() {
let err = send_json(make_client().get("http://127.0.0.1:1/x"), "Test").unwrap_err();
assert!(
matches!(err, ToriiError::Network { ref provider, .. } if provider == "Test"),
"expected Network, got: {err:?}"
);
}
#[test]
fn send_empty_ok_on_2xx_platform_api_on_failure() {
let server = MockServer::start();
server.mock(|when, then| {
when.method(POST).path("/del");
then.status(204);
});
server.mock(|when, then| {
when.method(POST).path("/forbidden");
then.status(403).body("nope");
});
assert!(send_empty(make_client().post(server.url("/del")), "Test").is_ok());
let err = send_empty(make_client().post(server.url("/forbidden")), "Test").unwrap_err();
match err {
ToriiError::PlatformApi {
status, message, ..
} => {
assert_eq!(status, 403);
assert_eq!(message, "nope");
}
other => panic!("expected PlatformApi, got: {other:?}"),
}
}
#[test]
fn extract_array_rejects_non_array_as_malformed_response() {
let json = serde_json::json!({ "values": [] });
let err = extract_array(&json, "Test").unwrap_err();
assert!(
matches!(err, ToriiError::MalformedResponse { ref provider, .. } if provider == "Test"),
"expected MalformedResponse, got: {err:?}"
);
let arr_json = serde_json::json!([1, 2]);
assert_eq!(extract_array(&arr_json, "Test").unwrap().len(), 2);
}
#[test]
fn send_text_and_send_bytes_return_raw_bodies() {
let server = MockServer::start();
server.mock(|when, then| {
when.method(GET).path("/log");
then.status(200).body("line1\nline2\n");
});
let text = send_text(make_client().get(server.url("/log")), "Test").unwrap();
assert_eq!(text, "line1\nline2\n");
let bytes = send_bytes(make_client().get(server.url("/log")), "Test").unwrap();
assert_eq!(bytes, b"line1\nline2\n");
}
}