hypersync_client/
rate_limit.rs1#[derive(Debug, Clone, Default)]
9pub struct RateLimitInfo {
10 pub limit: Option<u64>,
15 pub remaining: Option<u64>,
20 pub reset_secs: Option<u64>,
24 pub cost: Option<u64>,
29}
30
31impl std::fmt::Display for RateLimitInfo {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 let mut parts = Vec::new();
34 if let (Some(remaining), Some(limit)) = (self.remaining, self.limit) {
35 let cost = self.cost.unwrap_or(1);
36 parts.push(format!(
37 "remaining={}/{} reqs",
38 remaining / cost,
39 limit / cost,
40 ));
41 } else {
42 if let Some(remaining) = self.remaining {
43 parts.push(format!("remaining={remaining}"));
44 }
45 if let Some(limit) = self.limit {
46 parts.push(format!("limit={limit}"));
47 }
48 }
49 if let Some(reset) = self.reset_secs {
50 parts.push(format!("resets_in={reset}s"));
51 }
52 write!(f, "{}", parts.join(", "))
53 }
54}
55
56impl RateLimitInfo {
57 pub(crate) fn from_response(res: &reqwest::Response) -> Self {
61 Self {
62 limit: Self::parse_limit_header(res),
63 remaining: Self::parse_u64_header(res, "x-ratelimit-remaining"),
64 reset_secs: Self::parse_u64_header(res, "x-ratelimit-reset"),
65 cost: Self::parse_u64_header(res, "x-ratelimit-cost"),
66 }
67 }
68
69 pub fn is_rate_limited(&self) -> bool {
71 self.remaining == Some(0)
72 }
73
74 pub fn suggested_wait_secs(&self) -> Option<u64> {
76 self.reset_secs
77 }
78
79 fn parse_limit_header(res: &reqwest::Response) -> Option<u64> {
82 let value = res.headers().get("x-ratelimit-limit")?.to_str().ok()?;
83 let first = value.split(',').next()?.trim();
85 first.parse().ok()
86 }
87
88 fn parse_u64_header(res: &reqwest::Response, name: &str) -> Option<u64> {
90 res.headers().get(name)?.to_str().ok()?.trim().parse().ok()
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn test_is_rate_limited() {
100 let info = RateLimitInfo {
101 remaining: Some(0),
102 ..Default::default()
103 };
104 assert!(info.is_rate_limited());
105
106 let info = RateLimitInfo {
107 remaining: Some(5),
108 ..Default::default()
109 };
110 assert!(!info.is_rate_limited());
111
112 let info = RateLimitInfo::default();
113 assert!(!info.is_rate_limited());
114 }
115
116 #[test]
117 fn test_from_response_header_case_insensitive() {
118 let http_resp = http::Response::builder()
121 .header("X-RateLimit-Remaining", "42")
122 .header("X-RATELIMIT-RESET", "30")
123 .header("X-Ratelimit-Limit", "100, 100;w=60")
124 .header("X-Ratelimit-Cost", "10")
125 .body("")
126 .unwrap();
127 let resp: reqwest::Response = http_resp.into();
128
129 let info = RateLimitInfo::from_response(&resp);
130 assert_eq!(info.limit, Some(100));
131 assert_eq!(info.remaining, Some(42));
132 assert_eq!(info.reset_secs, Some(30));
133 assert_eq!(info.cost, Some(10));
134 }
135
136 #[test]
137 fn test_suggested_wait_secs() {
138 let info = RateLimitInfo {
140 reset_secs: Some(30),
141 ..Default::default()
142 };
143 assert_eq!(info.suggested_wait_secs(), Some(30));
144
145 let info = RateLimitInfo::default();
147 assert_eq!(info.suggested_wait_secs(), None);
148 }
149
150 #[test]
151 fn test_display_full() {
152 let info = RateLimitInfo {
153 limit: Some(50),
154 remaining: Some(0),
155 reset_secs: Some(59),
156 cost: Some(10),
157 };
158 assert_eq!(info.to_string(), "remaining=0/5 reqs, resets_in=59s");
159 }
160
161 #[test]
162 fn test_display_partial() {
163 let info = RateLimitInfo {
164 remaining: Some(3),
165 reset_secs: Some(30),
166 ..Default::default()
167 };
168 assert_eq!(info.to_string(), "remaining=3, resets_in=30s");
169 }
170
171 #[test]
172 fn test_display_empty() {
173 let info = RateLimitInfo::default();
174 assert_eq!(info.to_string(), "");
175 }
176}