eggserve_core/primitives/
method.rs1use std::fmt;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum MethodError {
12 Empty,
14 InvalidToken,
16}
17
18impl fmt::Display for MethodError {
19 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20 match self {
21 Self::Empty => write!(f, "method must not be empty"),
22 Self::InvalidToken => write!(f, "method contains invalid characters"),
23 }
24 }
25}
26
27impl std::error::Error for MethodError {}
28
29#[derive(Debug, Clone, PartialEq, Eq, Hash)]
46pub struct Method(String);
47
48impl Method {
49 pub fn new(value: impl Into<String>) -> Result<Self, MethodError> {
56 let s = value.into();
57 if s.is_empty() {
58 return Err(MethodError::Empty);
59 }
60 if !is_http_token(&s) {
61 return Err(MethodError::InvalidToken);
62 }
63 Ok(Self(s))
64 }
65
66 #[inline]
73 fn new_unchecked(value: &'static str) -> Self {
74 Self(value.to_string())
75 }
76
77 pub fn get() -> Self {
79 Self::new_unchecked("GET")
80 }
81
82 pub fn head() -> Self {
84 Self::new_unchecked("HEAD")
85 }
86
87 pub fn post() -> Self {
89 Self::new_unchecked("POST")
90 }
91
92 pub fn put() -> Self {
94 Self::new_unchecked("PUT")
95 }
96
97 pub fn delete() -> Self {
99 Self::new_unchecked("DELETE")
100 }
101
102 pub fn patch() -> Self {
104 Self::new_unchecked("PATCH")
105 }
106
107 pub fn options() -> Self {
109 Self::new_unchecked("OPTIONS")
110 }
111
112 pub fn trace() -> Self {
114 Self::new_unchecked("TRACE")
115 }
116
117 pub fn connect() -> Self {
119 Self::new_unchecked("CONNECT")
120 }
121
122 pub fn as_str(&self) -> &str {
124 &self.0
125 }
126
127 pub fn is_get(&self) -> bool {
129 self.0 == "GET"
130 }
131
132 pub fn is_head(&self) -> bool {
134 self.0 == "HEAD"
135 }
136
137 pub fn is_safe(&self) -> bool {
142 matches!(self.0.as_str(), "GET" | "HEAD" | "OPTIONS" | "TRACE")
143 }
144
145 pub fn is_idempotent(&self) -> bool {
150 matches!(
151 self.0.as_str(),
152 "GET" | "HEAD" | "PUT" | "DELETE" | "OPTIONS" | "TRACE"
153 )
154 }
155
156 pub fn permits_static_resolution(&self) -> bool {
162 self.0 == "GET" || self.0 == "HEAD"
163 }
164}
165
166impl fmt::Display for Method {
167 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168 f.write_str(&self.0)
169 }
170}
171
172impl AsRef<str> for Method {
173 fn as_ref(&self) -> &str {
174 &self.0
175 }
176}
177
178impl PartialEq<str> for Method {
179 fn eq(&self, other: &str) -> bool {
180 self.0 == other
181 }
182}
183
184impl PartialEq<&str> for Method {
185 fn eq(&self, other: &&str) -> bool {
186 self.0 == *other
187 }
188}
189
190fn is_http_token(s: &str) -> bool {
192 !s.is_empty()
193 && s.bytes().all(|b| matches!(b, 0x21 | 0x23..=0x27 | 0x2A | 0x2B | 0x2D..=0x2E | 0x30..=0x39 | 0x41..=0x5A | 0x5E..=0x7A | 0x7C | 0x7E))
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn standard_methods() {
202 assert_eq!(Method::get().as_str(), "GET");
203 assert_eq!(Method::head().as_str(), "HEAD");
204 assert_eq!(Method::post().as_str(), "POST");
205 assert_eq!(Method::put().as_str(), "PUT");
206 assert_eq!(Method::delete().as_str(), "DELETE");
207 assert_eq!(Method::patch().as_str(), "PATCH");
208 assert_eq!(Method::options().as_str(), "OPTIONS");
209 assert_eq!(Method::trace().as_str(), "TRACE");
210 assert_eq!(Method::connect().as_str(), "CONNECT");
211 }
212
213 #[test]
214 fn extension_method() {
215 let m = Method::new("PURGE").unwrap();
216 assert_eq!(m.as_str(), "PURGE");
217 }
218
219 #[test]
220 fn case_preserved() {
221 let m = Method::new("get").unwrap();
222 assert_eq!(m.as_str(), "get");
223 assert!(!m.is_get()); }
225
226 #[test]
227 fn empty_rejected() {
228 assert_eq!(Method::new("").unwrap_err(), MethodError::Empty);
229 }
230
231 #[test]
232 fn invalid_token_rejected() {
233 assert_eq!(
234 Method::new("GET POST").unwrap_err(),
235 MethodError::InvalidToken
236 );
237 assert_eq!(Method::new("GET\t").unwrap_err(), MethodError::InvalidToken);
238 assert_eq!(Method::new("").unwrap_err(), MethodError::Empty);
239 }
240
241 #[test]
242 fn valid_tokens() {
243 assert!(Method::new("X").is_ok());
244 assert!(Method::new("x-y-z").is_ok());
245 assert!(Method::new("!").is_ok());
246 assert!(Method::new("#").is_ok());
247 assert!(Method::new("0").is_ok());
248 }
249
250 #[test]
251 fn is_safe_classification() {
252 assert!(Method::get().is_safe());
253 assert!(Method::head().is_safe());
254 assert!(Method::options().is_safe());
255 assert!(Method::trace().is_safe());
256 assert!(!Method::post().is_safe());
257 assert!(!Method::put().is_safe());
258 assert!(!Method::delete().is_safe());
259 assert!(!Method::patch().is_safe());
260 }
261
262 #[test]
263 fn is_idempotent_classification() {
264 assert!(Method::get().is_idempotent());
265 assert!(Method::head().is_idempotent());
266 assert!(Method::put().is_idempotent());
267 assert!(Method::delete().is_idempotent());
268 assert!(Method::options().is_idempotent());
269 assert!(Method::trace().is_idempotent());
270 assert!(!Method::post().is_idempotent());
271 assert!(!Method::patch().is_idempotent());
272 }
273
274 #[test]
275 fn permits_static_resolution() {
276 assert!(Method::get().permits_static_resolution());
277 assert!(Method::head().permits_static_resolution());
278 assert!(!Method::post().permits_static_resolution());
279 assert!(!Method::put().permits_static_resolution());
280 }
281
282 #[test]
283 fn display() {
284 assert_eq!(format!("{}", Method::get()), "GET");
285 assert_eq!(format!("{}", Method::new("PURGE").unwrap()), "PURGE");
286 }
287
288 #[test]
289 fn eq_str() {
290 assert_eq!(Method::get(), "GET");
291 assert_eq!(Method::get(), "GET");
292 assert_ne!(Method::get(), "POST");
293 }
294
295 #[test]
296 fn method_error_display() {
297 assert!(!MethodError::Empty.to_string().is_empty());
298 assert!(!MethodError::InvalidToken.to_string().is_empty());
299 }
300
301 #[test]
302 fn method_error_is_error() {
303 let err: &dyn std::error::Error = &MethodError::Empty;
304 assert!(!err.to_string().is_empty());
305 }
306}