1#![warn(clippy::all)]
75#![warn(
76 absolute_paths_not_starting_with_crate,
77 rustdoc::invalid_html_tags,
78 missing_copy_implementations,
79 missing_debug_implementations,
80 semicolon_in_expressions_from_macros,
81 unreachable_pub,
82 unused_crate_dependencies,
83 unused_extern_crates,
84 variant_size_differences,
85 clippy::missing_const_for_fn
86)]
87#![deny(anonymous_parameters, macro_use_extern_crate, pointer_structural_match)]
88#![deny(missing_docs)]
89#![allow(clippy::module_name_repetitions)]
90
91mod convert;
92mod error;
93mod types;
94mod variants;
95
96use std::str::FromStr;
97
98use error::{Error, Result};
99use headers::HeaderValue;
100use types::CaseSensitiveHeaderMap;
101use variants::RATE_LIMIT_HEADERS;
102
103use time::{format_description::well_known::Rfc2822, Date, Duration};
104use types::Used;
105pub use types::{Limit, RateLimitVariant, Remaining, ResetTime, ResetTimeKind, Vendor};
106
107#[derive(Copy, Clone, Debug, PartialEq)]
109pub struct RateLimit {
110 pub limit: usize,
112 pub remaining: usize,
114 pub reset: ResetTime,
116 pub window: Option<Duration>,
120 pub vendor: Vendor,
122}
123
124impl RateLimit {
125 pub fn new<T: Into<CaseSensitiveHeaderMap>>(headers: T) -> std::result::Result<Self, Error> {
131 let headers = headers.into();
132 let value = Self::get_remaining_header(&headers)?;
133 let remaining = Remaining::new(value.to_str()?)?;
134
135 let (limit, variant) = if let Ok((limit, variant)) = Self::get_rate_limit_header(&headers) {
136 (Limit::new(limit.to_str()?)?, variant)
137 } else if let Ok((used, variant)) = Self::get_used_header(&headers) {
138 let used = Used::new(used.to_str()?)?;
141 let limit = used.count + remaining.count;
142 (Limit::from(limit), variant)
143 } else {
144 return Err(Error::MissingUsed);
145 };
146
147 let reset = if let Some(date_or_seconds) = Self::get_retry_after_header(&headers) {
149 if Date::parse(date_or_seconds.to_str()?, &Rfc2822).is_ok() {
150 ResetTime::new(date_or_seconds, ResetTimeKind::ImfFixdate)?
151 } else {
152 ResetTime::new(date_or_seconds, ResetTimeKind::Seconds)?
153 }
154 } else {
155 let (value, kind) = Self::get_reset_header(&headers)?;
156 ResetTime::new(value, kind)?
157 };
158
159 Ok(RateLimit {
160 limit: limit.count,
161 remaining: remaining.count,
162 reset,
163 window: variant.duration,
164 vendor: variant.vendor,
165 })
166 }
167
168 fn get_rate_limit_header(
169 header_map: &CaseSensitiveHeaderMap,
170 ) -> Result<(&HeaderValue, RateLimitVariant)> {
171 let variants = RATE_LIMIT_HEADERS.lock().map_err(|_| Error::Lock)?;
172
173 for variant in variants.iter() {
174 if let Some(limit) = &variant.limit_header {
175 if let Some(value) = header_map.get(limit) {
176 return Ok((value, variant.clone()));
177 }
178 }
179 }
180 Err(Error::MissingLimit)
181 }
182
183 fn get_used_header(
184 header_map: &CaseSensitiveHeaderMap,
185 ) -> Result<(&HeaderValue, RateLimitVariant)> {
186 let variants = RATE_LIMIT_HEADERS.lock().map_err(|_| Error::Lock)?;
187
188 for variant in variants.iter() {
189 if let Some(used) = &variant.used_header {
190 if let Some(value) = header_map.get(used) {
191 return Ok((value, variant.clone()));
192 }
193 }
194 }
195 Err(Error::MissingUsed)
196 }
197
198 fn get_remaining_header(header_map: &CaseSensitiveHeaderMap) -> Result<&HeaderValue> {
199 let variants = RATE_LIMIT_HEADERS.lock().map_err(|_| Error::Lock)?;
200
201 for variant in variants.iter() {
202 if let Some(value) = header_map.get(&variant.remaining_header) {
203 return Ok(value);
204 }
205 }
206 Err(Error::MissingRemaining)
207 }
208
209 fn get_reset_header(
210 header_map: &CaseSensitiveHeaderMap,
211 ) -> Result<(&HeaderValue, ResetTimeKind)> {
212 let variants = RATE_LIMIT_HEADERS.lock().map_err(|_| Error::Lock)?;
213
214 for variant in variants.iter() {
215 if let Some(value) = header_map.get(&variant.reset_header) {
216 return Ok((value, variant.reset_kind));
217 }
218 }
219 Err(Error::MissingRemaining)
220 }
221
222 fn get_retry_after_header(header_map: &CaseSensitiveHeaderMap) -> Option<&HeaderValue> {
223 header_map.get("Retry-After")
224 }
225
226 #[must_use]
228 pub const fn limit(&self) -> usize {
229 self.limit
230 }
231
232 #[must_use]
234 pub const fn remaining(&self) -> usize {
235 self.remaining
236 }
237
238 #[must_use]
240 pub const fn reset(&self) -> ResetTime {
241 self.reset
242 }
243}
244
245impl FromStr for RateLimit {
246 type Err = Error;
247
248 fn from_str(map: &str) -> Result<Self> {
249 RateLimit::new(CaseSensitiveHeaderMap::from_str(map)?)
250 }
251}
252
253#[cfg(test)]
254mod tests {
255
256 use crate::types::HeaderMapExt;
257
258 use super::*;
259 use headers::HeaderMap;
260 use indoc::indoc;
261 use time::{macros::datetime, OffsetDateTime};
262
263 #[test]
264 fn parse_limit_value() {
265 let limit = Limit::new(" 23 ").unwrap();
266 assert_eq!(limit.count, 23);
267 }
268
269 #[test]
270 fn parse_invalid_limit_value() {
271 assert!(Limit::new("foo").is_err());
272 assert!(Limit::new("0 foo").is_err());
273 assert!(Limit::new("bar 0").is_err());
274 }
275
276 #[test]
277 fn parse_vendor() {
278 let map = CaseSensitiveHeaderMap::from_str("x-ratelimit-limit: 5000").unwrap();
279 let (_, variant) = RateLimit::get_rate_limit_header(&map).unwrap();
280 assert_eq!(variant.vendor, Vendor::Github);
281
282 let map = CaseSensitiveHeaderMap::from_str("RateLimit-Limit: 5000").unwrap();
283 let (_, variant) = RateLimit::get_rate_limit_header(&map).unwrap();
284 assert_eq!(variant.vendor, Vendor::Standard);
285 }
286
287 #[test]
288 fn parse_retry_after_seconds() {
289 let map = CaseSensitiveHeaderMap::from_str("Retry-After: 30").unwrap();
290 let retry = RateLimit::get_retry_after_header(&map).unwrap();
291
292 assert_eq!("30", retry);
293 }
294
295 #[test]
296 fn parse_remaining_value() {
297 let remaining = Remaining::new(" 23 ").unwrap();
298 assert_eq!(remaining.count, 23);
299 }
300
301 #[test]
302 fn parse_invalid_remaining_value() {
303 assert!(Remaining::new("foo").is_err());
304 assert!(Remaining::new("0 foo").is_err());
305 assert!(Remaining::new("bar 0").is_err());
306 }
307
308 #[test]
309 fn parse_reset_timestamp() {
310 let v = HeaderValue::from_str("1350085394").unwrap();
311 assert_eq!(
312 ResetTime::new(&v, ResetTimeKind::Timestamp).unwrap(),
313 ResetTime::DateTime(OffsetDateTime::from_unix_timestamp(1_350_085_394).unwrap())
314 );
315 }
316
317 #[test]
318 fn parse_reset_seconds() {
319 let v = HeaderValue::from_str("100").unwrap();
320 assert_eq!(
321 ResetTime::new(&v, ResetTimeKind::Seconds).unwrap(),
322 ResetTime::Seconds(100)
323 );
324 }
325
326 #[test]
327 fn parse_reset_datetime() {
328 let v = HeaderValue::from_str("Tue, 15 Nov 1994 08:12:31 GMT").unwrap();
329 let d = ResetTime::new(&v, ResetTimeKind::ImfFixdate);
330 assert_eq!(
331 d.unwrap(),
332 ResetTime::DateTime(datetime!(1994-11-15 8:12:31 UTC))
333 );
334 }
335
336 #[test]
337 fn parse_header_map_newlines() {
338 let map = HeaderMap::from_raw(
339 "x-ratelimit-limit: 5000
340x-ratelimit-remaining: 4987
341x-ratelimit-reset: 1350085394
342",
343 )
344 .unwrap();
345
346 assert_eq!(map.len(), 3);
347 assert_eq!(
348 map.get("x-ratelimit-limit"),
349 Some(&HeaderValue::from_str("5000").unwrap())
350 );
351 assert_eq!(
352 map.get("x-ratelimit-remaining"),
353 Some(&HeaderValue::from_str("4987").unwrap())
354 );
355 assert_eq!(
356 map.get("x-ratelimit-reset"),
357 Some(&HeaderValue::from_str("1350085394").unwrap())
358 );
359 }
360
361 #[test]
362 fn parse_github_headers() {
363 let headers = indoc! {"
364 x-ratelimit-limit: 5000
365 x-ratelimit-remaining: 4987
366 x-ratelimit-reset: 1350085394
367 "};
368
369 let rate = RateLimit::from_str(headers).unwrap();
370 assert_eq!(rate.limit(), 5000);
371 assert_eq!(rate.remaining(), 4987);
372 assert_eq!(
373 rate.reset(),
374 ResetTime::DateTime(OffsetDateTime::from_unix_timestamp(1_350_085_394).unwrap())
375 );
376 }
377
378 #[test]
379 fn parse_reddit_headers() {
380 let headers = indoc! {"
381 X-Ratelimit-Used: 100
382 X-Ratelimit-Remaining: 22
383 X-Ratelimit-Reset: 30
384 "};
385
386 let rate = RateLimit::from_str(headers).unwrap();
387 assert_eq!(rate.limit(), 122);
388 assert_eq!(rate.remaining(), 22);
389 assert_eq!(rate.reset(), ResetTime::Seconds(30));
390 }
391
392 #[test]
393 fn parse_gitlab_headers() {
394 let headers = indoc! {"
395 RateLimit-Limit: 60
396 RateLimit-Observed: 67
397 RateLimit-Remaining: 0
398 RateLimit-Reset: 1609844400
399 "};
400
401 let rate = RateLimit::from_str(headers).unwrap();
402 assert_eq!(rate.limit(), 60);
403 assert_eq!(rate.remaining(), 0);
404 assert_eq!(
405 rate.reset(),
406 ResetTime::DateTime(OffsetDateTime::from_unix_timestamp(1_609_844_400).unwrap())
407 );
408 }
409
410 #[test]
411 fn retry_after_seconds_takes_precedence_over_reset() {
412 let headers = indoc! {"
413 X-Ratelimit-Used: 100
414 X-Ratelimit-Remaining: 22
415 X-Ratelimit-Reset: 30
416 Retry-After: 20
417 "};
418
419 let rate = RateLimit::from_str(headers).unwrap();
420 assert_eq!(rate.limit(), 122);
421 assert_eq!(rate.remaining(), 22);
422 assert_eq!(rate.reset(), ResetTime::Seconds(20));
423 }
424
425 #[test]
426 fn retry_after_date_takes_precedence_over_reset() {
427 let headers = indoc! {"
428 X-Ratelimit-Used: 100
429 X-Ratelimit-Remaining: 22
430 X-Ratelimit-Reset: 30
431 Retry-After: Wed, 21 Oct 2015 07:28:00 GMT
432 "};
433
434 let rate = RateLimit::from_str(headers).unwrap();
435 assert_eq!(rate.limit(), 122);
436 assert_eq!(rate.remaining(), 22);
437 assert_eq!(
438 rate.reset(),
439 ResetTime::DateTime(datetime!(2015-10-21 7:28:00.0 UTC))
440 );
441 }
442}