1use std::fmt;
7
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
34#[non_exhaustive]
35pub enum Method {
36 Get,
38 Post,
40 Put,
42 Delete,
44 Patch,
46 Head,
48 Options,
50 Custom(String),
52}
53
54impl std::str::FromStr for Method {
55 type Err = std::convert::Infallible;
56
57 fn from_str(s: &str) -> Result<Self, Self::Err> {
58 Ok(match s.to_ascii_uppercase().as_str() {
59 "GET" => Self::Get,
60 "POST" => Self::Post,
61 "PUT" => Self::Put,
62 "DELETE" => Self::Delete,
63 "PATCH" => Self::Patch,
64 "HEAD" => Self::Head,
65 "OPTIONS" => Self::Options,
66 other => Self::Custom(other.to_string()),
67 })
68 }
69}
70
71impl Method {
72 #[must_use]
74 pub fn as_str(&self) -> &str {
75 match self {
76 Self::Get => "GET",
77 Self::Post => "POST",
78 Self::Put => "PUT",
79 Self::Delete => "DELETE",
80 Self::Patch => "PATCH",
81 Self::Head => "HEAD",
82 Self::Options => "OPTIONS",
83 Self::Custom(s) => s.as_str(),
84 }
85 }
86
87 #[must_use]
89 pub fn has_body(&self) -> bool {
90 matches!(self, Self::Post | Self::Put | Self::Patch | Self::Custom(_))
91 }
92}
93
94impl fmt::Display for Method {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 f.write_str(self.as_str())
97 }
98}
99
100impl From<&str> for Method {
101 fn from(s: &str) -> Self {
102 s.parse().unwrap_or_else(|_| Method::Custom(s.to_string()))
103 }
104}
105
106impl From<String> for Method {
107 fn from(s: String) -> Self {
108 s.parse().unwrap_or(Method::Custom(s))
109 }
110}
111
112#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
143#[non_exhaustive]
144pub struct Request {
145 pub method: Method,
147 pub url: String,
149 pub headers: Vec<(String, String)>,
151 pub body: Option<Vec<u8>>,
153}
154
155impl Request {
156 pub fn get(url: impl Into<String>) -> Self {
158 Self {
159 method: Method::Get,
160 url: url.into(),
161 headers: Vec::new(),
162 body: None,
163 }
164 }
165
166 pub fn post(url: impl Into<String>, body: impl Into<Vec<u8>>) -> Self {
168 Self {
169 method: Method::Post,
170 url: url.into(),
171 headers: Vec::new(),
172 body: Some(body.into()),
173 }
174 }
175
176 pub fn put(url: impl Into<String>, body: impl Into<Vec<u8>>) -> Self {
178 Self {
179 method: Method::Put,
180 url: url.into(),
181 headers: Vec::new(),
182 body: Some(body.into()),
183 }
184 }
185
186 pub fn delete(url: impl Into<String>) -> Self {
188 Self {
189 method: Method::Delete,
190 url: url.into(),
191 headers: Vec::new(),
192 body: None,
193 }
194 }
195
196 pub fn with_method(method: impl Into<Method>, url: impl Into<String>) -> Self {
198 Self {
199 method: method.into(),
200 url: url.into(),
201 headers: Vec::new(),
202 body: None,
203 }
204 }
205
206 #[must_use]
210 pub fn method(&self) -> &Method {
211 &self.method
212 }
213
214 pub fn method_mut(&mut self) -> &mut Method {
216 &mut self.method
217 }
218
219 #[must_use]
221 pub fn url(&self) -> &str {
222 &self.url
223 }
224
225 pub fn url_mut(&mut self) -> &mut String {
227 &mut self.url
228 }
229
230 #[must_use]
232 pub fn headers(&self) -> &[(String, String)] {
233 &self.headers
234 }
235
236 pub fn headers_mut(&mut self) -> &mut Vec<(String, String)> {
238 &mut self.headers
239 }
240
241 #[must_use]
243 pub fn body_bytes(&self) -> Option<&[u8]> {
244 self.body.as_deref()
245 }
246
247 pub fn set_body(&mut self, body: impl Into<Vec<u8>>) {
249 self.body = Some(body.into());
250 }
251
252 pub fn clear_body(&mut self) {
254 self.body = None;
255 }
256
257 #[must_use]
261 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
262 self.headers.push((name.into(), value.into()));
263 self
264 }
265
266 pub fn add_header(&mut self, name: impl Into<String>, value: impl Into<String>) {
268 self.headers.push((name.into(), value.into()));
269 }
270
271 #[must_use]
273 pub fn with_body(mut self, body: impl Into<Vec<u8>>) -> Self {
274 self.body = Some(body.into());
275 self
276 }
277
278 #[must_use]
282 pub fn get_header(&self, name: &str) -> Option<&str> {
283 self.headers
284 .iter()
285 .find(|(k, _)| k.eq_ignore_ascii_case(name))
286 .map(|(_, v)| v.as_str())
287 }
288
289 #[must_use]
291 pub fn get_headers(&self, name: &str) -> Vec<&str> {
292 self.headers
293 .iter()
294 .filter(|(k, _)| k.eq_ignore_ascii_case(name))
295 .map(|(_, v)| v.as_str())
296 .collect()
297 }
298
299 #[must_use]
301 pub fn content_type(&self) -> Option<&str> {
302 self.get_header("content-type")
303 }
304
305 #[must_use]
307 pub fn has_body(&self) -> bool {
308 self.body.as_ref().is_some_and(|b| !b.is_empty())
309 }
310
311 #[must_use]
313 pub fn body_str(&self) -> Option<&str> {
314 self.body.as_ref().and_then(|b| std::str::from_utf8(b).ok())
315 }
316}
317
318impl fmt::Display for Request {
319 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
320 write!(f, "{} {}", self.method, self.url)?;
321 if let Some(ct) = self.content_type() {
322 write!(f, " [{ct}]")?;
323 }
324 if let Some(body) = &self.body {
325 write!(f, " ({} bytes)", body.len())?;
326 }
327 Ok(())
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334
335 #[test]
336 fn method_from_str() {
337 assert_eq!("GET".parse::<Method>().unwrap(), Method::Get);
338 assert_eq!("post".parse::<Method>().unwrap(), Method::Post);
339 assert_eq!(
340 "PURGE".parse::<Method>().unwrap(),
341 Method::Custom("PURGE".into())
342 );
343 }
344
345 #[test]
346 fn method_as_str_roundtrip() {
347 for method in &[
348 Method::Get,
349 Method::Post,
350 Method::Put,
351 Method::Delete,
352 Method::Patch,
353 Method::Head,
354 Method::Options,
355 ] {
356 assert_eq!(method.as_str().parse::<Method>().unwrap(), *method);
357 }
358 }
359
360 #[test]
361 fn method_has_body() {
362 assert!(!Method::Get.has_body());
363 assert!(Method::Post.has_body());
364 assert!(Method::Put.has_body());
365 assert!(!Method::Head.has_body());
366 }
367
368 #[test]
369 fn method_display() {
370 assert_eq!(Method::Get.to_string(), "GET");
371 assert_eq!(Method::Custom("PURGE".into()).to_string(), "PURGE");
372 }
373
374 #[test]
375 fn request_builder() {
376 let req = Request::get("https://example.com")
377 .header("X-Test", "value")
378 .header("Content-Type", "text/html");
379 assert_eq!(req.get_header("x-test"), Some("value"));
380 assert_eq!(req.content_type(), Some("text/html"));
381 }
382
383 #[test]
384 fn request_get_headers_multiple() {
385 let mut req = Request::get("https://example.com");
386 req.add_header("Cookie", "a=1");
387 req.add_header("Cookie", "b=2");
388 assert_eq!(req.get_headers("cookie").len(), 2);
389 }
390
391 #[test]
392 fn request_body_str() {
393 let req = Request::post("https://example.com", b"hello".to_vec());
394 assert_eq!(req.body_str(), Some("hello"));
395 assert!(req.has_body());
396 }
397
398 #[test]
399 fn request_display() {
400 let req = Request::post("https://example.com/api", b"data".to_vec())
401 .header("Content-Type", "application/json");
402 let display = req.to_string();
403 assert!(display.contains("POST"));
404 assert!(display.contains("example.com"));
405 assert!(display.contains("4 bytes"));
406 }
407
408 #[test]
409 fn request_equality() {
410 let a = Request::get("https://example.com");
411 let b = Request::get("https://example.com");
412 assert_eq!(a, b);
413 }
414
415 #[test]
416 fn request_with_method() {
417 let req = Request::with_method("PURGE", "https://example.com/cache");
418 assert_eq!(req.method, Method::Custom("PURGE".into()));
419 }
420
421 #[test]
422 fn request_put_and_delete() {
423 let put = Request::put("https://example.com/api", b"data".to_vec());
424 assert_eq!(put.method, Method::Put);
425 assert!(put.has_body());
426
427 let del = Request::delete("https://example.com/api/1");
428 assert_eq!(del.method, Method::Delete);
429 assert!(!del.has_body());
430 }
431
432 #[test]
433 fn request_serde_roundtrip() {
434 let req = Request::post("https://example.com", b"body".to_vec()).header("X-Test", "value");
435 let json = serde_json::to_string(&req).expect("serialize");
436 let deserialized: Request = serde_json::from_str(&json).expect("deserialize");
437 assert_eq!(req, deserialized);
438 }
439
440 #[test]
441 fn request_accessors() {
442 let req =
443 Request::post("https://example.com", b"test".to_vec()).header("Host", "example.com");
444 assert_eq!(req.url(), "https://example.com");
445 assert_eq!(*req.method(), Method::Post);
446 assert_eq!(req.headers().len(), 1);
447 assert_eq!(req.body_bytes(), Some(b"test".as_slice()));
448 }
449
450 #[test]
451 fn request_mutators() {
452 let mut req = Request::get("https://example.com");
453 req.set_body(b"new body");
454 assert!(req.has_body());
455 assert_eq!(req.body_str(), Some("new body"));
456 req.clear_body();
457 assert!(!req.has_body());
458 *req.url_mut() = "https://other.com".to_string();
459 assert_eq!(req.url(), "https://other.com");
460 }
461}