#![doc = include_str!("../README.md")]
mod convert;
mod error;
mod parser;
mod reset_time;
mod vendors;
pub mod headers;
pub mod retry_after;
use std::str::FromStr;
use error::{Error, Result};
#[cfg(feature = "http")]
use http::HeaderMap;
use std::time::Duration;
pub use headers::Headers;
pub use reset_time::ResetTime;
pub use vendors::{Vendor, VendorMask};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status {
Ready,
Wait(Duration),
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum RateLimit {
Rfc6585(headers::Headers),
RetryAfter(retry_after::RateLimit),
}
impl RateLimit {
#[cfg(feature = "http")]
pub fn new(headers: &HeaderMap) -> std::result::Result<Self, Error> {
Self::extract(crate::convert::header_map_str_pairs(headers))
}
pub fn extract<'a, I>(headers: I) -> std::result::Result<Self, Error>
where
I: IntoIterator<Item = (&'a str, &'a str)> + Clone,
{
let rfc6585 = headers::Headers::extract(headers.clone());
let retryafter = retry_after::RateLimit::extract(headers);
match (rfc6585, retryafter) {
(Ok(rfc6585), Ok(retryafter)) => {
if rfc6585.reset.duration() > retryafter.reset.duration() {
Ok(Self::Rfc6585(rfc6585))
} else {
Ok(Self::RetryAfter(retryafter))
}
}
(Ok(rfc6585), Err(_)) => Ok(Self::Rfc6585(rfc6585)),
(Err(_), Ok(retryafter)) => Ok(Self::RetryAfter(retryafter)),
(Err(e), Err(_)) => Err(e),
}
}
pub const fn is_limited(&self) -> bool {
match self {
Self::Rfc6585(headers) => headers.remaining == 0,
Self::RetryAfter(_) => true,
}
}
pub fn status(&self) -> Status {
if self.is_limited() {
Status::Wait(self.reset().duration())
} else {
Status::Ready
}
}
pub const fn reset(&self) -> ResetTime {
match self {
Self::Rfc6585(rfc6585) => rfc6585.reset,
Self::RetryAfter(retryafter) => retryafter.reset,
}
}
pub const fn limit(&self) -> Option<usize> {
match self {
Self::Rfc6585(rfc6585) => Some(rfc6585.limit),
Self::RetryAfter(_) => None,
}
}
pub const fn remaining(&self) -> Option<usize> {
match self {
Self::Rfc6585(rfc6585) => Some(rfc6585.remaining),
Self::RetryAfter(_) => None,
}
}
}
impl FromStr for RateLimit {
type Err = Error;
fn from_str(map: &str) -> Result<Self> {
RateLimit::extract(crate::convert::parse_header_lines(map))
}
}
#[cfg(feature = "reqwest")]
impl TryFrom<&reqwest::Response> for RateLimit {
type Error = Error;
fn try_from(response: &reqwest::Response) -> Result<Self> {
Self::extract(crate::convert::header_map_str_pairs(response.headers()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use indoc::indoc;
use std::str::FromStr;
use time::macros::datetime;
use crate::reset_time::ResetTime;
#[test]
fn use_later_reset_time_date() {
let headers = indoc! {"
X-Ratelimit-Used: 100
X-Ratelimit-Remaining: 22
X-Ratelimit-Reset: 30
Retry-After: Wed, 21 Oct 2099 07:28:00 GMT
"};
let rate = RateLimit::from_str(headers).unwrap();
assert_eq!(
rate.reset(),
ResetTime::DateTime(datetime!(2099-10-21 7:28:00.0 UTC))
);
}
#[test]
fn use_later_reset_time_seconds() {
let headers = indoc! {"
X-Ratelimit-Used: 100
X-Ratelimit-Remaining: 22
X-Ratelimit-Reset: 30
Retry-After: 20
"};
let rate = RateLimit::from_str(headers).unwrap();
assert_eq!(rate.reset(), ResetTime::Seconds(30));
}
#[test]
fn test_status_is_limited() {
let headers = indoc! {"
RateLimit-Limit: 10
RateLimit-Remaining: 0
RateLimit-Reset: 30
"};
let rate = RateLimit::from_str(headers).unwrap();
assert!(rate.is_limited());
match rate.status() {
Status::Wait(d) => assert_eq!(d, std::time::Duration::from_secs(30)),
_ => panic!("Expected Status::Wait"),
}
let headers = indoc! {"
RateLimit-Limit: 10
RateLimit-Remaining: 1
RateLimit-Reset: 30
"};
let rate = RateLimit::from_str(headers).unwrap();
assert!(!rate.is_limited());
assert_eq!(rate.status(), Status::Ready);
}
}