1use std::str::FromStr;
5
6use crate::{
7 error::{Error, Result},
8 parser::Parser,
9 reset_time::ResetTime,
10 vendors::{Vendor, VendorMask},
11};
12#[cfg(feature = "http")]
13use http::HeaderMap;
14use std::time::Duration;
15
16#[derive(Copy, Clone, Debug, PartialEq)]
18pub struct Headers {
19 pub limit: usize,
21 pub remaining: usize,
23 pub reset: ResetTime,
25 pub window: Option<Duration>,
29 pub vendor: Vendor,
31 pub candidates: VendorMask,
33}
34
35impl Headers {
36 #[cfg(feature = "http")]
62 pub fn new(headers: &HeaderMap) -> std::result::Result<Self, Error> {
63 Self::extract(crate::convert::header_map_str_pairs(headers))
64 }
65
66 pub fn extract<'a, I>(headers: I) -> std::result::Result<Self, Error>
68 where
69 I: IntoIterator<Item = (&'a str, &'a str)>,
70 {
71 let parser = Parser::new(headers);
72 parser.parse()
73 }
74
75 #[must_use]
77 pub const fn limit(&self) -> usize {
78 self.limit
79 }
80
81 #[must_use]
83 pub const fn remaining(&self) -> usize {
84 self.remaining
85 }
86
87 #[must_use]
89 pub const fn reset(&self) -> ResetTime {
90 self.reset
91 }
92}
93
94impl FromStr for Headers {
95 type Err = Error;
96
97 fn from_str(map: &str) -> Result<Self> {
98 Headers::extract(crate::convert::parse_header_lines(map))
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use crate::reset_time::ResetTimeKind;
106 use indoc::indoc;
107 use time::{OffsetDateTime, macros::datetime};
108
109 #[test]
110 fn parse_vendor() {
111 let map = Headers::from_str(
112 "x-ratelimit-limit: 5000\nx-ratelimit-remaining: 5\nx-ratelimit-reset: 1350085394",
113 )
114 .unwrap();
115 assert_eq!(map.vendor, Vendor::Generic);
116 assert!(map.candidates.contains(VendorMask::GITHUB));
117
118 let map =
119 Headers::from_str("RateLimit-Limit: 5000\nRateLimit-Remaining: 5\nRateLimit-Reset: 10")
120 .unwrap();
121 assert_eq!(map.vendor, Vendor::Generic);
122 }
123
124 #[test]
125 fn parse_reset_timestamp() {
126 let v = "1350085394";
128 assert_eq!(
129 ResetTime::new(v, ResetTimeKind::Timestamp).unwrap(),
130 ResetTime::DateTime(OffsetDateTime::from_unix_timestamp(1_350_085_394).unwrap())
131 );
132 }
133
134 #[test]
135 fn parse_reset_seconds() {
136 let v = "100";
137 assert_eq!(
138 ResetTime::new(v, ResetTimeKind::Seconds).unwrap(),
139 ResetTime::Seconds(100)
140 );
141 }
142
143 #[test]
144 fn parse_reset_datetime() {
145 let v = "Tue, 15 Nov 1994 08:12:31 GMT";
146 let d = ResetTime::new(v, ResetTimeKind::ImfFixdate);
147 assert_eq!(
148 d.unwrap(),
149 ResetTime::DateTime(datetime!(1994-11-15 8:12:31 UTC))
150 );
151 }
152
153 #[test]
154 fn parse_github_headers() {
155 let headers = indoc! {"
156 x-ratelimit-limit: 5000
157 x-ratelimit-remaining: 4987
158 x-ratelimit-reset: 1350085394
159 "};
160
161 let rate = Headers::from_str(headers).unwrap();
162 assert_eq!(rate.limit(), 5000);
163 assert_eq!(rate.remaining(), 4987);
164 assert_eq!(
165 rate.reset,
166 ResetTime::DateTime(OffsetDateTime::from_unix_timestamp(1_350_085_394).unwrap())
167 );
168 }
169
170 #[test]
171 fn parse_reddit_headers() {
172 let headers = indoc! {"
173 X-Ratelimit-Used: 100
174 X-Ratelimit-Remaining: 22
175 X-Ratelimit-Reset: 30
176 "};
177
178 let rate = Headers::from_str(headers).unwrap();
179 assert_eq!(rate.limit(), 122);
180 assert_eq!(rate.remaining(), 22);
181 assert_eq!(rate.reset, ResetTime::Seconds(30));
182 }
183
184 #[test]
185 fn parse_linear_headers() {
186 let headers = indoc! {"
187 X-RateLimit-Requests-Limit: 1500
188 X-RateLimit-Requests-Remaining: 1499
189 X-RateLimit-Requests-Reset: 1694721826678
190 "};
191
192 let rate = Headers::from_str(headers).unwrap();
193 assert_eq!(rate.limit(), 1500);
194 assert_eq!(rate.remaining(), 1499);
195 assert_eq!(
196 rate.reset,
197 ResetTime::DateTime(
198 OffsetDateTime::from_unix_timestamp_nanos(1_694_721_826_678_000_000).unwrap()
199 )
200 );
201 }
202
203 #[test]
204 fn parse_gitlab_headers() {
205 let headers = indoc! {"
206 RateLimit-Limit: 60
207 RateLimit-Observed: 67
208 RateLimit-Remaining: 0
209 RateLimit-Reset: 1609844400
210 "};
211
212 let rate = Headers::from_str(headers).unwrap();
213 assert_eq!(rate.limit(), 60);
214 assert_eq!(rate.remaining(), 0);
215 assert_eq!(
216 rate.reset,
217 ResetTime::DateTime(OffsetDateTime::from_unix_timestamp(1_609_844_400).unwrap())
218 );
219 }
220
221 #[test]
222 fn parse_twilio_headers() {
223 let headers = indoc! {"
224 X-RateLimit-Limit: 500
225 X-RateLimit-Remaining: 499
226 X-RateLimit-Reset: 1392815263
227 "};
228
229 let rate = Headers::from_str(headers).unwrap();
230 assert_eq!(rate.limit(), 500);
231 assert_eq!(rate.remaining(), 499);
232 assert_eq!(
233 rate.reset(),
234 ResetTime::DateTime(OffsetDateTime::from_unix_timestamp(1_392_815_263).unwrap())
235 );
236 }
237
238 #[test]
239 fn parse_vimeo_headers() {
240 let headers = indoc! {"
241 X-RateLimit-Limit: 500
242 X-RateLimit-Remaining: 499
243 X-RateLimit-Reset: Thu, 14 Sep 2023 21:00:00 GMT
244 "};
245
246 let rate = Headers::from_str(headers).unwrap();
247 assert_eq!(rate.limit(), 500);
248 assert_eq!(rate.remaining(), 499);
249 assert_eq!(rate.vendor, Vendor::Vimeo);
250 assert_eq!(
251 rate.reset(),
252 ResetTime::DateTime(datetime!(2023-09-14 21:00:00 UTC))
253 );
254 }
255
256 #[test]
257 fn parse_openai_headers() {
258 let headers = indoc! {"
259 x-ratelimit-limit-requests: 60
260 x-ratelimit-remaining-requests: 59
261 x-ratelimit-reset-requests: 1s
262 "};
263
264 let rate = Headers::from_str(headers).unwrap();
265 assert_eq!(rate.limit(), 60);
266 assert_eq!(rate.remaining(), 59);
267 assert_eq!(rate.vendor, Vendor::OpenAI);
268 assert_eq!(rate.reset(), ResetTime::Seconds(1));
269
270 let headers = indoc! {"
271 x-ratelimit-limit-requests: 60
272 x-ratelimit-remaining-requests: 59
273 x-ratelimit-reset-requests: 6m0s
274 "};
275
276 let rate = Headers::from_str(headers).unwrap();
277 assert_eq!(rate.reset(), ResetTime::Seconds(360));
278 }
279
280 #[test]
281 fn parse_unknown_headers() {
282 let headers = indoc! {"
283 X-Unknown-Limit: 5000
284 X-Unknown-Remaining: 4987
285 X-Unknown-Reset: 1350085394
286 "};
287
288 assert!(Headers::from_str(headers).is_err());
289 }
290
291 #[test]
292 fn parse_garbage_headers() {
293 let headers = indoc! {"
294 RateLimit-Limit: foo
295 Ratelimit-Remaining: bar
296 Ratelimit-Reset: baz
297 "};
298
299 assert!(Headers::from_str(headers).is_err());
301 }
302}