1use crate::time::{millis_to_secs, saturating_sub_ms};
2use http::header::{HeaderName, HeaderValue, RETRY_AFTER};
3use http::HeaderMap;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub struct RateLimitSnapshot {
8 pub allowed: bool,
10 pub limit: usize,
12 pub remaining: usize,
14 pub reset_at_ms: u64,
16}
17
18impl RateLimitSnapshot {
19 pub fn retry_after_ms(&self, now_ms: u64) -> u64 {
21 saturating_sub_ms(self.reset_at_ms, now_ms)
22 }
23
24 pub fn retry_after_secs(&self, now_ms: u64) -> u64 {
26 self.retry_after_ms(now_ms).div_ceil(1000).max(1)
27 }
28
29 pub fn to_headers(&self, now_ms: u64) -> HeaderMap {
31 let mut headers = HeaderMap::new();
32 insert_header(
33 &mut headers,
34 HeaderName::from_static("x-ratelimit-limit"),
35 self.limit,
36 );
37 insert_header(
38 &mut headers,
39 HeaderName::from_static("x-ratelimit-remaining"),
40 self.remaining,
41 );
42 insert_header(
43 &mut headers,
44 HeaderName::from_static("x-ratelimit-reset"),
45 millis_to_secs(self.reset_at_ms),
46 );
47
48 if !self.allowed {
49 insert_header(&mut headers, RETRY_AFTER, self.retry_after_secs(now_ms));
50 }
51
52 headers
53 }
54}
55
56#[derive(Debug, Clone, Copy)]
58pub struct RateLimitInfo(pub RateLimitSnapshot);
59
60fn insert_header(headers: &mut HeaderMap, name: HeaderName, value: impl ToString) {
61 if let Ok(value) = HeaderValue::from_str(&value.to_string()) {
62 headers.insert(name, value);
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 use crate::time::now_ms;
71
72 #[test]
73 fn headers_include_limit_metadata() {
74 let now_ms = now_ms();
75 let snapshot = RateLimitSnapshot {
76 allowed: false,
77 limit: 10,
78 remaining: 0,
79 reset_at_ms: now_ms + 30_000,
80 };
81
82 let headers = snapshot.to_headers(now_ms);
83 assert_eq!(
84 headers
85 .get("x-ratelimit-limit")
86 .and_then(|v| v.to_str().ok()),
87 Some("10")
88 );
89 assert_eq!(
90 headers
91 .get("x-ratelimit-remaining")
92 .and_then(|v| v.to_str().ok()),
93 Some("0")
94 );
95 assert!(headers.get(RETRY_AFTER).is_some());
96 }
97}