1use core::{fmt, str::FromStr};
22#[cfg(feature = "serde")]
23use serde::{Deserialize, Serialize};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
32#[cfg_attr(feature = "serde", serde(rename_all = "UPPERCASE"))]
33#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
34#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
35#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
36#[cfg_attr(feature = "proptest", derive(proptest_derive::Arbitrary))]
37pub enum HttpMethod {
38 Get,
40 Head,
42 Post,
44 Put,
46 Delete,
48 Connect,
50 Options,
52 Trace,
54 Patch,
56}
57
58impl HttpMethod {
59 #[must_use]
67 pub const fn as_str(&self) -> &'static str {
68 match self {
69 Self::Get => "GET",
70 Self::Head => "HEAD",
71 Self::Post => "POST",
72 Self::Put => "PUT",
73 Self::Delete => "DELETE",
74 Self::Connect => "CONNECT",
75 Self::Options => "OPTIONS",
76 Self::Trace => "TRACE",
77 Self::Patch => "PATCH",
78 }
79 }
80
81 #[must_use]
92 pub const fn is_safe(&self) -> bool {
93 matches!(self, Self::Get | Self::Head | Self::Options | Self::Trace)
94 }
95
96 #[must_use]
107 pub const fn is_idempotent(&self) -> bool {
108 matches!(
109 self,
110 Self::Get | Self::Head | Self::Put | Self::Delete | Self::Options | Self::Trace
111 )
112 }
113}
114
115impl fmt::Display for HttpMethod {
120 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121 f.write_str(self.as_str())
122 }
123}
124
125#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct ParseHttpMethodError;
128
129impl fmt::Display for ParseHttpMethodError {
130 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131 f.write_str("unknown HTTP method")
132 }
133}
134
135#[cfg(feature = "std")]
136impl std::error::Error for ParseHttpMethodError {}
137
138impl FromStr for HttpMethod {
139 type Err = ParseHttpMethodError;
140
141 fn from_str(s: &str) -> Result<Self, Self::Err> {
142 match s.trim() {
143 "GET" => Ok(Self::Get),
144 "HEAD" => Ok(Self::Head),
145 "POST" => Ok(Self::Post),
146 "PUT" => Ok(Self::Put),
147 "DELETE" => Ok(Self::Delete),
148 "CONNECT" => Ok(Self::Connect),
149 "OPTIONS" => Ok(Self::Options),
150 "TRACE" => Ok(Self::Trace),
151 "PATCH" => Ok(Self::Patch),
152 _ => Err(ParseHttpMethodError),
153 }
154 }
155}
156
157#[cfg(feature = "http")]
162mod http_interop {
163 use super::HttpMethod;
164
165 impl From<HttpMethod> for http::Method {
166 fn from(m: HttpMethod) -> Self {
167 match m {
168 HttpMethod::Get => Self::GET,
169 HttpMethod::Head => Self::HEAD,
170 HttpMethod::Post => Self::POST,
171 HttpMethod::Put => Self::PUT,
172 HttpMethod::Delete => Self::DELETE,
173 HttpMethod::Connect => Self::CONNECT,
174 HttpMethod::Options => Self::OPTIONS,
175 HttpMethod::Trace => Self::TRACE,
176 HttpMethod::Patch => Self::PATCH,
177 }
178 }
179 }
180
181 impl TryFrom<http::Method> for HttpMethod {
182 type Error = super::ParseHttpMethodError;
183
184 fn try_from(m: http::Method) -> Result<Self, Self::Error> {
185 m.as_str().parse()
186 }
187 }
188}
189
190#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn as_str_round_trips() {
200 let methods = [
201 HttpMethod::Get,
202 HttpMethod::Head,
203 HttpMethod::Post,
204 HttpMethod::Put,
205 HttpMethod::Delete,
206 HttpMethod::Connect,
207 HttpMethod::Options,
208 HttpMethod::Trace,
209 HttpMethod::Patch,
210 ];
211 for m in methods {
212 let s = m.as_str();
213 let parsed: HttpMethod = s.parse().expect("should parse");
214 assert_eq!(parsed, m, "round-trip failed for {s}");
215 }
216 }
217
218 #[test]
219 fn display_equals_as_str() {
220 assert_eq!(HttpMethod::Get.to_string(), HttpMethod::Get.as_str());
221 assert_eq!(HttpMethod::Patch.to_string(), "PATCH");
222 }
223
224 #[test]
225 fn is_safe() {
226 assert!(HttpMethod::Get.is_safe());
227 assert!(HttpMethod::Head.is_safe());
228 assert!(HttpMethod::Options.is_safe());
229 assert!(HttpMethod::Trace.is_safe());
230 assert!(!HttpMethod::Post.is_safe());
231 assert!(!HttpMethod::Put.is_safe());
232 assert!(!HttpMethod::Delete.is_safe());
233 assert!(!HttpMethod::Connect.is_safe());
234 assert!(!HttpMethod::Patch.is_safe());
235 }
236
237 #[test]
238 fn is_idempotent() {
239 assert!(HttpMethod::Get.is_idempotent());
240 assert!(HttpMethod::Head.is_idempotent());
241 assert!(HttpMethod::Put.is_idempotent());
242 assert!(HttpMethod::Delete.is_idempotent());
243 assert!(HttpMethod::Options.is_idempotent());
244 assert!(HttpMethod::Trace.is_idempotent());
245 assert!(!HttpMethod::Post.is_idempotent());
246 assert!(!HttpMethod::Connect.is_idempotent());
247 assert!(!HttpMethod::Patch.is_idempotent());
248 }
249
250 #[test]
251 fn parse_unknown_errors() {
252 assert!("BREW".parse::<HttpMethod>().is_err());
253 assert!("get".parse::<HttpMethod>().is_err());
254 }
255
256 #[cfg(feature = "serde")]
257 #[test]
258 fn serde_round_trip() {
259 let m = HttpMethod::Patch;
260 let json = serde_json::to_string(&m).unwrap();
261 assert_eq!(json, r#""PATCH""#);
262 let back: HttpMethod = serde_json::from_str(&json).unwrap();
263 assert_eq!(back, m);
264 }
265
266 #[cfg(feature = "http")]
267 #[test]
268 fn http_crate_round_trip() {
269 let pairs = [
270 (HttpMethod::Get, http::Method::GET),
271 (HttpMethod::Head, http::Method::HEAD),
272 (HttpMethod::Post, http::Method::POST),
273 (HttpMethod::Put, http::Method::PUT),
274 (HttpMethod::Delete, http::Method::DELETE),
275 (HttpMethod::Connect, http::Method::CONNECT),
276 (HttpMethod::Options, http::Method::OPTIONS),
277 (HttpMethod::Trace, http::Method::TRACE),
278 (HttpMethod::Patch, http::Method::PATCH),
279 ];
280 for (our, theirs) in pairs {
281 let converted: http::Method = our.into();
282 assert_eq!(converted, theirs);
283 let back: HttpMethod = converted.try_into().unwrap();
284 assert_eq!(back, our);
285 }
286 }
287
288 #[test]
289 fn parse_error_display() {
290 let err = "BREW".parse::<HttpMethod>().unwrap_err();
291 assert_eq!(err.to_string(), "unknown HTTP method");
292 }
293}