use std::time::Duration;
use http::HeaderMap;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct RateLimit {
pub limit: Option<u64>,
pub remaining: Option<u64>,
pub reset: Option<u64>,
}
impl RateLimit {
pub fn is_exhausted(&self) -> bool {
self.remaining == Some(0)
}
fn from_headers(headers: &HeaderMap) -> Option<Self> {
let limit = header_u64(headers, &["x-ratelimit-limit", "ratelimit-limit"]);
let remaining = header_u64(headers, &["x-ratelimit-remaining", "ratelimit-remaining"]);
let reset = header_u64(headers, &["x-ratelimit-reset", "ratelimit-reset"]);
if limit.is_none() && remaining.is_none() && reset.is_none() {
None
} else {
Some(RateLimit {
limit,
remaining,
reset,
})
}
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct ResponseMeta {
pub status: u16,
pub rate_limit: Option<RateLimit>,
pub retry_after: Option<Duration>,
}
impl ResponseMeta {
pub fn from_headers(status: u16, headers: &HeaderMap) -> Self {
ResponseMeta {
status,
rate_limit: RateLimit::from_headers(headers),
retry_after: retry_after(headers),
}
}
}
pub(crate) fn retry_after(headers: &HeaderMap) -> Option<Duration> {
header_u64(headers, &["retry-after"]).map(Duration::from_secs)
}
fn header_u64(headers: &HeaderMap, names: &[&str]) -> Option<u64> {
names
.iter()
.find_map(|name| headers.get(*name))
.and_then(|v| v.to_str().ok())
.and_then(|s| s.trim().parse().ok())
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::print_stdout
)]
mod tests {
use super::*;
fn headers(pairs: &[(&str, &str)]) -> HeaderMap {
let mut h = HeaderMap::new();
for (k, v) in pairs {
h.insert(
http::HeaderName::from_bytes(k.as_bytes()).unwrap(),
http::HeaderValue::from_str(v).unwrap(),
);
}
h
}
#[test]
fn parses_x_ratelimit_headers() {
let h = headers(&[
("x-ratelimit-limit", "100"),
("x-ratelimit-remaining", "0"),
("x-ratelimit-reset", "1700000000"),
]);
let rl = RateLimit::from_headers(&h).unwrap();
assert_eq!(rl.limit, Some(100));
assert_eq!(rl.remaining, Some(0));
assert_eq!(rl.reset, Some(1_700_000_000));
assert!(rl.is_exhausted());
}
#[test]
fn parses_ietf_ratelimit_headers() {
let h = headers(&[("ratelimit-remaining", "5")]);
let rl = RateLimit::from_headers(&h).unwrap();
assert_eq!(rl.remaining, Some(5));
assert!(!rl.is_exhausted());
}
#[test]
fn no_headers_yields_none() {
assert_eq!(RateLimit::from_headers(&HeaderMap::new()), None);
}
#[test]
fn retry_after_parses_delta_seconds() {
let h = headers(&[("retry-after", "30")]);
assert_eq!(retry_after(&h), Some(Duration::from_secs(30)));
}
#[test]
fn retry_after_ignores_http_date() {
let h = headers(&[("retry-after", "Wed, 21 Oct 2015 07:28:00 GMT")]);
assert_eq!(retry_after(&h), None);
}
#[test]
fn meta_bundles_status_and_fields() {
let h = headers(&[("x-ratelimit-remaining", "9"), ("retry-after", "2")]);
let meta = ResponseMeta::from_headers(429, &h);
assert_eq!(meta.status, 429);
assert_eq!(meta.rate_limit.unwrap().remaining, Some(9));
assert_eq!(meta.retry_after, Some(Duration::from_secs(2)));
}
}