use std::time::Duration;
use aspero_core::{Error, HttpResponse, Usage};
const MAX_MESSAGE: usize = 300;
pub(crate) fn usage(resp: &HttpResponse) -> Usage {
let num = |name: &str| resp.header(name).and_then(|v| v.trim().parse::<f64>().ok());
let mut usage = Usage::default();
usage.cost = num("x-ratelimit-cost-usd");
usage.remaining = num("x-ratelimit-remaining-usd");
usage.resets_at = num("x-ratelimit-reset")
.and_then(|secs| Duration::try_from_secs_f64(secs).ok())
.and_then(|d| resp.received_at.checked_add(d).ok());
usage
}
pub(crate) fn check(resp: &HttpResponse) -> Result<(), Error> {
if resp.is_success() {
return Ok(());
}
let message = message(resp);
Err(match resp.status {
400 => Error::BadRequest(message.unwrap_or_else(|| "rejected by OpenAlex".into())),
401 | 403 => Error::AuthRejected { message },
404 => Error::NotFound,
429 if quota_exhausted(resp) => Error::QuotaExhausted {
resets_at: usage(resp).resets_at,
},
429 => Error::RateLimited {
retry_after: resp
.header("retry-after")
.and_then(|v| v.trim().parse().ok())
.map(Duration::from_secs),
},
status => Error::Server { status, message },
})
}
fn quota_exhausted(resp: &HttpResponse) -> bool {
let body_says = resp.text().contains("not_enough_credits");
let budget_spent = resp
.header("x-ratelimit-remaining-usd")
.and_then(|v| v.trim().parse::<f64>().ok())
.is_some_and(|r| r <= 0.0);
body_says || budget_spent
}
fn message(resp: &HttpResponse) -> Option<String> {
let json: serde_json::Value = serde_json::from_slice(&resp.body).ok()?;
let text = ["message", "error"]
.iter()
.find_map(|k| json.get(k).and_then(|v| v.as_str()))?;
Some(match text.char_indices().nth(MAX_MESSAGE) {
Some((cut, _)) => format!("{}…", &text[..cut]),
None => text.to_owned(),
})
}