1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4#[derive(Debug, Clone, PartialEq, Eq, Hash)]
6pub enum HttpMethod {
7 Get,
8 Head,
9 Post,
10 Put,
11 Patch,
12 Delete,
13 Options,
14 Trace,
15 Connect,
16 Other(String),
17}
18
19#[must_use]
21pub fn parse_method(input: &str) -> Option<HttpMethod> {
22 let normalized = normalize_method(input)?;
23
24 Some(match normalized.as_str() {
25 "GET" => HttpMethod::Get,
26 "HEAD" => HttpMethod::Head,
27 "POST" => HttpMethod::Post,
28 "PUT" => HttpMethod::Put,
29 "PATCH" => HttpMethod::Patch,
30 "DELETE" => HttpMethod::Delete,
31 "OPTIONS" => HttpMethod::Options,
32 "TRACE" => HttpMethod::Trace,
33 "CONNECT" => HttpMethod::Connect,
34 _ => HttpMethod::Other(normalized),
35 })
36}
37
38#[must_use]
40pub fn normalize_method(input: &str) -> Option<String> {
41 let trimmed = input.trim();
42 if trimmed.is_empty() || trimmed.chars().any(char::is_whitespace) {
43 return None;
44 }
45
46 if trimmed.bytes().all(is_token_byte) {
47 Some(trimmed.to_ascii_uppercase())
48 } else {
49 None
50 }
51}
52
53#[must_use]
55pub fn is_standard_method(input: &str) -> bool {
56 matches!(
57 normalize_method(input).as_deref(),
58 Some(
59 "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "TRACE" | "CONNECT"
60 )
61 )
62}
63
64#[must_use]
66pub fn is_safe_method(input: &str) -> bool {
67 matches!(
68 normalize_method(input).as_deref(),
69 Some("GET" | "HEAD" | "OPTIONS" | "TRACE")
70 )
71}
72
73#[must_use]
75pub fn is_idempotent_method(input: &str) -> bool {
76 matches!(
77 normalize_method(input).as_deref(),
78 Some("GET" | "HEAD" | "OPTIONS" | "TRACE" | "PUT" | "DELETE")
79 )
80}
81
82#[must_use]
84pub fn method_allows_body(input: &str) -> bool {
85 match parse_method(input) {
86 Some(HttpMethod::Get | HttpMethod::Head | HttpMethod::Trace) | None => false,
87 Some(_) => true,
88 }
89}
90
91#[must_use]
93pub fn method_expects_body(input: &str) -> bool {
94 matches!(
95 normalize_method(input).as_deref(),
96 Some("POST" | "PUT" | "PATCH")
97 )
98}
99
100fn is_token_byte(byte: u8) -> bool {
101 byte.is_ascii_alphanumeric()
102 || matches!(
103 byte,
104 b'!' | b'#'
105 | b'$'
106 | b'%'
107 | b'&'
108 | b'\''
109 | b'*'
110 | b'+'
111 | b'-'
112 | b'.'
113 | b'^'
114 | b'_'
115 | b'`'
116 | b'|'
117 | b'~'
118 )
119}