hypersync_client/
rate_limit.rs1#[derive(Debug, Clone, Default)]
10pub struct RateLimitInfo {
11 pub limit: Option<u64>,
16 pub remaining: Option<u64>,
21 pub reset_secs: Option<u64>,
25 pub cost: Option<u64>,
30 pub retry_after_secs: Option<u64>,
35}
36
37impl std::fmt::Display for RateLimitInfo {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 let mut parts = Vec::new();
40 if let (Some(remaining), Some(limit)) = (self.remaining, self.limit) {
41 let cost = self.cost.unwrap_or(1);
42 parts.push(format!(
43 "remaining={}/{} reqs ({}/{} budget, cost={})",
44 remaining / cost,
45 limit / cost,
46 remaining,
47 limit,
48 cost
49 ));
50 } else {
51 if let Some(remaining) = self.remaining {
52 parts.push(format!("remaining={remaining}"));
53 }
54 if let Some(limit) = self.limit {
55 parts.push(format!("limit={limit}"));
56 }
57 }
58 if let Some(reset) = self.reset_secs {
59 parts.push(format!("resets_in={reset}s"));
60 }
61 if let Some(retry) = self.retry_after_secs {
62 parts.push(format!("retry_after={retry}s"));
63 }
64 write!(f, "{}", parts.join(", "))
65 }
66}
67
68impl RateLimitInfo {
69 pub(crate) fn from_response(res: &reqwest::Response) -> Self {
73 Self {
74 limit: Self::parse_limit_header(res),
75 remaining: Self::parse_u64_header(res, "x-ratelimit-remaining"),
76 reset_secs: Self::parse_u64_header(res, "x-ratelimit-reset"),
77 cost: Self::parse_u64_header(res, "x-ratelimit-cost"),
78 retry_after_secs: Self::parse_u64_header(res, "retry-after"),
79 }
80 }
81
82 pub fn is_rate_limited(&self) -> bool {
85 self.remaining == Some(0) || self.retry_after_secs.is_some()
86 }
87
88 pub fn suggested_wait_secs(&self) -> Option<u64> {
92 self.retry_after_secs.or(self.reset_secs)
93 }
94
95 fn parse_limit_header(res: &reqwest::Response) -> Option<u64> {
98 let value = res.headers().get("x-ratelimit-limit")?.to_str().ok()?;
99 let first = value.split(',').next()?.trim();
101 first.parse().ok()
102 }
103
104 fn parse_u64_header(res: &reqwest::Response, name: &str) -> Option<u64> {
106 res.headers().get(name)?.to_str().ok()?.trim().parse().ok()
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn test_is_rate_limited() {
116 let info = RateLimitInfo {
117 remaining: Some(0),
118 ..Default::default()
119 };
120 assert!(info.is_rate_limited());
121
122 let info = RateLimitInfo {
123 remaining: Some(5),
124 ..Default::default()
125 };
126 assert!(!info.is_rate_limited());
127
128 let info = RateLimitInfo::default();
129 assert!(!info.is_rate_limited());
130
131 let info = RateLimitInfo {
133 retry_after_secs: Some(5),
134 ..Default::default()
135 };
136 assert!(info.is_rate_limited());
137
138 let info = RateLimitInfo {
140 remaining: Some(10),
141 retry_after_secs: Some(3),
142 ..Default::default()
143 };
144 assert!(info.is_rate_limited());
145 }
146
147 #[test]
148 fn test_from_response_header_case_insensitive() {
149 let http_resp = http::Response::builder()
152 .header("X-RateLimit-Remaining", "42")
153 .header("X-RATELIMIT-RESET", "30")
154 .header("X-Ratelimit-Limit", "100, 100;w=60")
155 .header("X-Ratelimit-Cost", "10")
156 .header("Retry-After", "5")
157 .body("")
158 .unwrap();
159 let resp: reqwest::Response = http_resp.into();
160
161 let info = RateLimitInfo::from_response(&resp);
162 assert_eq!(info.limit, Some(100));
163 assert_eq!(info.remaining, Some(42));
164 assert_eq!(info.reset_secs, Some(30));
165 assert_eq!(info.cost, Some(10));
166 assert_eq!(info.retry_after_secs, Some(5));
167 }
168
169 #[test]
170 fn test_suggested_wait_secs() {
171 let info = RateLimitInfo {
173 retry_after_secs: Some(5),
174 reset_secs: Some(30),
175 ..Default::default()
176 };
177 assert_eq!(info.suggested_wait_secs(), Some(5));
178
179 let info = RateLimitInfo {
181 reset_secs: Some(30),
182 ..Default::default()
183 };
184 assert_eq!(info.suggested_wait_secs(), Some(30));
185
186 let info = RateLimitInfo::default();
188 assert_eq!(info.suggested_wait_secs(), None);
189 }
190
191 #[test]
192 fn test_display_full() {
193 let info = RateLimitInfo {
194 limit: Some(50),
195 remaining: Some(0),
196 reset_secs: Some(59),
197 cost: Some(10),
198 retry_after_secs: Some(5),
199 };
200 assert_eq!(
201 info.to_string(),
202 "remaining=0/5 reqs (0/50 budget, cost=10), resets_in=59s, retry_after=5s"
203 );
204 }
205
206 #[test]
207 fn test_display_partial() {
208 let info = RateLimitInfo {
209 remaining: Some(3),
210 reset_secs: Some(30),
211 ..Default::default()
212 };
213 assert_eq!(info.to_string(), "remaining=3, resets_in=30s");
214 }
215
216 #[test]
217 fn test_display_empty() {
218 let info = RateLimitInfo::default();
219 assert_eq!(info.to_string(), "");
220 }
221}