use super::error::{RatelimitError, RatelimitErrorType, RatelimitResult};
use hyper::header::{HeaderMap, HeaderValue};
use std::convert::TryFrom;
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum RatelimitHeaders {
GlobalLimited {
reset_after: u64,
},
None,
Present {
bucket: Option<String>,
global: bool,
limit: u64,
remaining: u64,
reset: u64,
reset_after: u64,
},
}
impl RatelimitHeaders {
pub const fn global(&self) -> bool {
match self {
Self::GlobalLimited { .. } => true,
Self::None => false,
Self::Present { global, .. } => *global,
}
}
}
impl TryFrom<&'_ HeaderMap<HeaderValue>> for RatelimitHeaders {
type Error = RatelimitError;
fn try_from(map: &'_ HeaderMap<HeaderValue>) -> RatelimitResult<Self> {
match parse_map(map) {
Ok(v) => Ok(v),
Err(why) => {
let headers = &[
"x-ratelimit-bucket",
"x-ratelimit-limit",
"x-ratelimit-remaining",
"x-ratelimit-reset",
];
if headers.iter().any(|k| map.contains_key(*k)) {
Err(why)
} else if map.contains_key("x-ratelimit-global") {
Ok(Self::GlobalLimited {
reset_after: header_int(map, "x-ratelimit-reset-after")?,
})
} else {
Ok(Self::None)
}
}
}
}
}
#[allow(clippy::cast_possible_truncation)]
fn parse_map(map: &HeaderMap<HeaderValue>) -> RatelimitResult<RatelimitHeaders> {
if let Ok(true) = header_bool(map, "x-ratelimit-global") {
let retry_after = header_int(map, "retry-after")?;
return Ok(RatelimitHeaders::GlobalLimited {
reset_after: retry_after,
});
}
let bucket = header_str(map, "x-ratelimit-bucket")
.ok()
.map(ToOwned::to_owned);
let limit = header_int(map, "x-ratelimit-limit")?;
let remaining = header_int(map, "x-ratelimit-remaining")?;
let reset = header_float(map, "x-ratelimit-reset")?;
#[allow(clippy::cast_sign_loss)]
let reset = (reset * 1000.).ceil() as u64;
let reset_after = header_float(map, "x-ratelimit-reset-after")?;
#[allow(clippy::cast_sign_loss)]
let reset_after = (reset_after * 1000.).ceil() as u64;
Ok(RatelimitHeaders::Present {
bucket,
global: false,
limit,
remaining,
reset,
reset_after,
})
}
fn header_bool(map: &HeaderMap<HeaderValue>, name: &'static str) -> RatelimitResult<bool> {
let value = map
.get(name)
.ok_or_else(|| RatelimitError::header_missing(name))?;
let text = value.to_str().map_err(|source| {
RatelimitError::header_not_utf8(name, value.as_bytes().to_owned(), source)
})?;
let end = text.parse().map_err(|source| RatelimitError {
kind: RatelimitErrorType::ParsingBoolText {
name,
text: text.to_owned(),
},
source: Some(Box::new(source)),
})?;
Ok(end)
}
fn header_float(map: &HeaderMap<HeaderValue>, name: &'static str) -> RatelimitResult<f64> {
let value = map
.get(name)
.ok_or_else(|| RatelimitError::header_missing(name))?;
let text = value.to_str().map_err(|source| {
RatelimitError::header_not_utf8(name, value.as_bytes().to_owned(), source)
})?;
let end = text.parse().map_err(|source| RatelimitError {
kind: RatelimitErrorType::ParsingFloatText {
name,
text: text.to_owned(),
},
source: Some(Box::new(source)),
})?;
Ok(end)
}
fn header_int(map: &HeaderMap<HeaderValue>, name: &'static str) -> RatelimitResult<u64> {
let value = map
.get(name)
.ok_or_else(|| RatelimitError::header_missing(name))?;
let text = value.to_str().map_err(|source| {
RatelimitError::header_not_utf8(name, value.as_bytes().to_owned(), source)
})?;
let end = text.parse().map_err(|source| RatelimitError {
kind: RatelimitErrorType::ParsingIntText {
name,
text: text.to_owned(),
},
source: Some(Box::new(source)),
})?;
Ok(end)
}
fn header_str<'a>(map: &'a HeaderMap<HeaderValue>, name: &'static str) -> RatelimitResult<&'a str> {
let value = map
.get(name)
.ok_or_else(|| RatelimitError::header_missing(name))?;
let text = value.to_str().map_err(|source| {
RatelimitError::header_not_utf8(name, value.as_bytes().to_owned(), source)
})?;
Ok(text)
}
#[cfg(test)]
mod tests {
use super::RatelimitHeaders;
use hyper::header::{HeaderMap, HeaderName, HeaderValue};
use std::{convert::TryFrom, error::Error};
#[test]
fn test_global() -> Result<(), Box<dyn Error>> {
let map = {
let mut map = HeaderMap::new();
map.insert(
HeaderName::from_static("x-ratelimit-global"),
HeaderValue::from_static("true"),
);
map.insert(
HeaderName::from_static("retry-after"),
HeaderValue::from_static("65"),
);
map
};
let headers = RatelimitHeaders::try_from(&map)?;
assert!(
matches!(headers, RatelimitHeaders::GlobalLimited { reset_after } if reset_after == 65)
);
Ok(())
}
}