1use std::{collections::HashMap, hash::Hash, time::Duration};
19
20use bytes::Bytes;
21use http::{Method, StatusCode, status::InvalidStatusCode};
22use jiff::{Timestamp, fmt::rfc2822::DateTimeParser};
23
24#[derive(Clone, Debug)]
28pub struct HttpStatus {
29 inner: StatusCode,
30}
31
32impl HttpStatus {
33 #[must_use]
35 pub const fn new(code: StatusCode) -> Self {
36 Self { inner: code }
37 }
38
39 #[inline]
41 #[must_use]
42 pub const fn as_u16(&self) -> u16 {
43 self.inner.as_u16()
44 }
45
46 #[inline]
48 #[must_use]
49 pub fn as_str(&self) -> &str {
50 self.inner.as_str()
51 }
52
53 #[inline]
55 #[must_use]
56 pub fn is_informational(&self) -> bool {
57 self.inner.is_informational()
58 }
59
60 #[inline]
62 #[must_use]
63 pub fn is_success(&self) -> bool {
64 self.inner.is_success()
65 }
66
67 #[inline]
69 #[must_use]
70 pub fn is_redirection(&self) -> bool {
71 self.inner.is_redirection()
72 }
73
74 #[inline]
76 #[must_use]
77 pub fn is_client_error(&self) -> bool {
78 self.inner.is_client_error()
79 }
80
81 #[inline]
83 #[must_use]
84 pub fn is_server_error(&self) -> bool {
85 self.inner.is_server_error()
86 }
87}
88
89impl TryFrom<u16> for HttpStatus {
90 type Error = InvalidStatusCode;
91
92 fn try_from(code: u16) -> Result<Self, Self::Error> {
98 Ok(Self {
99 inner: StatusCode::from_u16(code)?,
100 })
101 }
102}
103
104#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
106pub enum HttpMethod {
107 GET,
108 POST,
109 PUT,
110 DELETE,
111 PATCH,
112}
113
114impl From<HttpMethod> for Method {
115 fn from(value: HttpMethod) -> Self {
116 match value {
117 HttpMethod::GET => Self::GET,
118 HttpMethod::POST => Self::POST,
119 HttpMethod::PUT => Self::PUT,
120 HttpMethod::DELETE => Self::DELETE,
121 HttpMethod::PATCH => Self::PATCH,
122 }
123 }
124}
125
126#[derive(Clone, Debug)]
128pub struct HttpResponse {
129 pub status: HttpStatus,
131 pub headers: HashMap<String, String>,
133 pub body: Bytes,
135}
136
137#[must_use]
141pub fn parse_retry_after(value: &str, now: Timestamp) -> Option<Duration> {
142 if let Ok(seconds) = value.parse::<u64>() {
143 return Some(Duration::from_secs(seconds));
144 }
145
146 let retry_at = DateTimeParser::new().parse_timestamp(value).ok()?;
147 let delay = retry_at.duration_since(now);
148 if delay.is_negative() {
149 Some(Duration::ZERO)
150 } else {
151 Some(delay.unsigned_abs())
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use rstest::rstest;
158
159 use super::*;
160
161 #[rstest]
162 #[case(100, [true, false, false, false, false])]
163 #[case(199, [true, false, false, false, false])]
164 #[case(200, [false, true, false, false, false])]
165 #[case(299, [false, true, false, false, false])]
166 #[case(300, [false, false, true, false, false])]
167 #[case(399, [false, false, true, false, false])]
168 #[case(400, [false, false, false, true, false])]
169 #[case(499, [false, false, false, true, false])]
170 #[case(500, [false, false, false, false, true])]
171 #[case(599, [false, false, false, false, true])]
172 #[case(600, [false; 5])]
173 #[case(999, [false; 5])]
174 fn status_classifies_boundaries(#[case] code: u16, #[case] expected: [bool; 5]) {
175 let status = HttpStatus::try_from(code).unwrap();
176
177 assert_eq!(status.as_u16(), code);
178 assert_eq!(status.as_str(), code.to_string());
179 assert_eq!(
180 [
181 status.is_informational(),
182 status.is_success(),
183 status.is_redirection(),
184 status.is_client_error(),
185 status.is_server_error()
186 ],
187 expected,
188 );
189 }
190
191 #[rstest]
192 #[case(0)]
193 #[case(99)]
194 #[case(1000)]
195 #[case(u16::MAX)]
196 fn status_rejects_invalid_code(#[case] code: u16) {
197 let error = HttpStatus::try_from(code).unwrap_err();
198
199 assert_eq!(error.to_string(), "invalid status code");
200 }
201
202 #[rstest]
203 #[case(HttpMethod::GET, Method::GET)]
204 #[case(HttpMethod::POST, Method::POST)]
205 #[case(HttpMethod::PUT, Method::PUT)]
206 #[case(HttpMethod::DELETE, Method::DELETE)]
207 #[case(HttpMethod::PATCH, Method::PATCH)]
208 fn method_conversion_preserves_verb(#[case] input: HttpMethod, #[case] expected: Method) {
209 assert_eq!(Method::from(input), expected);
210 }
211}