use crate::error::{Error, Result};
use std::time::Duration;
pub struct HttpResponse {
pub status: u16,
pub body: Vec<u8>,
pub retry_after: Option<Duration>,
}
impl HttpResponse {
pub fn new(status: u16, body: Vec<u8>) -> Self {
Self {
status,
body,
retry_after: None,
}
}
}
pub trait HttpTransport: Send + Sync {
fn post(&self, url: &str, headers: &[(String, String)], body: &[u8]) -> Result<HttpResponse>;
fn get(&self, url: &str, headers: &[(String, String)]) -> Result<HttpResponse> {
let _ = (url, headers);
Err(Error::Config(
"GET не поддерживается этим транспортом; реализуйте HttpTransport::get".into(),
))
}
}
pub fn parse_retry_after(header: Option<&str>) -> Option<Duration> {
let secs: u64 = header?.trim().parse().ok()?;
Some(Duration::from_secs(secs))
}
pub(crate) fn parse_response(
status: u16,
body: &[u8],
retry_after: Option<Duration>,
) -> Result<serde_json::Value> {
let text = String::from_utf8_lossy(body);
let parsed: serde_json::Value = if body.is_empty() {
serde_json::Value::Object(Default::default())
} else {
match serde_json::from_slice(body) {
Ok(v) => v,
Err(_) => {
if (200..300).contains(&status) {
return Ok(serde_json::Value::Null);
}
return Err(Error::Api {
code: "response.not_json".into(),
message: format!("ответ не является JSON (HTTP {status})"),
status,
raw: text.into_owned(),
retry_after,
});
}
}
};
if let Some(err) = parsed.get("error") {
let code = err
.get("code")
.and_then(|c| c.as_str())
.unwrap_or("unknown")
.to_string();
let message = err
.get("message")
.and_then(|m| m.as_str())
.unwrap_or("Неизвестная ошибка")
.to_string();
return Err(Error::Api {
code,
message,
status,
raw: text.into_owned(),
retry_after,
});
}
if !(200..300).contains(&status) {
let message = parsed
.get("message")
.and_then(|m| m.as_str())
.map(|s| s.to_string())
.unwrap_or_else(|| format!("HTTP {status}"));
return Err(Error::Api {
code: format!("http.{status}"),
message,
status,
raw: text.into_owned(),
retry_after,
});
}
if let Some(result) = parsed.get("result") {
return Ok(result.clone());
}
Ok(parsed)
}