1use std::fmt;
7use std::str::FromStr;
8
9use thiserror::Error;
10
11#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13pub enum Method {
14 Get,
15 Head,
16 Post,
17 Put,
18 Delete,
19 Connect,
20 Options,
21 Trace,
22 Patch,
23 Extension(Box<str>),
24}
25
26impl Method {
27 pub fn as_str(&self) -> &str {
29 match self {
30 Method::Get => "GET",
31 Method::Head => "HEAD",
32 Method::Post => "POST",
33 Method::Put => "PUT",
34 Method::Delete => "DELETE",
35 Method::Connect => "CONNECT",
36 Method::Options => "OPTIONS",
37 Method::Trace => "TRACE",
38 Method::Patch => "PATCH",
39 Method::Extension(token) => token,
40 }
41 }
42
43 pub fn is_idempotent(&self) -> bool {
45 matches!(
46 self,
47 Method::Get
48 | Method::Head
49 | Method::Put
50 | Method::Delete
51 | Method::Options
52 | Method::Trace
53 )
54 }
55
56 pub fn is_cacheable(&self) -> bool {
58 matches!(self, Method::Get | Method::Head)
59 }
60
61 pub fn is_safe(&self) -> bool {
63 matches!(
64 self,
65 Method::Get | Method::Head | Method::Options | Method::Trace
66 )
67 }
68
69 fn from_token(token: &str) -> Result<Self, MethodError> {
70 match token {
71 "GET" => Ok(Method::Get),
72 "HEAD" => Ok(Method::Head),
73 "POST" => Ok(Method::Post),
74 "PUT" => Ok(Method::Put),
75 "DELETE" => Ok(Method::Delete),
76 "CONNECT" => Ok(Method::Connect),
77 "OPTIONS" => Ok(Method::Options),
78 "TRACE" => Ok(Method::Trace),
79 "PATCH" => Ok(Method::Patch),
80 _ => {
81 if is_token(token) {
82 Ok(Method::Extension(token.into()))
83 } else {
84 Err(MethodError::InvalidToken)
85 }
86 }
87 }
88 }
89}
90
91impl fmt::Display for Method {
92 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93 f.write_str(self.as_str())
94 }
95}
96
97impl FromStr for Method {
98 type Err = MethodError;
99
100 fn from_str(s: &str) -> Result<Self, Self::Err> {
101 Method::from_token(s)
102 }
103}
104
105#[derive(Debug, Error, PartialEq, Eq)]
107pub enum MethodError {
108 #[error("invalid method token")]
109 InvalidToken,
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
114pub enum HttpVersion {
115 Http10,
116 Http11,
117 Other(u8, u8),
118}
119
120impl HttpVersion {
121 pub fn major(self) -> u8 {
123 match self {
124 HttpVersion::Http10 => 1,
125 HttpVersion::Http11 => 1,
126 HttpVersion::Other(major, _) => major,
127 }
128 }
129
130 pub fn minor(self) -> u8 {
132 match self {
133 HttpVersion::Http10 => 0,
134 HttpVersion::Http11 => 1,
135 HttpVersion::Other(_, minor) => minor,
136 }
137 }
138
139 pub const HTTP_1_1: HttpVersion = HttpVersion::Http11;
141}
142
143impl fmt::Display for HttpVersion {
144 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145 match self {
146 HttpVersion::Http10 => f.write_str("HTTP/1.0"),
147 HttpVersion::Http11 => f.write_str("HTTP/1.1"),
148 HttpVersion::Other(major, minor) => write!(f, "HTTP/{major}.{minor}"),
149 }
150 }
151}
152
153impl FromStr for HttpVersion {
154 type Err = VersionError;
155
156 fn from_str(s: &str) -> Result<Self, Self::Err> {
157 if !s.starts_with("HTTP/") {
158 return Err(VersionError::InvalidPrefix);
159 }
160 let remainder = &s[5..];
161 let mut parts = remainder.split('.');
162 let major = parts
163 .next()
164 .ok_or(VersionError::InvalidFormat)?
165 .parse::<u8>()
166 .map_err(|_| VersionError::InvalidNumber)?;
167 let minor = parts
168 .next()
169 .ok_or(VersionError::InvalidFormat)?
170 .parse::<u8>()
171 .map_err(|_| VersionError::InvalidNumber)?;
172 if parts.next().is_some() {
173 return Err(VersionError::InvalidFormat);
174 }
175 Ok(match (major, minor) {
176 (1, 0) => HttpVersion::Http10,
177 (1, 1) => HttpVersion::Http11,
178 (maj, min) => HttpVersion::Other(maj, min),
179 })
180 }
181}
182
183#[derive(Debug, Error, PartialEq, Eq)]
185pub enum VersionError {
186 #[error("invalid HTTP version prefix")]
187 InvalidPrefix,
188 #[error("invalid HTTP version format")]
189 InvalidFormat,
190 #[error("invalid HTTP version number")]
191 InvalidNumber,
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
196pub struct StatusCode(u16);
197
198impl StatusCode {
199 pub fn from_u16(code: u16) -> Result<Self, StatusCodeError> {
201 if (100..=599).contains(&code) {
202 Ok(StatusCode(code))
203 } else {
204 Err(StatusCodeError::OutOfRange(code))
205 }
206 }
207
208 pub fn as_u16(self) -> u16 {
210 self.0
211 }
212
213 pub fn canonical_reason(self) -> Option<&'static str> {
215 match self.0 {
216 100 => Some("Continue"),
217 101 => Some("Switching Protocols"),
218 102 => Some("Processing"),
219 103 => Some("Early Hints"),
220 200 => Some("OK"),
221 201 => Some("Created"),
222 202 => Some("Accepted"),
223 203 => Some("Non-Authoritative Information"),
224 204 => Some("No Content"),
225 205 => Some("Reset Content"),
226 206 => Some("Partial Content"),
227 300 => Some("Multiple Choices"),
228 301 => Some("Moved Permanently"),
229 302 => Some("Found"),
230 303 => Some("See Other"),
231 304 => Some("Not Modified"),
232 305 => Some("Use Proxy"),
233 307 => Some("Temporary Redirect"),
234 308 => Some("Permanent Redirect"),
235 400 => Some("Bad Request"),
236 401 => Some("Unauthorized"),
237 402 => Some("Payment Required"),
238 403 => Some("Forbidden"),
239 404 => Some("Not Found"),
240 405 => Some("Method Not Allowed"),
241 406 => Some("Not Acceptable"),
242 407 => Some("Proxy Authentication Required"),
243 408 => Some("Request Timeout"),
244 409 => Some("Conflict"),
245 410 => Some("Gone"),
246 411 => Some("Length Required"),
247 412 => Some("Precondition Failed"),
248 413 => Some("Content Too Large"),
249 414 => Some("URI Too Long"),
250 415 => Some("Unsupported Media Type"),
251 416 => Some("Range Not Satisfiable"),
252 417 => Some("Expectation Failed"),
253 418 => Some("I'm a teapot"),
254 421 => Some("Misdirected Request"),
255 422 => Some("Unprocessable Content"),
256 426 => Some("Upgrade Required"),
257 428 => Some("Precondition Required"),
258 429 => Some("Too Many Requests"),
259 431 => Some("Request Header Fields Too Large"),
260 451 => Some("Unavailable For Legal Reasons"),
261 500 => Some("Internal Server Error"),
262 501 => Some("Not Implemented"),
263 502 => Some("Bad Gateway"),
264 503 => Some("Service Unavailable"),
265 504 => Some("Gateway Timeout"),
266 505 => Some("HTTP Version Not Supported"),
267 511 => Some("Network Authentication Required"),
268 _ => None,
269 }
270 }
271
272 pub const CONTINUE: StatusCode = StatusCode(100);
273 pub const OK: StatusCode = StatusCode(200);
274 pub const CREATED: StatusCode = StatusCode(201);
275 pub const PARTIAL_CONTENT: StatusCode = StatusCode(206);
276 pub const NOT_FOUND: StatusCode = StatusCode(404);
277 pub const NOT_MODIFIED: StatusCode = StatusCode(304);
278 pub const BAD_REQUEST: StatusCode = StatusCode(400);
279 pub const REQUEST_TIMEOUT: StatusCode = StatusCode(408);
280 pub const METHOD_NOT_ALLOWED: StatusCode = StatusCode(405);
281 pub const PRECONDITION_FAILED: StatusCode = StatusCode(412);
282 pub const RANGE_NOT_SATISFIABLE: StatusCode = StatusCode(416);
283 pub const EXPECTATION_FAILED: StatusCode = StatusCode(417);
284 pub const INTERNAL_SERVER_ERROR: StatusCode = StatusCode(500);
285 pub const NOT_IMPLEMENTED: StatusCode = StatusCode(501);
286}
287
288impl fmt::Display for StatusCode {
289 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
290 write!(f, "{}", self.0)
291 }
292}
293
294impl TryFrom<u16> for StatusCode {
295 type Error = StatusCodeError;
296
297 fn try_from(value: u16) -> Result<Self, Self::Error> {
298 StatusCode::from_u16(value)
299 }
300}
301
302impl From<StatusCode> for u16 {
303 fn from(code: StatusCode) -> Self {
304 code.0
305 }
306}
307
308#[derive(Debug, Error, PartialEq, Eq)]
310pub enum StatusCodeError {
311 #[error("status code {0} is outside the valid range 100-599")]
312 OutOfRange(u16),
313}
314
315fn is_token(value: &str) -> bool {
316 !value.is_empty() && value.bytes().all(is_tchar)
317}
318
319const fn is_tchar(byte: u8) -> bool {
320 matches!(
321 byte,
322 b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+' | b'-' | b'.' | b'^' | b'_' | b'`'
323 | b'|' | b'~'
324 | b'0'..=b'9'
325 | b'A'..=b'Z'
326 | b'a'..=b'z'
327 )
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 #[test]
335 fn method_parsing_standard() {
336 assert_eq!(Method::from_str("GET").unwrap(), Method::Get);
337 assert_eq!(Method::from_str("POST").unwrap(), Method::Post);
338 }
339
340 #[test]
341 fn method_parsing_extension() {
342 let ext = Method::from_str("FOO").unwrap();
343 assert!(matches!(ext, Method::Extension(_)));
344 assert_eq!(ext.as_str(), "FOO");
345 }
346
347 #[test]
348 fn method_invalid_token() {
349 assert_eq!(Method::from_str("inv alid"), Err(MethodError::InvalidToken));
350 assert_eq!(Method::from_str(""), Err(MethodError::InvalidToken));
351 }
352
353 #[test]
354 fn version_parsing() {
355 assert_eq!(
356 HttpVersion::from_str("HTTP/1.1").unwrap(),
357 HttpVersion::Http11
358 );
359 assert_eq!(
360 HttpVersion::from_str("HTTP/1.0").unwrap(),
361 HttpVersion::Http10
362 );
363 assert_eq!(
364 HttpVersion::from_str("HTTP/2.0").unwrap(),
365 HttpVersion::Other(2, 0)
366 );
367 assert!(HttpVersion::from_str("HTTP/1").is_err());
368 }
369
370 #[test]
371 fn status_code_bounds() {
372 assert!(StatusCode::from_u16(99).is_err());
373 assert!(StatusCode::from_u16(600).is_err());
374 assert_eq!(StatusCode::from_u16(200).unwrap().as_u16(), 200);
375 }
376}