diff --git a/Cargo.toml b/Cargo.toml index 72f866f0..6fdd3e19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "http-types" -version = "2.10.0" +version = "2.12.0" license = "MIT OR Apache-2.0" repository = "https://github.com/http-rs/http-types" documentation = "https://docs.rs/http-types" @@ -45,7 +45,7 @@ serde_json = "1.0.51" serde = { version = "1.0.106", features = ["derive"] } serde_urlencoded = "0.7.0" rand = "0.7.3" -serde_qs = "0.7.0" +serde_qs = "0.8.3" base64 = "0.13.0" [dev-dependencies] diff --git a/src/auth/authorization.rs b/src/auth/authorization.rs index cc8fa5b1..c0950c54 100644 --- a/src/auth/authorization.rs +++ b/src/auth/authorization.rs @@ -132,11 +132,10 @@ mod test { } #[test] - fn bad_request_on_parse_error() -> crate::Result<()> { + fn bad_request_on_parse_error() { let mut headers = Headers::new(); headers.insert(AUTHORIZATION, ""); let err = Authorization::from_headers(headers).unwrap_err(); assert_eq!(err.status(), 400); - Ok(()) } } diff --git a/src/auth/basic_auth.rs b/src/auth/basic_auth.rs index afcd6046..34013b01 100644 --- a/src/auth/basic_auth.rs +++ b/src/auth/basic_auth.rs @@ -135,11 +135,10 @@ mod test { } #[test] - fn bad_request_on_parse_error() -> crate::Result<()> { + fn bad_request_on_parse_error() { let mut headers = Headers::new(); headers.insert(AUTHORIZATION, ""); let err = BasicAuth::from_headers(headers).unwrap_err(); assert_eq!(err.status(), 400); - Ok(()) } } diff --git a/src/auth/www_authenticate.rs b/src/auth/www_authenticate.rs index f7f43c8f..01192452 100644 --- a/src/auth/www_authenticate.rs +++ b/src/auth/www_authenticate.rs @@ -159,11 +159,10 @@ mod test { } #[test] - fn bad_request_on_parse_error() -> crate::Result<()> { + fn bad_request_on_parse_error() { let mut headers = Headers::new(); headers.insert(WWW_AUTHENTICATE, ""); let err = WwwAuthenticate::from_headers(headers).unwrap_err(); assert_eq!(err.status(), 400); - Ok(()) } } diff --git a/src/body.rs b/src/body.rs index 5817565c..d9a012de 100644 --- a/src/body.rs +++ b/src/body.rs @@ -277,7 +277,7 @@ impl Body { pub async fn into_json(mut self) -> crate::Result { let mut buf = Vec::with_capacity(1024); self.read_to_end(&mut buf).await?; - Ok(serde_json::from_slice(&buf).status(StatusCode::UnprocessableEntity)?) + serde_json::from_slice(&buf).status(StatusCode::UnprocessableEntity) } /// Creates a `Body` from a type, serializing it using form encoding. @@ -346,7 +346,7 @@ impl Body { /// ``` pub async fn into_form(self) -> crate::Result { let s = self.into_string().await?; - Ok(serde_urlencoded::from_str(&s).status(StatusCode::UnprocessableEntity)?) + serde_urlencoded::from_str(&s).status(StatusCode::UnprocessableEntity) } /// Create a `Body` from a file. @@ -419,6 +419,45 @@ impl Body { pub fn set_mime(&mut self, mime: impl Into) { self.mime = mime.into(); } + + /// Create a Body by chaining another Body after this one, consuming both. + /// + /// If both Body instances have a length, and their sum does not overflow, + /// the resulting Body will have a length. + /// + /// If both Body instances have the same fallback MIME type, the resulting + /// Body will have the same fallback MIME type; otherwise, the resulting + /// Body will have the fallback MIME type `application/octet-stream`. + /// + /// # Examples + /// + /// ``` + /// # fn main() -> http_types::Result<()> { async_std::task::block_on(async { + /// use http_types::Body; + /// use async_std::io::Cursor; + /// + /// let cursor = Cursor::new("Hello "); + /// let body = Body::from_reader(cursor, None).chain(Body::from("Nori")); + /// assert_eq!(&body.into_string().await.unwrap(), "Hello Nori"); + /// # Ok(()) }) } + /// ``` + pub fn chain(self, other: Body) -> Self { + let mime = if self.mime == other.mime { + self.mime.clone() + } else { + mime::BYTE_STREAM + }; + let length = match (self.length, other.length) { + (Some(l1), Some(l2)) => (l1 - self.bytes_read).checked_add(l2 - other.bytes_read), + _ => None, + }; + Self { + mime, + length, + reader: Box::new(futures_lite::io::AsyncReadExt::chain(self, other)), + bytes_read: 0, + } + } } impl Debug for Body { @@ -468,7 +507,7 @@ impl AsyncRead for Body { cx: &mut Context<'_>, buf: &mut [u8], ) -> Poll> { - let mut buf = match self.length { + let buf = match self.length { None => buf, Some(length) if length == self.bytes_read => return Poll::Ready(Ok(0)), Some(length) => { @@ -477,7 +516,7 @@ impl AsyncRead for Body { } }; - let bytes = ready!(Pin::new(&mut self.reader).poll_read(cx, &mut buf))?; + let bytes = ready!(Pin::new(&mut self.reader).poll_read(cx, buf))?; self.bytes_read += bytes; Poll::Ready(Ok(bytes)) } @@ -512,7 +551,7 @@ async fn peek_mime(file: &mut async_std::fs::File) -> io::Result> { /// This is useful for plain-text formats such as HTML and CSS. #[cfg(all(feature = "fs", not(target_os = "unknown")))] fn guess_ext(path: &std::path::Path) -> Option { - let ext = path.extension().map(|p| p.to_str()).flatten(); + let ext = path.extension().and_then(|p| p.to_str()); ext.and_then(Mime::from_extension) } @@ -526,7 +565,7 @@ mod test { async fn json_status() { #[derive(Debug, Deserialize)] struct Foo { - inner: String, + _inner: String, } let body = Body::empty(); let res = body.into_json::().await; @@ -537,7 +576,7 @@ mod test { async fn form_status() { #[derive(Debug, Deserialize)] struct Foo { - inner: String, + _inner: String, } let body = Body::empty(); let res = body.into_form::().await; @@ -613,4 +652,148 @@ mod test { Ok(()) } + + #[async_std::test] + async fn chain_strings() -> crate::Result<()> { + for buf_len in 1..13 { + let mut body = Body::from("hello ").chain(Body::from("world")); + assert_eq!(body.len(), Some(11)); + assert_eq!(body.mime(), &mime::PLAIN); + assert_eq!( + read_with_buffers_of_size(&mut body, buf_len).await?, + "hello world" + ); + assert_eq!(body.bytes_read, 11); + } + + Ok(()) + } + + #[async_std::test] + async fn chain_mixed_bytes_string() -> crate::Result<()> { + for buf_len in 1..13 { + let mut body = Body::from(&b"hello "[..]).chain(Body::from("world")); + assert_eq!(body.len(), Some(11)); + assert_eq!(body.mime(), &mime::BYTE_STREAM); + assert_eq!( + read_with_buffers_of_size(&mut body, buf_len).await?, + "hello world" + ); + assert_eq!(body.bytes_read, 11); + } + + Ok(()) + } + + #[async_std::test] + async fn chain_mixed_reader_string() -> crate::Result<()> { + for buf_len in 1..13 { + let mut body = + Body::from_reader(Cursor::new("hello "), Some(6)).chain(Body::from("world")); + assert_eq!(body.len(), Some(11)); + assert_eq!(body.mime(), &mime::BYTE_STREAM); + assert_eq!( + read_with_buffers_of_size(&mut body, buf_len).await?, + "hello world" + ); + assert_eq!(body.bytes_read, 11); + } + + Ok(()) + } + + #[async_std::test] + async fn chain_mixed_nolen_len() -> crate::Result<()> { + for buf_len in 1..13 { + let mut body = + Body::from_reader(Cursor::new("hello "), None).chain(Body::from("world")); + assert_eq!(body.len(), None); + assert_eq!(body.mime(), &mime::BYTE_STREAM); + assert_eq!( + read_with_buffers_of_size(&mut body, buf_len).await?, + "hello world" + ); + assert_eq!(body.bytes_read, 11); + } + + Ok(()) + } + + #[async_std::test] + async fn chain_mixed_len_nolen() -> crate::Result<()> { + for buf_len in 1..13 { + let mut body = + Body::from("hello ").chain(Body::from_reader(Cursor::new("world"), None)); + assert_eq!(body.len(), None); + assert_eq!(body.mime(), &mime::BYTE_STREAM); + assert_eq!( + read_with_buffers_of_size(&mut body, buf_len).await?, + "hello world" + ); + assert_eq!(body.bytes_read, 11); + } + + Ok(()) + } + + #[async_std::test] + async fn chain_short() -> crate::Result<()> { + for buf_len in 1..26 { + let mut body = Body::from_reader(Cursor::new("hello xyz"), Some(6)) + .chain(Body::from_reader(Cursor::new("world abc"), Some(5))); + assert_eq!(body.len(), Some(11)); + assert_eq!(body.mime(), &mime::BYTE_STREAM); + assert_eq!( + read_with_buffers_of_size(&mut body, buf_len).await?, + "hello world" + ); + assert_eq!(body.bytes_read, 11); + } + + Ok(()) + } + + #[async_std::test] + async fn chain_many() -> crate::Result<()> { + for buf_len in 1..13 { + let mut body = Body::from("hello") + .chain(Body::from(&b" "[..])) + .chain(Body::from("world")); + assert_eq!(body.len(), Some(11)); + assert_eq!(body.mime(), &mime::BYTE_STREAM); + assert_eq!( + read_with_buffers_of_size(&mut body, buf_len).await?, + "hello world" + ); + assert_eq!(body.bytes_read, 11); + } + + Ok(()) + } + + #[async_std::test] + async fn chain_skip_start() -> crate::Result<()> { + for buf_len in 1..26 { + let mut body1 = Body::from_reader(Cursor::new("1234 hello xyz"), Some(11)); + let mut buf = vec![0; 5]; + body1.read(&mut buf).await?; + assert_eq!(buf, b"1234 "); + + let mut body2 = Body::from_reader(Cursor::new("321 world abc"), Some(9)); + let mut buf = vec![0; 4]; + body2.read(&mut buf).await?; + assert_eq!(buf, b"321 "); + + let mut body = body1.chain(body2); + assert_eq!(body.len(), Some(11)); + assert_eq!(body.mime(), &mime::BYTE_STREAM); + assert_eq!( + read_with_buffers_of_size(&mut body, buf_len).await?, + "hello world" + ); + assert_eq!(body.bytes_read, 11); + } + + Ok(()) + } } diff --git a/src/cache/age.rs b/src/cache/age.rs index 4d23c1a3..3009d6fe 100644 --- a/src/cache/age.rs +++ b/src/cache/age.rs @@ -113,11 +113,10 @@ mod test { } #[test] - fn bad_request_on_parse_error() -> crate::Result<()> { + fn bad_request_on_parse_error() { let mut headers = Headers::new(); headers.insert(AGE, ""); let err = Age::from_headers(headers).unwrap_err(); assert_eq!(err.status(), 400); - Ok(()) } } diff --git a/src/cache/cache_control/cache_directive.rs b/src/cache/cache_control/cache_directive.rs index 859d9c0c..a78be117 100644 --- a/src/cache/cache_control/cache_directive.rs +++ b/src/cache/cache_control/cache_directive.rs @@ -47,26 +47,29 @@ impl CacheDirective { /// Check whether this directive is valid in an HTTP request. pub fn valid_in_req(&self) -> bool { use CacheDirective::*; - matches!(self, - MaxAge(_) | MaxStale(_) | MinFresh(_) | NoCache | NoStore | NoTransform - | OnlyIfCached) + matches!( + self, + MaxAge(_) | MaxStale(_) | MinFresh(_) | NoCache | NoStore | NoTransform | OnlyIfCached + ) } /// Check whether this directive is valid in an HTTP response. pub fn valid_in_res(&self) -> bool { use CacheDirective::*; - matches!(self, + matches!( + self, MustRevalidate - | NoCache - | NoStore - | NoTransform - | Public - | Private - | ProxyRevalidate - | MaxAge(_) - | SMaxAge(_) - | StaleIfError(_) - | StaleWhileRevalidate(_)) + | NoCache + | NoStore + | NoTransform + | Public + | Private + | ProxyRevalidate + | MaxAge(_) + | SMaxAge(_) + | StaleIfError(_) + | StaleWhileRevalidate(_) + ) } /// Create an instance from a string slice. @@ -85,7 +88,7 @@ impl CacheDirective { return Ok(None); } - s.to_lowercase(); + let s = s.to_lowercase(); let mut parts = s.split('='); let next = parts.next().unwrap(); diff --git a/src/cache/cache_control/mod.rs b/src/cache/cache_control/mod.rs index b15b809a..d7e962bd 100644 --- a/src/cache/cache_control/mod.rs +++ b/src/cache/cache_control/mod.rs @@ -45,11 +45,10 @@ mod test { } #[test] - fn bad_request_on_parse_error() -> crate::Result<()> { + fn bad_request_on_parse_error() { let mut headers = Headers::new(); headers.insert(CACHE_CONTROL, "min-fresh=0.9"); // floats are not supported let err = CacheControl::from_headers(headers).unwrap_err(); assert_eq!(err.status(), 400); - Ok(()) } } diff --git a/src/cache/clear_site_data/mod.rs b/src/cache/clear_site_data/mod.rs index c816062d..4f58247d 100644 --- a/src/cache/clear_site_data/mod.rs +++ b/src/cache/clear_site_data/mod.rs @@ -96,8 +96,8 @@ impl ClearSiteData { let mut output = String::new(); for (n, etag) in self.entries.iter().enumerate() { match n { - 0 => write!(output, "{}", etag.to_string()).unwrap(), - _ => write!(output, ", {}", etag.to_string()).unwrap(), + 0 => write!(output, "{}", etag).unwrap(), + _ => write!(output, ", {}", etag).unwrap(), }; } @@ -280,7 +280,7 @@ mod test { entries.apply(&mut res); let entries = ClearSiteData::from_headers(res)?.unwrap(); - assert_eq!(entries.wildcard(), true); + assert!(entries.wildcard()); let mut entries = entries.iter(); assert_eq!(entries.next().unwrap(), &ClearDirective::Cache); Ok(()) @@ -292,7 +292,7 @@ mod test { res.insert_header("clear-site-data", r#""cookies""#); let entries = ClearSiteData::from_headers(res)?.unwrap(); - assert_eq!(entries.wildcard(), false); + assert!(!entries.wildcard()); let mut entries = entries.iter(); assert_eq!(entries.next().unwrap(), &ClearDirective::Cookies); @@ -300,7 +300,7 @@ mod test { res.insert_header("clear-site-data", r#""*""#); let entries = ClearSiteData::from_headers(res)?.unwrap(); - assert_eq!(entries.wildcard(), true); + assert!(entries.wildcard()); let mut entries = entries.iter(); assert_eq!(entries.next(), None); Ok(()) diff --git a/src/cache/expires.rs b/src/cache/expires.rs index 73c3e66d..2fda328d 100644 --- a/src/cache/expires.rs +++ b/src/cache/expires.rs @@ -120,11 +120,10 @@ mod test { } #[test] - fn bad_request_on_parse_error() -> crate::Result<()> { + fn bad_request_on_parse_error() { let mut headers = Headers::new(); headers.insert(EXPIRES, ""); let err = Expires::from_headers(headers).unwrap_err(); assert_eq!(err.status(), 400); - Ok(()) } } diff --git a/src/conditional/etag.rs b/src/conditional/etag.rs index 548e4bdb..e3255974 100644 --- a/src/conditional/etag.rs +++ b/src/conditional/etag.rs @@ -177,21 +177,19 @@ mod test { } #[test] - fn bad_request_on_parse_error() -> crate::Result<()> { + fn bad_request_on_parse_error() { let mut headers = Headers::new(); headers.insert(ETAG, ""); let err = ETag::from_headers(headers).unwrap_err(); assert_eq!(err.status(), 400); - Ok(()) } #[test] - fn validate_quotes() -> crate::Result<()> { + fn validate_quotes() { assert_entry_err(r#""hello"#, "Invalid ETag header"); assert_entry_err(r#"hello""#, "Invalid ETag header"); assert_entry_err(r#"/O"valid content""#, "Invalid ETag header"); assert_entry_err(r#"/Wvalid content""#, "Invalid ETag header"); - Ok(()) } fn assert_entry_err(s: &str, msg: &str) { @@ -202,9 +200,8 @@ mod test { } #[test] - fn validate_characters() -> crate::Result<()> { + fn validate_characters() { assert_entry_err(r#"""hello""#, "Invalid ETag header"); assert_entry_err("\"hello\x7F\"", "Invalid ETag header"); - Ok(()) } } diff --git a/src/conditional/if_match.rs b/src/conditional/if_match.rs index 24f961c5..ee62a132 100644 --- a/src/conditional/if_match.rs +++ b/src/conditional/if_match.rs @@ -88,8 +88,8 @@ impl IfMatch { let mut output = String::new(); for (n, etag) in self.entries.iter().enumerate() { match n { - 0 => write!(output, "{}", etag.to_string()).unwrap(), - _ => write!(output, ", {}", etag.to_string()).unwrap(), + 0 => write!(output, "{}", etag).unwrap(), + _ => write!(output, ", {}", etag).unwrap(), }; } @@ -278,7 +278,7 @@ mod test { entries.apply(&mut res); let entries = IfMatch::from_headers(res)?.unwrap(); - assert_eq!(entries.wildcard(), true); + assert!(entries.wildcard()); let mut entries = entries.iter(); assert_eq!( entries.next().unwrap(), diff --git a/src/conditional/if_modified_since.rs b/src/conditional/if_modified_since.rs index d964c427..595af3d6 100644 --- a/src/conditional/if_modified_since.rs +++ b/src/conditional/if_modified_since.rs @@ -116,11 +116,10 @@ mod test { } #[test] - fn bad_request_on_parse_error() -> crate::Result<()> { + fn bad_request_on_parse_error() { let mut headers = Headers::new(); headers.insert(IF_MODIFIED_SINCE, ""); let err = IfModifiedSince::from_headers(headers).unwrap_err(); assert_eq!(err.status(), 400); - Ok(()) } } diff --git a/src/conditional/if_none_match.rs b/src/conditional/if_none_match.rs index 608b45bd..b38fc43c 100644 --- a/src/conditional/if_none_match.rs +++ b/src/conditional/if_none_match.rs @@ -94,8 +94,8 @@ impl IfNoneMatch { let mut output = String::new(); for (n, etag) in self.entries.iter().enumerate() { match n { - 0 => write!(output, "{}", etag.to_string()).unwrap(), - _ => write!(output, ", {}", etag.to_string()).unwrap(), + 0 => write!(output, "{}", etag).unwrap(), + _ => write!(output, ", {}", etag).unwrap(), }; } @@ -284,7 +284,7 @@ mod test { entries.apply(&mut res); let entries = IfNoneMatch::from_headers(res)?.unwrap(); - assert_eq!(entries.wildcard(), true); + assert!(entries.wildcard()); let mut entries = entries.iter(); assert_eq!( entries.next().unwrap(), diff --git a/src/conditional/if_unmodified_since.rs b/src/conditional/if_unmodified_since.rs index 2696cb0b..f5317a07 100644 --- a/src/conditional/if_unmodified_since.rs +++ b/src/conditional/if_unmodified_since.rs @@ -116,11 +116,10 @@ mod test { } #[test] - fn bad_request_on_parse_error() -> crate::Result<()> { + fn bad_request_on_parse_error() { let mut headers = Headers::new(); headers.insert(IF_UNMODIFIED_SINCE, ""); let err = IfUnmodifiedSince::from_headers(headers).unwrap_err(); assert_eq!(err.status(), 400); - Ok(()) } } diff --git a/src/conditional/last_modified.rs b/src/conditional/last_modified.rs index a4fb3153..4d73a4d1 100644 --- a/src/conditional/last_modified.rs +++ b/src/conditional/last_modified.rs @@ -115,11 +115,10 @@ mod test { } #[test] - fn bad_request_on_parse_error() -> crate::Result<()> { + fn bad_request_on_parse_error() { let mut headers = Headers::new(); headers.insert(LAST_MODIFIED, ""); let err = LastModified::from_headers(headers).unwrap_err(); assert_eq!(err.status(), 400); - Ok(()) } } diff --git a/src/conditional/vary.rs b/src/conditional/vary.rs index 38d4b68b..60b5a037 100644 --- a/src/conditional/vary.rs +++ b/src/conditional/vary.rs @@ -278,7 +278,7 @@ mod test { entries.apply(&mut res); let entries = Vary::from_headers(res)?.unwrap(); - assert_eq!(entries.wildcard(), true); + assert!(entries.wildcard()); let mut entries = entries.iter(); assert_eq!(entries.next().unwrap(), "User-Agent"); Ok(()) diff --git a/src/content/accept.rs b/src/content/accept.rs index 828cd533..f898d66e 100644 --- a/src/content/accept.rs +++ b/src/content/accept.rs @@ -125,7 +125,7 @@ impl Accept { // Try and find the first encoding that matches. for accept in &self.entries { - if available.contains(&accept) { + if available.contains(accept) { return Ok(accept.media_type.clone().into()); } } @@ -137,7 +137,7 @@ impl Accept { } } - let mut err = Error::new_adhoc("No suitable ContentEncoding found"); + let mut err = Error::new_adhoc("No suitable Content-Type found"); err.set_status(StatusCode::NotAcceptable); Err(err) } diff --git a/src/content/accept_encoding.rs b/src/content/accept_encoding.rs index 8a2814e6..af9aa1d3 100644 --- a/src/content/accept_encoding.rs +++ b/src/content/accept_encoding.rs @@ -107,7 +107,7 @@ impl AcceptEncoding { sort_by_weight(&mut self.entries); } - /// Determine the most suitable `Content-Type` encoding. + /// Determine the most suitable `Content-Encoding` encoding. /// /// # Errors /// @@ -118,7 +118,7 @@ impl AcceptEncoding { // Try and find the first encoding that matches. for encoding in &self.entries { - if available.contains(&encoding) { + if available.contains(encoding) { return Ok(encoding.into()); } } @@ -130,7 +130,7 @@ impl AcceptEncoding { } } - let mut err = Error::new_adhoc("No suitable ContentEncoding found"); + let mut err = Error::new_adhoc("No suitable Content-Encoding found"); err.set_status(StatusCode::NotAcceptable); Err(err) } @@ -149,7 +149,7 @@ impl AcceptEncoding { pub fn value(&self) -> HeaderValue { let mut output = String::new(); for (n, directive) in self.entries.iter().enumerate() { - let directive: HeaderValue = directive.clone().into(); + let directive: HeaderValue = (*directive).into(); match n { 0 => write!(output, "{}", directive).unwrap(), _ => write!(output, ", {}", directive).unwrap(), diff --git a/src/content/content_length.rs b/src/content/content_length.rs index b4495c9f..598abea2 100644 --- a/src/content/content_length.rs +++ b/src/content/content_length.rs @@ -98,11 +98,10 @@ mod test { } #[test] - fn bad_request_on_parse_error() -> crate::Result<()> { + fn bad_request_on_parse_error() { let mut headers = Headers::new(); headers.insert(CONTENT_LENGTH, ""); let err = ContentLength::from_headers(headers).unwrap_err(); assert_eq!(err.status(), 400); - Ok(()) } } diff --git a/src/content/content_location.rs b/src/content/content_location.rs index a381923e..5357dfb2 100644 --- a/src/content/content_location.rs +++ b/src/content/content_location.rs @@ -122,13 +122,12 @@ mod test { } #[test] - fn bad_request_on_parse_error() -> crate::Result<()> { + fn bad_request_on_parse_error() { let mut headers = Headers::new(); headers.insert(CONTENT_LOCATION, "htt://"); let err = ContentLocation::from_headers(Url::parse("https://example.net").unwrap(), headers) .unwrap_err(); assert_eq!(err.status(), 400); - Ok(()) } } diff --git a/src/content/content_type.rs b/src/content/content_type.rs index 3c2a1227..f8d91eb3 100644 --- a/src/content/content_type.rs +++ b/src/content/content_type.rs @@ -130,11 +130,10 @@ mod test { } #[test] - fn bad_request_on_parse_error() -> crate::Result<()> { + fn bad_request_on_parse_error() { let mut headers = Headers::new(); headers.insert(CONTENT_TYPE, ""); let err = ContentType::from_headers(headers).unwrap_err(); assert_eq!(err.status(), 400); - Ok(()) } } diff --git a/src/content/encoding_proposal.rs b/src/content/encoding_proposal.rs index bd126900..7d329447 100644 --- a/src/content/encoding_proposal.rs +++ b/src/content/encoding_proposal.rs @@ -123,15 +123,14 @@ mod test { use super::*; #[test] - fn smoke() -> crate::Result<()> { + fn smoke() { let _ = EncodingProposal::new(Encoding::Gzip, Some(0.0)).unwrap(); let _ = EncodingProposal::new(Encoding::Gzip, Some(0.5)).unwrap(); let _ = EncodingProposal::new(Encoding::Gzip, Some(1.0)).unwrap(); - Ok(()) } #[test] - fn error_code_500() -> crate::Result<()> { + fn error_code_500() { let err = EncodingProposal::new(Encoding::Gzip, Some(1.1)).unwrap_err(); assert_eq!(err.status(), 500); @@ -140,6 +139,5 @@ mod test { let err = EncodingProposal::new(Encoding::Gzip, Some(-0.0)).unwrap_err(); assert_eq!(err.status(), 500); - Ok(()) } } diff --git a/src/content/media_type_proposal.rs b/src/content/media_type_proposal.rs index 33805f21..bdb047c5 100644 --- a/src/content/media_type_proposal.rs +++ b/src/content/media_type_proposal.rs @@ -57,7 +57,7 @@ impl MediaTypeProposal { .remove_param("q") .map(|param| param.as_str().parse()) .transpose()?; - Ok(Self::new(media_type, weight)?) + Self::new(media_type, weight) } } @@ -135,15 +135,14 @@ mod test { use crate::mime; #[test] - fn smoke() -> crate::Result<()> { + fn smoke() { let _ = MediaTypeProposal::new(mime::JSON, Some(0.0)).unwrap(); let _ = MediaTypeProposal::new(mime::XML, Some(0.5)).unwrap(); let _ = MediaTypeProposal::new(mime::HTML, Some(1.0)).unwrap(); - Ok(()) } #[test] - fn error_code_500() -> crate::Result<()> { + fn error_code_500() { let err = MediaTypeProposal::new(mime::JSON, Some(1.1)).unwrap_err(); assert_eq!(err.status(), 500); @@ -152,6 +151,5 @@ mod test { let err = MediaTypeProposal::new(mime::HTML, Some(-0.0)).unwrap_err(); assert_eq!(err.status(), 500); - Ok(()) } } diff --git a/src/error.rs b/src/error.rs index 40bffff1..04fbcd8d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -159,7 +159,29 @@ impl Error { /// Retrieves a reference to the type name of the error, if available. pub fn type_name(&self) -> Option<&str> { - self.type_name.as_deref() + self.type_name + } + + /// Converts anything which implements `Display` into an `http_types::Error`. + /// + /// This is handy for errors which are not `Send + Sync + 'static` because `std::error::Error` requires `Display`. + /// Note that any assiciated context not included in the `Display` output will be lost, + /// and so this may be lossy for some types which implement `std::error::Error`. + /// + /// **Note: Prefer `error.into()` via `From>` when possible!** + pub fn from_display(error: D) -> Self { + anyhow::Error::msg(error.to_string()).into() + } + + /// Converts anything which implements `Debug` into an `http_types::Error`. + /// + /// This is handy for errors which are not `Send + Sync + 'static` because `std::error::Error` requires `Debug`. + /// Note that any assiciated context not included in the `Debug` output will be lost, + /// and so this may be lossy for some types which implement `std::error::Error`. + /// + /// **Note: Prefer `error.into()` via `From>` when possible!** + pub fn from_debug(error: D) -> Self { + anyhow::Error::msg(format!("{:?}", error)).into() } } @@ -180,6 +202,7 @@ impl> From for Error { Self::new(StatusCode::InternalServerError, error) } } + impl AsRef for Error { fn as_ref(&self) -> &(dyn StdError + Send + Sync + 'static) { self.error.as_ref() @@ -215,3 +238,9 @@ impl From for Box { Box::::from(error.error) } } + +impl AsRef for Error { + fn as_ref(&self) -> &anyhow::Error { + &self.error + } +} diff --git a/src/headers/header_name.rs b/src/headers/header_name.rs index d4eb21be..77edb7ae 100644 --- a/src/headers/header_name.rs +++ b/src/headers/header_name.rs @@ -114,7 +114,7 @@ impl<'a> PartialEq<&'a str> for HeaderName { impl PartialEq for HeaderName { fn eq(&self, other: &String) -> bool { - match HeaderName::from_str(&other) { + match HeaderName::from_str(other) { Err(_) => false, Ok(other) => self == &other, } diff --git a/src/headers/header_values.rs b/src/headers/header_values.rs index 98521211..27fc0045 100644 --- a/src/headers/header_values.rs +++ b/src/headers/header_values.rs @@ -44,7 +44,7 @@ impl HeaderValues { /// An iterator visiting all header values in arbitrary order. pub fn iter(&self) -> Values<'_> { - Values::new_values(&self) + Values::new_values(self) } // /// An iterator visiting all header values in arbitrary order, with mutable @@ -146,6 +146,7 @@ impl AsMut for HeaderValues { &mut self.inner[0] } } + impl Deref for HeaderValues { type Target = HeaderValue; @@ -170,6 +171,22 @@ impl<'a> IntoIterator for &'a HeaderValues { } } +impl From> for HeaderValues { + fn from(headers: Vec) -> Self { + Self { inner: headers } + } +} + +impl IntoIterator for HeaderValues { + type Item = HeaderValue; + type IntoIter = std::vec::IntoIter; + + #[inline] + fn into_iter(self) -> Self::IntoIter { + self.inner.into_iter() + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/hyperium_http.rs b/src/hyperium_http.rs index 3f8a3779..6fa8c672 100644 --- a/src/hyperium_http.rs +++ b/src/hyperium_http.rs @@ -1,8 +1,8 @@ // This is the compat file for the "hyperium/http" crate. -use crate::headers::{HeaderName, HeaderValue}; -use crate::{Body, Headers, Method, Request, Response, StatusCode, Url, Version}; -use std::convert::TryFrom; +use crate::headers::{HeaderName, HeaderValue, Headers}; +use crate::{Body, Error, Method, Request, Response, StatusCode, Url, Version}; +use std::convert::{TryFrom, TryInto}; use std::str::FromStr; impl From for Method { @@ -54,6 +54,92 @@ impl From for http::Version { } } +impl TryFrom for HeaderName { + type Error = Error; + + fn try_from(name: http::header::HeaderName) -> Result { + let name = name.as_str().as_bytes().to_owned(); + HeaderName::from_bytes(name) + } +} + +impl TryFrom for http::header::HeaderName { + type Error = Error; + + fn try_from(name: HeaderName) -> Result { + let name = name.as_str().as_bytes(); + http::header::HeaderName::from_bytes(name).map_err(Error::new_adhoc) + } +} + +impl TryFrom for HeaderValue { + type Error = Error; + + fn try_from(value: http::header::HeaderValue) -> Result { + let value = value.as_bytes().to_owned(); + HeaderValue::from_bytes(value) + } +} + +impl TryFrom for http::header::HeaderValue { + type Error = Error; + + fn try_from(value: HeaderValue) -> Result { + let value = value.as_str().as_bytes(); + http::header::HeaderValue::from_bytes(value).map_err(Error::new_adhoc) + } +} + +impl TryFrom for Headers { + type Error = Error; + + fn try_from(hyperium_headers: http::HeaderMap) -> Result { + let mut headers = Headers::new(); + + hyperium_headers + .into_iter() + .map(|(name, value)| { + if let Some(name) = name { + let value: HeaderValue = value.try_into()?; + let name: HeaderName = name.try_into()?; + headers.append(name, value); + } + Ok(()) + }) + .collect::, Error>>()?; + + Ok(headers) + } +} + +impl TryFrom for http::HeaderMap { + type Error = Error; + + fn try_from(headers: Headers) -> Result { + let mut hyperium_headers = http::HeaderMap::new(); + + headers + .into_iter() + .map(|(name, values)| { + let name: http::header::HeaderName = name.try_into()?; + + values + .into_iter() + .map(|value| { + let value: http::header::HeaderValue = value.try_into()?; + hyperium_headers.append(&name, value); + Ok(()) + }) + .collect::, Error>>()?; + + Ok(()) + }) + .collect::, Error>>()?; + + Ok(hyperium_headers) + } +} + fn hyperium_headers_to_headers(hyperium_headers: http::HeaderMap, headers: &mut Headers) { for (name, value) in hyperium_headers { let value = value.as_bytes().to_owned(); @@ -61,7 +147,7 @@ fn hyperium_headers_to_headers(hyperium_headers: http::HeaderMap, headers: &mut if let Some(name) = name { let name = name.as_str().as_bytes().to_owned(); let name = unsafe { HeaderName::from_bytes_unchecked(name) }; - headers.insert(name, value).unwrap(); + headers.append(name, value); } } } diff --git a/src/lib.rs b/src/lib.rs index 480ce106..7da04da4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,9 +8,9 @@ //! ``` //! # fn main() -> Result<(), http_types::url::ParseError> { //! # -//! use http_types::{Url, Method, Request, Response, StatusCode}; +//! use http_types::{Method, Request, Response, StatusCode}; //! -//! let mut req = Request::new(Method::Get, Url::parse("https://example.com")?); +//! let mut req = Request::new(Method::Get, "https://example.com"); //! req.set_body("Hello, Nori!"); //! //! let mut res = Response::new(StatusCode::Ok); diff --git a/src/macros.rs b/src/macros.rs index b23b2b7f..e29ae6c3 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -2,13 +2,13 @@ #[macro_export] macro_rules! bail { ($msg:literal $(,)?) => { - return $crate::private::Err($crate::format_err!($msg)); + return $crate::private::Err($crate::format_err!($msg)) }; ($msg:expr $(,)?) => { - return $crate::private::Err($crate::format_err!($msg)); + return $crate::private::Err($crate::format_err!($msg)) }; ($msg:expr, $($arg:tt)*) => { - return $crate::private::Err($crate::format_err!($msg, $($arg)*)); + return $crate::private::Err($crate::format_err!($msg, $($arg)*)) }; } @@ -23,17 +23,17 @@ macro_rules! bail { macro_rules! ensure { ($cond:expr, $msg:literal $(,)?) => { if !$cond { - return $crate::private::Err($crate::format_err!($msg)); + return $crate::private::Err($crate::format_err!($msg)) } }; ($cond:expr, $msg:expr $(,)?) => { if !$cond { - return $crate::private::Err($crate::format_err!($msg)); + return $crate::private::Err($crate::format_err!($msg)) } }; ($cond:expr, $msg:expr, $($arg:tt)*) => { if !$cond { - return $crate::private::Err($crate::format_err!($msg, $($arg)*)); + return $crate::private::Err($crate::format_err!($msg, $($arg)*)) } }; } @@ -49,17 +49,17 @@ macro_rules! ensure { macro_rules! ensure_eq { ($left:expr, $right:expr, $msg:literal $(,)?) => { if $left != $right { - return $crate::private::Err($crate::format_err!($msg)); + return $crate::private::Err($crate::format_err!($msg)) } }; ($left:expr, $right:expr, $msg:expr $(,)?) => { if $left != $right { - return $crate::private::Err($crate::format_err!($msg)); + return $crate::private::Err($crate::format_err!($msg)) } }; ($left:expr, $right:expr, $msg:expr, $($arg:tt)*) => { if $left != $right { - return $crate::private::Err($crate::format_err!($msg, $($arg)*)); + return $crate::private::Err($crate::format_err!($msg, $($arg)*)) } }; } @@ -90,13 +90,13 @@ macro_rules! format_err { #[macro_export] macro_rules! bail_status { ($status:literal, $msg:literal $(,)?) => {{ - return $crate::private::Err($crate::format_err_status!($status, $msg)); + return $crate::private::Err($crate::format_err_status!($status, $msg)) }}; ($status:literal, $msg:expr $(,)?) => { - return $crate::private::Err($crate::format_err_status!($status, $msg)); + return $crate::private::Err($crate::format_err_status!($status, $msg)) }; ($status:literal, $msg:expr, $($arg:tt)*) => { - return $crate::private::Err($crate::format_err_status!($status, $msg, $($arg)*)); + return $crate::private::Err($crate::format_err_status!($status, $msg, $($arg)*)) }; } @@ -112,17 +112,17 @@ macro_rules! bail_status { macro_rules! ensure_status { ($cond:expr, $status:literal, $msg:literal $(,)?) => { if !$cond { - return $crate::private::Err($crate::format_err_status!($status, $msg)); + return $crate::private::Err($crate::format_err_status!($status, $msg)) } }; ($cond:expr, $status:literal, $msg:expr $(,)?) => { if !$cond { - return $crate::private::Err($crate::format_err_status!($status, $msg)); + return $crate::private::Err($crate::format_err_status!($status, $msg)) } }; ($cond:expr, $status:literal, $msg:expr, $($arg:tt)*) => { if !$cond { - return $crate::private::Err($crate::format_err_status!($status, $msg, $($arg)*)); + return $crate::private::Err($crate::format_err_status!($status, $msg, $($arg)*)) } }; } @@ -139,17 +139,17 @@ macro_rules! ensure_status { macro_rules! ensure_eq_status { ($left:expr, $right:expr, $status:literal, $msg:literal $(,)?) => { if $left != $right { - return $crate::private::Err($crate::format_err_status!($status, $msg)); + return $crate::private::Err($crate::format_err_status!($status, $msg)) } }; ($left:expr, $right:expr, $status:literal, $msg:expr $(,)?) => { if $left != $right { - return $crate::private::Err($crate::format_err_status!($status, $msg)); + return $crate::private::Err($crate::format_err_status!($status, $msg)) } }; ($left:expr, $right:expr, $status:literal, $msg:expr, $($arg:tt)*) => { if $left != $right { - return $crate::private::Err($crate::format_err_status!($status, $msg, $($arg)*)); + return $crate::private::Err($crate::format_err_status!($status, $msg, $($arg)*)) } }; } diff --git a/src/method.rs b/src/method.rs index 52718f30..4507b97a 100644 --- a/src/method.rs +++ b/src/method.rs @@ -5,49 +5,354 @@ use std::str::FromStr; /// HTTP request methods. /// -/// [Read more](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) +/// See also [Mozilla's documentation][Mozilla docs], the [RFC7231, Section 4][] and +/// [IANA's Hypertext Transfer Protocol (HTTP) Method Registry][HTTP Method Registry]. +/// +/// [Mozilla docs]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods +/// [RFC7231, Section 4]: https://tools.ietf.org/html/rfc7231#section-4 +/// [HTTP Method Registry]: https://www.iana.org/assignments/http-methods/http-methods.xhtml #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] pub enum Method { - /// The GET method requests a representation of the specified resource. Requests using GET - /// should only retrieve data. + /// The ACL method modifies the access control list (which can be read via the DAV:acl + /// property) of a resource. + /// + /// See [RFC3744, Section 8.1][]. + /// + /// [RFC3744, Section 8.1]: https://tools.ietf.org/html/rfc3744#section-8.1 + Acl, + + /// A collection can be placed under baseline control with a BASELINE-CONTROL request. + /// + /// See [RFC3253, Section 12.6][]. + /// + /// [RFC3253, Section 12.6]: https://tools.ietf.org/html/rfc3253#section-12.6 + BaselineControl, + + /// The BIND method modifies the collection identified by the Request- URI, by adding a new + /// binding from the segment specified in the BIND body to the resource identified in the BIND + /// body. + /// + /// See [RFC5842, Section 4][]. + /// + /// [RFC5842, Section 4]: https://tools.ietf.org/html/rfc5842#section-4 + Bind, + + /// A CHECKIN request can be applied to a checked-out version-controlled resource to produce a + /// new version whose content and dead properties are copied from the checked-out resource. + /// + /// See [RFC3253, Section 4.4][] and [RFC3253, Section 9.4][]. + /// + /// [RFC3253, Section 4.4]: https://tools.ietf.org/html/rfc3253#section-4.4 + /// [RFC3253, Section 9.4]: https://tools.ietf.org/html/rfc3253#section-9.4 + Checkin, + + /// A CHECKOUT request can be applied to a checked-in version-controlled resource to allow + /// modifications to the content and dead properties of that version-controlled resource. + /// + /// See [RFC3253, Section 4.3][] and [RFC3253, Section 8.8][]. + /// + /// [RFC3253, Section 4.3]: https://tools.ietf.org/html/rfc3253#section-4.3 + /// [RFC3253, Section 8.8]: https://tools.ietf.org/html/rfc3253#section-8.8 + Checkout, + + /// The CONNECT method requests that the recipient establish a tunnel to the destination origin + /// server identified by the request-target and, if successful, thereafter restrict its + /// behavior to blind forwarding of packets, in both directions, until the tunnel is closed. + /// + /// See [RFC7231, Section 4.3.6][]. + /// + /// [RFC7231, Section 4.3.6]: https://tools.ietf.org/html/rfc7231#section-4.3.6 + Connect, + + /// The COPY method creates a duplicate of the source resource identified by the Request-URI, + /// in the destination resource identified by the URI in the Destination header. + /// + /// See [RFC4918, Section 9.8][]. + /// + /// [RFC4918, Section 9.8]: https://tools.ietf.org/html/rfc4918#section-9.8 + Copy, + + /// The DELETE method requests that the origin server remove the association between the target + /// resource and its current functionality. + /// + /// See [RFC7231, Section 4.3.5][]. + /// + /// [RFC7231, Section 4.3.5]: https://tools.ietf.org/html/rfc7231#section-4.3.5 + Delete, + + /// The GET method requests transfer of a current selected representation for the target + /// resource. + /// + /// See [RFC7231, Section 4.3.1][]. + /// + /// [RFC7231, Section 4.3.1]: https://tools.ietf.org/html/rfc7231#section-4.3.1 Get, - /// The HEAD method asks for a response identical to that of a GET request, but without the response body. + /// The HEAD method is identical to GET except that the server MUST NOT send a message body in + /// the response. + /// + /// See [RFC7231, Section 4.3.2][]. + /// + /// [RFC7231, Section 4.3.2]: https://tools.ietf.org/html/rfc7231#section-4.3.2 Head, - /// The POST method is used to submit an entity to the specified resource, often causing a - /// change in state or side effects on the server. + /// A LABEL request can be applied to a version to modify the labels that select that version. + /// + /// See [RFC3253, Section 8.2][]. + /// + /// [RFC3253, Section 8.2]: https://tools.ietf.org/html/rfc3253#section-8.2 + Label, + + /// The LINK method establishes one or more Link relationships between the existing resource + /// identified by the Request-URI and other existing resources. + /// + /// See [RFC2068, Section 19.6.1.2][]. + /// + /// [RFC2068, Section 19.6.1.2]: https://tools.ietf.org/html/rfc2068#section-19.6.1.2 + Link, + + /// The LOCK method is used to take out a lock of any access type and to refresh an existing + /// lock. + /// + /// See [RFC4918, Section 9.10][]. + /// + /// [RFC4918, Section 9.10]: https://tools.ietf.org/html/rfc4918#section-9.10 + Lock, + + /// The MERGE method performs the logical merge of a specified version (the "merge source") + /// into a specified version-controlled resource (the "merge target"). + /// + /// See [RFC3253, Section 11.2][]. + /// + /// [RFC3253, Section 11.2]: https://tools.ietf.org/html/rfc3253#section-11.2 + Merge, + + /// A MKACTIVITY request creates a new activity resource. + /// + /// See [RFC3253, Section 13.5]. + /// + /// [RFC3253, Section 13.5]: https://tools.ietf.org/html/rfc3253#section-13.5 + MkActivity, + + /// An HTTP request using the MKCALENDAR method creates a new calendar collection resource. + /// + /// See [RFC4791, Section 5.3.1][] and [RFC8144, Section 2.3][]. + /// + /// [RFC4791, Section 5.3.1]: https://tools.ietf.org/html/rfc4791#section-5.3.1 + /// [RFC8144, Section 2.3]: https://tools.ietf.org/html/rfc8144#section-2.3 + MkCalendar, + + /// MKCOL creates a new collection resource at the location specified by the Request-URI. + /// + /// See [RFC4918, Section 9.3][], [RFC5689, Section 3][] and [RFC8144, Section 2.3][]. + /// + /// [RFC4918, Section 9.3]: https://tools.ietf.org/html/rfc4918#section-9.3 + /// [RFC5689, Section 3]: https://tools.ietf.org/html/rfc5689#section-3 + /// [RFC8144, Section 2.3]: https://tools.ietf.org/html/rfc5689#section-3 + MkCol, + + /// The MKREDIRECTREF method requests the creation of a redirect reference resource. + /// + /// See [RFC4437, Section 6][]. + /// + /// [RFC4437, Section 6]: https://tools.ietf.org/html/rfc4437#section-6 + MkRedirectRef, + + /// A MKWORKSPACE request creates a new workspace resource. + /// + /// See [RFC3253, Section 6.3][]. + /// + /// [RFC3253, Section 6.3]: https://tools.ietf.org/html/rfc3253#section-6.3 + MkWorkspace, + + /// The MOVE operation on a non-collection resource is the logical equivalent of a copy (COPY), + /// followed by consistency maintenance processing, followed by a delete of the source, where + /// all three actions are performed in a single operation. + /// + /// See [RFC4918, Section 9.9][]. + /// + /// [RFC4918, Section 9.9]: https://tools.ietf.org/html/rfc4918#section-9.9 + Move, + + /// The OPTIONS method requests information about the communication options available for the + /// target resource, at either the origin server or an intervening intermediary. + /// + /// See [RFC7231, Section 4.3.7][]. + /// + /// [RFC7231, Section 4.3.7]: https://tools.ietf.org/html/rfc7231#section-4.3.7 + Options, + + /// The ORDERPATCH method is used to change the ordering semantics of a collection, to change + /// the order of the collection's members in the ordering, or both. + /// + /// See [RFC3648, Section 7][]. + /// + /// [RFC3648, Section 7]: https://tools.ietf.org/html/rfc3648#section-7 + OrderPatch, + + /// The PATCH method requests that a set of changes described in the request entity be applied + /// to the resource identified by the Request- URI. + /// + /// See [RFC5789, Section 2][]. + /// + /// [RFC5789, Section 2]: https://tools.ietf.org/html/rfc5789#section-2 + Patch, + + /// The POST method requests that the target resource process the representation enclosed in + /// the request according to the resource's own specific semantics. + /// + /// For example, POST is used for the following functions (among others): + /// + /// - Providing a block of data, such as the fields entered into an HTML form, to a + /// data-handling process; + /// - Posting a message to a bulletin board, newsgroup, mailing list, blog, or similar group + /// of articles; + /// - Creating a new resource that has yet to be identified by the origin server; and + /// - Appending data to a resource's existing representation(s). + /// + /// See [RFC7231, Section 4.3.3][]. + /// + /// [RFC7231, Section 4.3.3]: https://tools.ietf.org/html/rfc7231#section-4.3.3 Post, - /// The PUT method replaces all current representations of the target resource with the request - /// payload. + /// This method is never used by an actual client. This method will appear to be used when an + /// HTTP/1.1 server or intermediary attempts to parse an HTTP/2 connection preface. + /// + /// See [RFC7540, Section 3.5][] and [RFC7540, Section 11.6][] + /// + /// [RFC7540, Section 3.5]: https://tools.ietf.org/html/rfc7540#section-3.5 + /// [RFC7540, Section 11.6]: https://tools.ietf.org/html/rfc7540#section-11.6 + Pri, + + /// The PROPFIND method retrieves properties defined on the resource identified by the + /// Request-URI. + /// + /// See [RFC4918, Section 9.1][] and [RFC8144, Section 2.1][]. + /// + /// [RFC4918, Section 9.1]: https://tools.ietf.org/html/rfc4918#section-9.1 + /// [RFC8144, Section 2.1]: https://tools.ietf.org/html/rfc8144#section-2.1 + PropFind, + + /// The PROPPATCH method processes instructions specified in the request body to set and/or + /// remove properties defined on the resource identified by the Request-URI. + /// + /// See [RFC4918, Section 9.2][] and [RFC8144, Section 2.2][]. + /// + /// [RFC4918, Section 9.2]: https://tools.ietf.org/html/rfc4918#section-9.2 + /// [RFC8144, Section 2.2]: https://tools.ietf.org/html/rfc8144#section-2.2 + PropPatch, + + /// The PUT method requests that the state of the target resource be created or replaced with + /// the state defined by the representation enclosed in the request message payload. + /// + /// See [RFC7231, Section 4.3.4][]. + /// + /// [RFC7231, Section 4.3.4]: https://tools.ietf.org/html/rfc7231#section-4.3.4 Put, - /// The DELETE method deletes the specified resource. - Delete, + /// The REBIND method removes a binding to a resource from a collection, and adds a binding to + /// that resource into the collection identified by the Request-URI. + /// + /// See [RFC5842, Section 6][]. + /// + /// [RFC5842, Section 6]: https://tools.ietf.org/html/rfc5842#section-6 + Rebind, - /// The CONNECT method establishes a tunnel to the server identified by the target resource. - Connect, + /// A REPORT request is an extensible mechanism for obtaining information about a resource. + /// + /// See [RFC3253, Section 3.6][] and [RFC8144, Section 2.1][]. + /// + /// [RFC3253, Section 3.6]: https://tools.ietf.org/html/rfc3253#section-3.6 + /// [RFC8144, Section 2.1]: https://tools.ietf.org/html/rfc8144#section-2.1 + Report, - /// The OPTIONS method is used to describe the communication options for the target resource. - Options, + /// The client invokes the SEARCH method to initiate a server-side search. The body of the + /// request defines the query. + /// + /// See [RFC5323, Section 2][]. + /// + /// [RFC5323, Section 2]: https://tools.ietf.org/html/rfc5323#section-2 + Search, - /// The TRACE method performs a message loop-back test along the path to the target resource. + /// The TRACE method requests a remote, application-level loop-back of the request message. + /// + /// See [RFC7231, Section 4.3.8][]. + /// + /// [RFC7231, Section 4.3.8]: https://tools.ietf.org/html/rfc7231#section-4.3.8 Trace, - /// The PATCH method is used to apply partial modifications to a resource. - Patch, + /// The UNBIND method modifies the collection identified by the Request- URI by removing the + /// binding identified by the segment specified in the UNBIND body. + /// + /// See [RFC5842, Section 5][]. + /// + /// [RFC5842, Section 5]: https://tools.ietf.org/html/rfc5842#section-5 + Unbind, + + /// An UNCHECKOUT request can be applied to a checked-out version-controlled resource to cancel + /// the CHECKOUT and restore the pre-CHECKOUT state of the version-controlled resource. + /// + /// See [RFC3253, Section 4.5][]. + /// + /// [RFC3253, Section 4.5]: https://tools.ietf.org/html/rfc3253#section-4.5 + Uncheckout, + + /// The UNLINK method removes one or more Link relationships from the existing resource + /// identified by the Request-URI. + /// + /// See [RFC2068, Section 19.6.1.3][]. + /// + /// [RFC2068, Section 19.6.1.3]: https://tools.ietf.org/html/rfc2068#section-19.6.1.3 + Unlink, + + /// The UNLOCK method removes the lock identified by the lock token in the Lock-Token request + /// header. + /// + /// See [RFC4918, Section 9.11][]. + /// + /// [RFC4918, Section 9.11]: https://tools.ietf.org/html/rfc4918#section-9.11 + Unlock, + + /// The UPDATE method modifies the content and dead properties of a checked-in + /// version-controlled resource (the "update target") to be those of a specified version (the + /// "update source") from the version history of that version-controlled resource. + /// + /// See [RFC3253, Section 7.1][]. + /// + /// [RFC3253, Section 7.1]: https://tools.ietf.org/html/rfc3253#section-7.1 + Update, + + /// The UPDATEREDIRECTREF method requests the update of a redirect reference resource. + /// + /// See [RFC4437, Section 7][]. + /// + /// [RFC4437, Section 7]: https://tools.ietf.org/html/rfc4437#section-7 + UpdateRedirectRef, + + /// A VERSION-CONTROL request can be used to create a version-controlled resource at the + /// request-URL. + /// + /// See [RFC3253, Section 3.5]. + /// + /// [RFC3253, Section 3.5]: https://tools.ietf.org/html/rfc3253#section-3.5 + VersionControl, } impl Method { - /// Whether a method is considered "safe", meaning the request is - /// essentially read-only. + /// Whether a method is considered "safe", meaning the request is essentially read-only. /// /// See [the spec](https://tools.ietf.org/html/rfc7231#section-4.2.1) for more details. pub fn is_safe(&self) -> bool { matches!( self, - Method::Get | Method::Head | Method::Options | Method::Trace + Method::Get + | Method::Head + | Method::Options + | Method::Pri + | Method::PropFind + | Method::Report + | Method::Search + | Method::Trace ) } } @@ -86,23 +391,13 @@ impl Serialize for Method { where S: Serializer, { - serializer.serialize_str(&self.to_string()) + serializer.serialize_str(self.as_ref()) } } impl Display for Method { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Get => write!(f, "GET"), - Self::Head => write!(f, "HEAD"), - Self::Post => write!(f, "POST"), - Self::Put => write!(f, "PUT"), - Self::Delete => write!(f, "DELETE"), - Self::Connect => write!(f, "CONNECT"), - Self::Options => write!(f, "OPTIONS"), - Self::Trace => write!(f, "TRACE"), - Self::Patch => write!(f, "PATCH"), - } + f.write_str(AsRef::::as_ref(self)) } } @@ -111,15 +406,45 @@ impl FromStr for Method { fn from_str(s: &str) -> Result { match &*s.to_ascii_uppercase() { + "ACL" => Ok(Self::Acl), + "BASELINE-CONTROL" => Ok(Self::BaselineControl), + "BIND" => Ok(Self::Bind), + "CHECKIN" => Ok(Self::Checkin), + "CHECKOUT" => Ok(Self::Checkout), + "CONNECT" => Ok(Self::Connect), + "COPY" => Ok(Self::Copy), + "DELETE" => Ok(Self::Delete), "GET" => Ok(Self::Get), "HEAD" => Ok(Self::Head), + "LABEL" => Ok(Self::Label), + "LINK" => Ok(Self::Link), + "LOCK" => Ok(Self::Lock), + "MERGE" => Ok(Self::Merge), + "MKACTIVITY" => Ok(Self::MkActivity), + "MKCALENDAR" => Ok(Self::MkCalendar), + "MKCOL" => Ok(Self::MkCol), + "MKREDIRECTREF" => Ok(Self::MkRedirectRef), + "MKWORKSPACE" => Ok(Self::MkWorkspace), + "MOVE" => Ok(Self::Move), + "OPTIONS" => Ok(Self::Options), + "ORDERPATCH" => Ok(Self::OrderPatch), + "PATCH" => Ok(Self::Patch), "POST" => Ok(Self::Post), + "PRI" => Ok(Self::Pri), + "PROPFIND" => Ok(Self::PropFind), + "PROPPATCH" => Ok(Self::PropPatch), "PUT" => Ok(Self::Put), - "DELETE" => Ok(Self::Delete), - "CONNECT" => Ok(Self::Connect), - "OPTIONS" => Ok(Self::Options), + "REBIND" => Ok(Self::Rebind), + "REPORT" => Ok(Self::Report), + "SEARCH" => Ok(Self::Search), "TRACE" => Ok(Self::Trace), - "PATCH" => Ok(Self::Patch), + "UNBIND" => Ok(Self::Unbind), + "UNCHECKOUT" => Ok(Self::Uncheckout), + "UNLINK" => Ok(Self::Unlink), + "UNLOCK" => Ok(Self::Unlock), + "UPDATE" => Ok(Self::Update), + "UPDATEREDIRECTREF" => Ok(Self::UpdateRedirectRef), + "VERSION-CONTROL" => Ok(Self::VersionControl), _ => crate::bail!("Invalid HTTP method"), } } @@ -136,21 +461,53 @@ impl<'a> std::convert::TryFrom<&'a str> for Method { impl AsRef for Method { fn as_ref(&self) -> &str { match self { + Self::Acl => "ACL", + Self::BaselineControl => "BASELINE-CONTROL", + Self::Bind => "BIND", + Self::Checkin => "CHECKIN", + Self::Checkout => "CHECKOUT", + Self::Connect => "CONNECT", + Self::Copy => "COPY", + Self::Delete => "DELETE", Self::Get => "GET", Self::Head => "HEAD", + Self::Label => "LABEL", + Self::Link => "LINK", + Self::Lock => "LOCK", + Self::Merge => "MERGE", + Self::MkActivity => "MKACTIVITY", + Self::MkCalendar => "MKCALENDAR", + Self::MkCol => "MKCOL", + Self::MkRedirectRef => "MKREDIRECTREF", + Self::MkWorkspace => "MKWORKSPACE", + Self::Move => "MOVE", + Self::Options => "OPTIONS", + Self::OrderPatch => "ORDERPATCH", + Self::Patch => "PATCH", Self::Post => "POST", + Self::Pri => "PRI", + Self::PropFind => "PROPFIND", + Self::PropPatch => "PROPPATCH", Self::Put => "PUT", - Self::Delete => "DELETE", - Self::Connect => "CONNECT", - Self::Options => "OPTIONS", + Self::Rebind => "REBIND", + Self::Report => "REPORT", + Self::Search => "SEARCH", Self::Trace => "TRACE", - Self::Patch => "PATCH", + Self::Unbind => "UNBIND", + Self::Uncheckout => "UNCHECKOUT", + Self::Unlink => "UNLINK", + Self::Unlock => "UNLOCK", + Self::Update => "UPDATE", + Self::UpdateRedirectRef => "UPDATEREDIRECTREF", + Self::VersionControl => "VERSION-CONTROL", } } } #[cfg(test)] mod test { + use std::collections::HashSet; + use super::Method; #[test] @@ -159,9 +516,69 @@ mod test { assert_eq!(Some("PATCH"), serde_json::to_value(Method::Patch)?.as_str()); Ok(()) } + #[test] - fn serde_fail() -> Result<(), serde_json::Error> { + fn serde_fail() { serde_json::from_str::("\"ABC\"").expect_err("Did deserialize from invalid string"); + } + + #[test] + fn names() -> Result<(), crate::Error> { + let method_names = [ + "ACL", + "BASELINE-CONTROL", + "BIND", + "CHECKIN", + "CHECKOUT", + "CONNECT", + "COPY", + "DELETE", + "GET", + "HEAD", + "LABEL", + "LINK", + "LOCK", + "MERGE", + "MKACTIVITY", + "MKCALENDAR", + "MKCOL", + "MKREDIRECTREF", + "MKWORKSPACE", + "MOVE", + "OPTIONS", + "ORDERPATCH", + "PATCH", + "POST", + "PRI", + "PROPFIND", + "PROPPATCH", + "PUT", + "REBIND", + "REPORT", + "SEARCH", + "TRACE", + "UNBIND", + "UNCHECKOUT", + "UNLINK", + "UNLOCK", + "UPDATE", + "UPDATEREDIRECTREF", + "VERSION-CONTROL", + ]; + + let methods = method_names + .iter() + .map(|s| s.parse::()) + .collect::, _>>()?; + + // check that we didn't accidentally map two methods to the same variant + assert_eq!(methods.len(), method_names.len()); + + // check that a method's name and the name it is parsed from match + for method in methods { + assert_eq!(method.as_ref().parse::()?, method); + } + Ok(()) } } diff --git a/src/mime/constants.rs b/src/mime/constants.rs index f4298c84..191c3785 100644 --- a/src/mime/constants.rs +++ b/src/mime/constants.rs @@ -20,12 +20,23 @@ macro_rules! mime_const { }; (with_params, $name:ident, $desc:expr, $base:expr, $sub:expr, $is_utf8:expr, $doccomment:expr) => { - mime_const!(doc_expanded, $name, $desc, $base, $sub, $is_utf8, - concat!( + mime_const!( + doc_expanded, + $name, + $desc, + $base, + $sub, + $is_utf8, + concat!( "Content-Type for ", $desc, ".\n\n# Mime Type\n\n```text\n", - $base, "/", $sub, $doccomment, "\n```") + $base, + "/", + $sub, + $doccomment, + "\n```" + ) ); }; diff --git a/src/mime/mod.rs b/src/mime/mod.rs index 8e46d26d..ddb207f0 100644 --- a/src/mime/mod.rs +++ b/src/mime/mod.rs @@ -43,7 +43,7 @@ impl Mime { /// Sniff the mime type from a byte slice. pub fn sniff(bytes: &[u8]) -> crate::Result { let info = Infer::new(); - let mime = match info.get(&bytes) { + let mime = match info.get(bytes) { Some(info) => info.mime, None => crate::bail!("Could not sniff the mime type"), }; diff --git a/src/mime/parse.rs b/src/mime/parse.rs index 4c02ec4a..f2ec5698 100644 --- a/src/mime/parse.rs +++ b/src/mime/parse.rs @@ -153,12 +153,12 @@ fn is_http_whitespace_char(c: char) -> bool { /// [code point sequence collection](https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points) fn collect_code_point_sequence_char(input: &str, delimiter: char) -> (&str, &str) { - input.split_at(input.find(delimiter).unwrap_or_else(|| input.len())) + input.split_at(input.find(delimiter).unwrap_or(input.len())) } /// [code point sequence collection](https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points) fn collect_code_point_sequence_slice<'a>(input: &'a str, delimiter: &[char]) -> (&'a str, &'a str) { - input.split_at(input.find(delimiter).unwrap_or_else(|| input.len())) + input.split_at(input.find(delimiter).unwrap_or(input.len())) } /// [HTTP quoted string collection](https://fetch.spec.whatwg.org/#collect-an-http-quoted-string) diff --git a/src/other/date.rs b/src/other/date.rs index 6889af95..e416adda 100644 --- a/src/other/date.rs +++ b/src/other/date.rs @@ -132,11 +132,10 @@ mod test { } #[test] - fn bad_request_on_parse_error() -> crate::Result<()> { + fn bad_request_on_parse_error() { let mut headers = Headers::new(); headers.insert(DATE, ""); let err = Date::from_headers(headers).unwrap_err(); assert_eq!(err.status(), 400); - Ok(()) } } diff --git a/src/other/expect.rs b/src/other/expect.rs index 5e8fd307..895487c2 100644 --- a/src/other/expect.rs +++ b/src/other/expect.rs @@ -100,11 +100,10 @@ mod test { } #[test] - fn bad_request_on_parse_error() -> crate::Result<()> { + fn bad_request_on_parse_error() { let mut headers = Headers::new(); headers.insert(EXPECT, ""); let err = Expect::from_headers(headers).unwrap_err(); assert_eq!(err.status(), 400); - Ok(()) } } diff --git a/src/other/referer.rs b/src/other/referer.rs index 10510b1b..bbb09d14 100644 --- a/src/other/referer.rs +++ b/src/other/referer.rs @@ -126,13 +126,12 @@ mod test { } #[test] - fn bad_request_on_parse_error() -> crate::Result<()> { + fn bad_request_on_parse_error() { let mut headers = Headers::new(); headers.insert(REFERER, "htt://"); let err = Referer::from_headers(Url::parse("https://example.net").unwrap(), headers).unwrap_err(); assert_eq!(err.status(), 500); - Ok(()) } #[test] diff --git a/src/other/source_map.rs b/src/other/source_map.rs index 349a99a2..225d0aab 100644 --- a/src/other/source_map.rs +++ b/src/other/source_map.rs @@ -123,13 +123,12 @@ mod test { } #[test] - fn bad_request_on_parse_error() -> crate::Result<()> { + fn bad_request_on_parse_error() { let mut headers = Headers::new(); headers.insert(SOURCE_MAP, "htt://"); let err = SourceMap::from_headers(Url::parse("https://example.net").unwrap(), headers) .unwrap_err(); assert_eq!(err.status(), 500); - Ok(()) } #[test] diff --git a/src/parse_utils.rs b/src/parse_utils.rs index 7e4674dd..a2dc2356 100644 --- a/src/parse_utils.rs +++ b/src/parse_utils.rs @@ -5,7 +5,7 @@ pub(crate) fn parse_token(input: &str) -> (Option<&str>, &str) { let mut end_of_token = 0; for (i, c) in input.char_indices() { if tchar(c) { - end_of_token = i; + end_of_token = i + 1; } else { break; } @@ -14,7 +14,7 @@ pub(crate) fn parse_token(input: &str) -> (Option<&str>, &str) { if end_of_token == 0 { (None, input) } else { - (Some(&input[..end_of_token + 1]), &input[end_of_token + 1..]) + (Some(&input[..end_of_token]), &input[end_of_token..]) } } @@ -125,7 +125,7 @@ mod test { assert_eq!(parse_token("key=value"), (Some("key"), "=value")); assert_eq!(parse_token("KEY=value"), (Some("KEY"), "=value")); assert_eq!(parse_token("0123)=value"), (Some("0123"), ")=value")); - + assert_eq!(parse_token("a=b"), (Some("a"), "=b")); assert_eq!( parse_token("!#$%&'*+-.^_`|~=value"), (Some("!#$%&'*+-.^_`|~"), "=value",) diff --git a/src/proxies/forwarded.rs b/src/proxies/forwarded.rs index 79b95442..f8de7e5a 100644 --- a/src/proxies/forwarded.rs +++ b/src/proxies/forwarded.rs @@ -11,7 +11,7 @@ const X_FORWARDED_BY: HeaderName = HeaderName::from_lowercase_str("x-forwarded-b /// A rust representation of the [forwarded /// header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded). -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct Forwarded<'a> { by: Option>, forwarded_for: Vec>, @@ -197,12 +197,12 @@ impl<'a> Forwarded<'a> { let mut input = input; let mut forwarded = Forwarded::new(); - if starts_with_ignore_case("for=", input) { - input = forwarded.parse_for(input)?; - } - while !input.is_empty() { - input = forwarded.parse_forwarded_pair(input)?; + input = if starts_with_ignore_case("for=", input) { + forwarded.parse_for(input)? + } else { + forwarded.parse_forwarded_pair(input)? + } } Ok(forwarded) @@ -442,8 +442,12 @@ fn match_ignore_case<'a>(start: &'static str, input: &'a str) -> (bool, &'a str) } fn starts_with_ignore_case(start: &'static str, input: &str) -> bool { - let len = start.len(); - input[..len].eq_ignore_ascii_case(start) + if start.len() <= input.len() { + let len = start.len(); + input[..len].eq_ignore_ascii_case(start) + } else { + false + } } impl std::fmt::Display for Forwarded<'_> { @@ -455,14 +459,14 @@ impl std::fmt::Display for Forwarded<'_> { impl ToHeaderValues for Forwarded<'_> { type Iter = std::option::IntoIter; fn to_header_values(&self) -> crate::Result { - Ok(self.value()?.to_header_values()?) + self.value()?.to_header_values() } } impl ToHeaderValues for &Forwarded<'_> { type Iter = std::option::IntoIter; fn to_header_values(&self) -> crate::Result { - Ok(self.value()?.to_header_values()?) + self.value()?.to_header_values() } } @@ -494,6 +498,11 @@ mod tests { use crate::{Method::Get, Request, Response, Result}; use url::Url; + #[test] + fn starts_with_ignore_case_can_handle_short_inputs() { + assert!(!starts_with_ignore_case("helloooooo", "h")); + } + #[test] fn parsing_for() -> Result<()> { assert_eq!( @@ -545,12 +554,12 @@ mod tests { assert_eq!(forwarded.forwarded_for(), vec!["client.com"]); assert_eq!(forwarded.host(), Some("host.com")); assert_eq!(forwarded.proto(), Some("https")); - assert!(matches!(forwarded, Forwarded{..})); + assert!(matches!(forwarded, Forwarded { .. })); Ok(()) } #[test] - fn bad_parse() -> Result<()> { + fn bad_parse() { let err = Forwarded::parse("by=proxy.com;for=client;host=example.com;host").unwrap_err(); assert_eq!( err.to_string(), @@ -580,7 +589,6 @@ mod tests { err.to_string(), "unable to parse forwarded header: for= without valid value" ); - Ok(()) } #[test] @@ -611,7 +619,7 @@ mod tests { } #[test] - fn formatting_edge_cases() -> Result<()> { + fn formatting_edge_cases() { let mut forwarded = Forwarded::new(); forwarded.add_for(r#"quote: " backslash: \"#); forwarded.add_for(";proto=https"); @@ -619,7 +627,6 @@ mod tests { forwarded.to_string(), r#"for="quote: \" backslash: \\", for=";proto=https""# ); - Ok(()) } #[test] @@ -644,7 +651,7 @@ mod tests { assert_eq!(forwarded.forwarded_for(), vec!["client"]); assert_eq!(forwarded.host(), Some("example.com")); assert_eq!(forwarded.proto(), Some("https")); - assert!(matches!(forwarded, Forwarded{..})); + assert!(matches!(forwarded, Forwarded { .. })); Ok(()) } @@ -669,4 +676,19 @@ mod tests { assert_eq!(forwarded.by(), Some("by")); Ok(()) } + + #[test] + fn round_trip() -> Result<()> { + let inputs = [ + "for=client,for=b,for=c;by=proxy.com;host=example.com;proto=https", + "by=proxy.com;proto=https;host=example.com;for=a,for=b", + ]; + for input in inputs { + let forwarded = Forwarded::parse(input).map_err(|_| crate::Error::new_adhoc(input))?; + let header = forwarded.to_header_values()?.next().unwrap(); + let parsed = Forwarded::parse(header.as_str())?; + assert_eq!(forwarded, parsed); + } + Ok(()) + } } diff --git a/src/request.rs b/src/request.rs index a0ff58c5..d442a6a1 100644 --- a/src/request.rs +++ b/src/request.rs @@ -6,7 +6,7 @@ use std::ops::Index; use std::pin::Pin; use std::task::{Context, Poll}; -use crate::convert::{DeserializeOwned, Serialize}; +use crate::convert::{Deserialize, DeserializeOwned, Serialize}; use crate::headers::{ self, HeaderName, HeaderValue, HeaderValues, Headers, Names, ToHeaderValues, Values, CONTENT_TYPE, @@ -21,9 +21,9 @@ pin_project_lite::pin_project! { /// # Examples /// /// ``` - /// use http_types::{Url, Method, Request}; + /// use http_types::Request; /// - /// let mut req = Request::new(Method::Get, Url::parse("https://example.com").unwrap()); + /// let mut req = Request::get("https://example.com"); /// req.set_body("Hello, Nori!"); /// ``` #[derive(Debug)] @@ -157,8 +157,8 @@ impl Request { /// ``` /// # fn main() -> Result<(), http_types::Error> { /// # - /// use http_types::{Method, Request, Response, StatusCode, Url}; - /// let mut req = Request::new(Method::Get, Url::parse("https://example.com")?); + /// use http_types::{Request, Response, StatusCode}; + /// let mut req = Request::get("https://example.com"); /// assert_eq!(req.url().scheme(), "https"); /// # /// # Ok(()) } @@ -175,7 +175,7 @@ impl Request { /// # fn main() -> Result<(), http_types::Error> { /// # /// use http_types::{Method, Request, Response, StatusCode, Url}; - /// let mut req = Request::new(Method::Get, Url::parse("https://example.com")?); + /// let mut req = Request::get("https://example.com"); /// req.url_mut().set_scheme("http"); /// assert_eq!(req.url().scheme(), "http"); /// # @@ -190,9 +190,9 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request, Url}; + /// use http_types::{Method, Request}; /// - /// let mut req = Request::new(Method::Get, Url::parse("https://example.com").unwrap()); + /// let mut req = Request::get("https://example.com"); /// req.set_body("Hello, Nori!"); /// ``` pub fn set_body(&mut self, body: impl Into) { @@ -208,9 +208,9 @@ impl Request { /// # use async_std::io::prelude::*; /// # fn main() -> http_types::Result<()> { async_std::task::block_on(async { /// # - /// use http_types::{Body, Method, Request, Url}; + /// use http_types::{Body, Method, Request}; /// - /// let mut req = Request::new(Method::Get, Url::parse("https://example.com").unwrap()); + /// let mut req = Request::get("https://example.com"); /// req.set_body("Hello, Nori!"); /// let mut body: Body = req.replace_body("Hello, Chashu!"); /// @@ -234,9 +234,9 @@ impl Request { /// # use async_std::io::prelude::*; /// # fn main() -> http_types::Result<()> { async_std::task::block_on(async { /// # - /// use http_types::{Body, Method, Request, Url}; + /// use http_types::{Body, Request}; /// - /// let mut req = Request::new(Method::Get, Url::parse("https://example.com").unwrap()); + /// let mut req = Request::get("https://example.com"); /// req.set_body("Hello, Nori!"); /// let mut body = "Hello, Chashu!".into(); /// req.swap_body(&mut body); @@ -260,9 +260,9 @@ impl Request { /// # use async_std::io::prelude::*; /// # fn main() -> http_types::Result<()> { async_std::task::block_on(async { /// # - /// use http_types::{Body, Method, Request, Url}; + /// use http_types::{Body, Request}; /// - /// let mut req = Request::new(Method::Get, Url::parse("https://example.com").unwrap()); + /// let mut req = Request::get("https://example.com"); /// req.set_body("Hello, Nori!"); /// let mut body: Body = req.take_body(); /// @@ -293,9 +293,9 @@ impl Request { /// # use std::io::prelude::*; /// # fn main() -> http_types::Result<()> { async_std::task::block_on(async { /// use async_std::io::Cursor; - /// use http_types::{Body, Method, Request, Url}; + /// use http_types::{Body, Request}; /// - /// let mut req = Request::new(Method::Get, Url::parse("https://example.com").unwrap()); + /// let mut req = Request::get("https://example.com"); /// /// let cursor = Cursor::new("Hello Nori"); /// let body = Body::from_reader(cursor, None); @@ -319,10 +319,10 @@ impl Request { /// /// ``` /// # fn main() -> http_types::Result<()> { async_std::task::block_on(async { - /// use http_types::{Body, Method, Request, Url}; + /// use http_types::{Body, Request}; /// /// let bytes = vec![1, 2, 3]; - /// let mut req = Request::new(Method::Get, Url::parse("https://example.com").unwrap()); + /// let mut req = Request::get("https://example.com"); /// req.set_body(Body::from_bytes(bytes)); /// /// let bytes = req.body_bytes().await?; @@ -346,7 +346,7 @@ impl Request { /// ``` /// # fn main() -> http_types::Result<()> { async_std::task::block_on(async { /// use http_types::convert::{Deserialize, Serialize}; - /// use http_types::{Body, Method, Request, Url}; + /// use http_types::{Body, Request}; /// /// #[derive(Debug, Serialize, Deserialize)] /// struct Cat { @@ -356,7 +356,7 @@ impl Request { /// let cat = Cat { /// name: String::from("chashu"), /// }; - /// let mut req = Request::new(Method::Get, Url::parse("https://example.com").unwrap()); + /// let mut req = Request::get("https://example.com"); /// req.set_body(Body::from_json(&cat)?); /// /// let cat: Cat = req.body_json().await?; @@ -380,7 +380,7 @@ impl Request { /// ``` /// # fn main() -> http_types::Result<()> { async_std::task::block_on(async { /// use http_types::convert::{Deserialize, Serialize}; - /// use http_types::{Body, Method, Request, Url}; + /// use http_types::{Body, Request}; /// /// #[derive(Debug, Serialize, Deserialize)] /// struct Cat { @@ -390,7 +390,7 @@ impl Request { /// let cat = Cat { /// name: String::from("chashu"), /// }; - /// let mut req = Request::new(Method::Get, Url::parse("https://example.com").unwrap()); + /// let mut req = Request::get("https://example.com"); /// req.set_body(Body::from_form(&cat)?); /// /// let cat: Cat = req.body_form().await?; @@ -424,9 +424,9 @@ impl Request { /// ``` /// # fn main() -> Result<(), Box> { /// # - /// use http_types::{Method, Request, Url}; + /// use http_types::Request; /// - /// let mut req = Request::new(Method::Get, Url::parse("https://example.com")?); + /// let mut req = Request::get("https://example.com"); /// req.insert_header("Content-Type", "text/plain"); /// # /// # Ok(()) } @@ -450,9 +450,9 @@ impl Request { /// ``` /// # fn main() -> Result<(), Box> { /// # - /// use http_types::{Method, Request, Url}; + /// use http_types::Request; /// - /// let mut req = Request::new(Method::Get, Url::parse("https://example.com")?); + /// let mut req = Request::get("https://example.com"); /// req.append_header("Content-Type", "text/plain"); /// # /// # Ok(()) } @@ -503,11 +503,11 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request, Url, Version}; + /// use http_types::{Request, Version}; /// /// # fn main() -> Result<(), http_types::Error> { /// # - /// let mut req = Request::new(Method::Get, Url::parse("https://example.com")?); + /// let mut req = Request::get("https://example.com"); /// assert_eq!(req.version(), None); /// /// req.set_version(Some(Version::Http2_0)); @@ -524,11 +524,11 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request, Url, Version}; + /// use http_types::{Request, Version}; /// /// # fn main() -> Result<(), http_types::Error> { /// # - /// let mut req = Request::new(Method::Get, Url::parse("https://example.com")?); + /// let mut req = Request::get("https://example.com"); /// req.set_version(Some(Version::Http2_0)); /// # /// # Ok(()) } @@ -594,9 +594,9 @@ impl Request { /// ``` /// # fn main() -> Result<(), http_types::Error> { /// # - /// use http_types::{Method, Request, Url, Version}; + /// use http_types::{Request, Version}; /// - /// let mut req = Request::new(Method::Get, Url::parse("https://example.com")?); + /// let mut req = Request::get("https://example.com"); /// req.ext_mut().insert("hello from the extension"); /// assert_eq!(req.ext().get(), Some(&"hello from the extension")); /// # @@ -612,25 +612,35 @@ impl Request { /// /// ``` /// use http_types::convert::Deserialize; - /// use http_types::{Method, Request, Url}; + /// use http_types::Request; /// use std::collections::HashMap; /// + /// // An owned structure: + /// /// #[derive(Deserialize)] /// struct Index { /// page: u32, /// selections: HashMap, /// } /// - /// let req = Request::new( - /// Method::Get, - /// Url::parse("https://httpbin.org/get?page=2&selections[width]=narrow&selections[height]=tall").unwrap(), - /// ); + /// let mut req = Request::get("https://httpbin.org/get?page=2&selections[width]=narrow&selections[height]=tall"); /// let Index { page, selections } = req.query().unwrap(); /// assert_eq!(page, 2); /// assert_eq!(selections["width"], "narrow"); /// assert_eq!(selections["height"], "tall"); + /// + /// // Using borrows: + /// + /// #[derive(Deserialize)] + /// struct Query<'q> { + /// format: &'q str, + /// } + /// + /// let mut req = Request::get("https://httpbin.org/get?format=bananna"); + /// let Query { format } = req.query().unwrap(); + /// assert_eq!(format, "bananna"); /// ``` - pub fn query(&self) -> crate::Result { + pub fn query<'de, T: Deserialize<'de>>(&'de self) -> crate::Result { // Default to an empty query string if no query parameter has been specified. // This allows successful deserialisation of structs where all fields are optional // when none of those fields has actually been passed by the caller. @@ -648,7 +658,7 @@ impl Request { /// /// ``` /// use http_types::convert::Serialize; - /// use http_types::{Method, Request, Url}; + /// use http_types::{Method, Request}; /// use std::collections::HashMap; /// /// #[derive(Serialize)] @@ -658,10 +668,7 @@ impl Request { /// } /// /// let query = Index { page: 2, topics: vec!["rust", "crabs", "crustaceans"] }; - /// let mut req = Request::new( - /// Method::Get, - /// Url::parse("https://httpbin.org/get").unwrap(), - /// ); + /// let mut req = Request::get("https://httpbin.org/get"); /// req.set_query(&query).unwrap(); /// assert_eq!(req.url().query(), Some("page=2&topics[0]=rust&topics[1]=crabs&topics[2]=crustaceans")); /// ``` @@ -680,7 +687,7 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request, Url}; + /// use http_types::{Method, Request}; /// /// let mut req = Request::get("https://example.com"); /// req.set_body("Hello, Nori!"); @@ -702,7 +709,7 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request, Url}; + /// use http_types::{Method, Request}; /// /// let mut req = Request::head("https://example.com"); /// assert_eq!(req.method(), Method::Head); @@ -723,7 +730,7 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request, Url}; + /// use http_types::{Method, Request}; /// /// let mut req = Request::post("https://example.com"); /// assert_eq!(req.method(), Method::Post); @@ -744,7 +751,7 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request, Url}; + /// use http_types::{Method, Request}; /// /// let mut req = Request::put("https://example.com"); /// assert_eq!(req.method(), Method::Put); @@ -764,7 +771,7 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request, Url}; + /// use http_types::{Method, Request}; /// /// let mut req = Request::delete("https://example.com"); /// assert_eq!(req.method(), Method::Delete); @@ -785,7 +792,7 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request, Url}; + /// use http_types::{Method, Request}; /// /// let mut req = Request::connect("https://example.com"); /// assert_eq!(req.method(), Method::Connect); @@ -806,7 +813,7 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request, Url}; + /// use http_types::{Method, Request}; /// /// let mut req = Request::options("https://example.com"); /// assert_eq!(req.method(), Method::Options); @@ -827,7 +834,7 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request, Url}; + /// use http_types::{Method, Request}; /// /// let mut req = Request::trace("https://example.com"); /// assert_eq!(req.method(), Method::Trace); @@ -847,7 +854,7 @@ impl Request { /// # Examples /// /// ``` - /// use http_types::{Method, Request, Url}; + /// use http_types::{Method, Request}; /// /// let mut req = Request::patch("https://example.com"); /// assert_eq!(req.method(), Method::Patch); diff --git a/src/security/timing_allow_origin.rs b/src/security/timing_allow_origin.rs index 2c92d277..e88b59bb 100644 --- a/src/security/timing_allow_origin.rs +++ b/src/security/timing_allow_origin.rs @@ -308,12 +308,11 @@ mod test { } #[test] - fn bad_request_on_parse_error() -> crate::Result<()> { + fn bad_request_on_parse_error() { let mut headers = Headers::new(); headers.insert(TIMING_ALLOW_ORIGIN, "server; "); let err = TimingAllowOrigin::from_headers(headers).unwrap_err(); assert_eq!(err.status(), 400); - Ok(()) } #[test] @@ -326,7 +325,7 @@ mod test { origins.apply(&mut headers); let origins = TimingAllowOrigin::from_headers(headers)?.unwrap(); - assert_eq!(origins.wildcard(), true); + assert!(origins.wildcard()); let origin = origins.iter().next().unwrap(); assert_eq!(origin, &Url::parse("https://example.com")?); Ok(()) diff --git a/src/status_code.rs b/src/status_code.rs index b98ee9bd..d0bf8c4a 100644 --- a/src/status_code.rs +++ b/src/status_code.rs @@ -149,38 +149,32 @@ pub enum StatusCode { /// 400 Bad Request /// /// The server could not understand the request due to invalid syntax. + BadRequest = 400, + + /// 401 Unauthorized /// /// Although the HTTP standard specifies "unauthorized", semantically this /// response means "unauthenticated". That is, the client must /// authenticate itself to get the requested response. - BadRequest = 400, + Unauthorized = 401, - /// 401 Unauthorized + /// 402 Payment Required /// /// This response code is reserved for future use. The initial aim for /// creating this code was using it for digital payment systems, however /// this status code is used very rarely and no standard convention /// exists. - Unauthorized = 401, + PaymentRequired = 402, - /// 402 Payment Required + /// 403 Forbidden /// /// The client does not have access rights to the content; that is, it is /// unauthorized, so the server is refusing to give the requested /// resource. Unlike 401, the client's identity is known to the server. - PaymentRequired = 402, - - /// 403 Forbidden - /// - /// The server can not find requested resource. In the browser, this means - /// the URL is not recognized. In an API, this can also mean that the - /// endpoint is valid but the resource itself does not exist. Servers - /// may also send this response instead of 403 to hide the existence of - /// a resource from an unauthorized client. This response code is probably - /// the most famous one due to its frequent occurrence on the web. Forbidden = 403, /// 404 Not Found + /// /// The server can not find requested resource. In the browser, this means /// the URL is not recognized. In an API, this can also mean that the /// endpoint is valid but the resource itself does not exist. Servers @@ -433,7 +427,7 @@ impl StatusCode { /// If this returns `true` it indicates that the request was received, /// continuing process. pub fn is_informational(&self) -> bool { - let num: u16 = self.clone().into(); + let num: u16 = (*self).into(); (100..200).contains(&num) } @@ -442,7 +436,7 @@ impl StatusCode { /// If this returns `true` it indicates that the request was successfully /// received, understood, and accepted. pub fn is_success(&self) -> bool { - let num: u16 = self.clone().into(); + let num: u16 = (*self).into(); (200..300).contains(&num) } @@ -451,7 +445,7 @@ impl StatusCode { /// If this returns `true` it indicates that further action needs to be /// taken in order to complete the request. pub fn is_redirection(&self) -> bool { - let num: u16 = self.clone().into(); + let num: u16 = (*self).into(); (300..400).contains(&num) } @@ -460,7 +454,7 @@ impl StatusCode { /// If this returns `true` it indicates that the request contains bad syntax /// or cannot be fulfilled. pub fn is_client_error(&self) -> bool { - let num: u16 = self.clone().into(); + let num: u16 = (*self).into(); (400..500).contains(&num) } @@ -469,7 +463,7 @@ impl StatusCode { /// If this returns `true` it indicates that the server failed to fulfill an /// apparently valid request. pub fn is_server_error(&self) -> bool { - let num: u16 = self.clone().into(); + let num: u16 = (*self).into(); (500..600).contains(&num) } diff --git a/src/trace/server_timing/mod.rs b/src/trace/server_timing/mod.rs index f46490fe..fd29a5ba 100644 --- a/src/trace/server_timing/mod.rs +++ b/src/trace/server_timing/mod.rs @@ -262,11 +262,10 @@ mod test { } #[test] - fn bad_request_on_parse_error() -> crate::Result<()> { + fn bad_request_on_parse_error() { let mut headers = Headers::new(); headers.insert(SERVER_TIMING, "server; "); let err = ServerTiming::from_headers(headers).unwrap_err(); assert_eq!(err.status(), 400); - Ok(()) } } diff --git a/src/trace/server_timing/parse.rs b/src/trace/server_timing/parse.rs index b37e81e6..4469336a 100644 --- a/src/trace/server_timing/parse.rs +++ b/src/trace/server_timing/parse.rs @@ -65,7 +65,7 @@ fn parse_entry(s: &str) -> crate::Result { let millis: f64 = value.parse().map_err(|_| { format_err!("Server timing duration params must be a valid double-precision floating-point number.") })?; - dur = Some(Duration::from_secs_f64(millis / 1000.0)); + dur = Some(Duration::from_secs_f64(millis) / 1000); } "desc" => { // Ensure quotes line up, and strip them from the resulting output diff --git a/src/trace/trace_context.rs b/src/trace/trace_context.rs index 74a4ebd7..8857bf02 100644 --- a/src/trace/trace_context.rs +++ b/src/trace/trace_context.rs @@ -269,18 +269,17 @@ mod test { assert_eq!(context.trace_id(), 1); assert_eq!(context.parent_id().unwrap(), 3735928559); assert_eq!(context.flags, 0); - assert_eq!(context.sampled(), false); + assert!(!context.sampled()); Ok(()) } #[test] - fn no_header() -> crate::Result<()> { + fn no_header() { let context = TraceContext::new(); assert_eq!(context.version(), 0); assert_eq!(context.parent_id(), None); assert_eq!(context.flags, 1); - assert_eq!(context.sampled(), true); - Ok(()) + assert!(context.sampled()); } #[test] @@ -288,7 +287,7 @@ mod test { let mut headers = crate::Headers::new(); headers.insert(TRACEPARENT, "00-01-02-00"); let context = TraceContext::from_headers(&mut headers)?.unwrap(); - assert_eq!(context.sampled(), false); + assert!(!context.sampled()); Ok(()) } @@ -297,7 +296,7 @@ mod test { let mut headers = crate::Headers::new(); headers.insert(TRACEPARENT, "00-01-02-01"); let context = TraceContext::from_headers(&mut headers)?.unwrap(); - assert_eq!(context.sampled(), true); + assert!(context.sampled()); Ok(()) } } diff --git a/src/transfer/encoding_proposal.rs b/src/transfer/encoding_proposal.rs index 0961b84b..72f083d5 100644 --- a/src/transfer/encoding_proposal.rs +++ b/src/transfer/encoding_proposal.rs @@ -125,15 +125,14 @@ mod test { use super::*; #[test] - fn smoke() -> crate::Result<()> { + fn smoke() { let _ = EncodingProposal::new(Encoding::Gzip, Some(0.0)).unwrap(); let _ = EncodingProposal::new(Encoding::Gzip, Some(0.5)).unwrap(); let _ = EncodingProposal::new(Encoding::Gzip, Some(1.0)).unwrap(); - Ok(()) } #[test] - fn error_code_500() -> crate::Result<()> { + fn error_code_500() { let err = EncodingProposal::new(Encoding::Gzip, Some(1.1)).unwrap_err(); assert_eq!(err.status(), 500); @@ -142,6 +141,5 @@ mod test { let err = EncodingProposal::new(Encoding::Gzip, Some(-0.0)).unwrap_err(); assert_eq!(err.status(), 500); - Ok(()) } } diff --git a/src/transfer/te.rs b/src/transfer/te.rs index 7be659e6..17be9d3b 100644 --- a/src/transfer/te.rs +++ b/src/transfer/te.rs @@ -37,6 +37,7 @@ use std::slice; /// # /// # Ok(()) } /// ``` +#[allow(clippy::upper_case_acronyms)] pub struct TE { wildcard: bool, entries: Vec, @@ -108,7 +109,7 @@ impl TE { sort_by_weight(&mut self.entries); } - /// Determine the most suitable `Content-Type` encoding. + /// Determine the most suitable `Transfer-Encoding` encoding. /// /// # Errors /// @@ -119,7 +120,7 @@ impl TE { // Try and find the first encoding that matches. for encoding in &self.entries { - if available.contains(&encoding) { + if available.contains(encoding) { return Ok(encoding.into()); } } @@ -131,7 +132,7 @@ impl TE { } } - let mut err = Error::new_adhoc("No suitable ContentEncoding found"); + let mut err = Error::new_adhoc("No suitable Transfer-Encoding found"); err.set_status(StatusCode::NotAcceptable); Err(err) } @@ -150,7 +151,7 @@ impl TE { pub fn value(&self) -> HeaderValue { let mut output = String::new(); for (n, directive) in self.entries.iter().enumerate() { - let directive: HeaderValue = directive.clone().into(); + let directive: HeaderValue = (*directive).into(); match n { 0 => write!(output, "{}", directive).unwrap(), _ => write!(output, ", {}", directive).unwrap(), diff --git a/src/upgrade/connection.rs b/src/upgrade/connection.rs index a6c3c64e..49f01a3b 100644 --- a/src/upgrade/connection.rs +++ b/src/upgrade/connection.rs @@ -12,9 +12,7 @@ pub struct Connection { impl Debug for Connection { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let inner = "Box"; - f.debug_struct("Connection") - .field(&"inner", &inner) - .finish() + f.debug_struct("Connection").field("inner", &inner).finish() } } diff --git a/src/version.rs b/src/version.rs index a5870543..189c31c3 100644 --- a/src/version.rs +++ b/src/version.rs @@ -1,114 +1,130 @@ -use serde::{de::Error, de::Visitor, Deserialize, Deserializer, Serialize, Serializer}; -/// The version of the HTTP protocol in use. -#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] -#[non_exhaustive] -pub enum Version { - /// HTTP/0.9 - Http0_9, - - /// HTTP/1.0 - Http1_0, - - /// HTTP/1.1 - Http1_1, - - /// HTTP/2.0 - Http2_0, - - /// HTTP/3.0 - Http3_0, -} - -impl Serialize for Version { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -struct VersionVisitor; - -impl<'de> Visitor<'de> for VersionVisitor { - type Value = Version; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(formatter, "a HTTP version as &str") - } - - fn visit_str(self, v: &str) -> Result - where - E: Error, - { - match v { - "HTTP/0.9" => Ok(Version::Http0_9), - "HTTP/1.0" => Ok(Version::Http1_0), - "HTTP/1.1" => Ok(Version::Http1_1), - "HTTP/2" => Ok(Version::Http2_0), - "HTTP/3" => Ok(Version::Http3_0), - _ => Err(Error::invalid_value(serde::de::Unexpected::Str(v), &self)), - } - } - - fn visit_string(self, v: String) -> Result - where - E: Error, - { - self.visit_str(&v) - } -} - -impl<'de> Deserialize<'de> for Version { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_str(VersionVisitor) - } -} - -impl std::fmt::Display for Version { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { - Version::Http0_9 => "HTTP/0.9", - Version::Http1_0 => "HTTP/1.0", - Version::Http1_1 => "HTTP/1.1", - Version::Http2_0 => "HTTP/2", - Version::Http3_0 => "HTTP/3", - }) - } -} - -#[cfg(test)] -mod test { - use super::*; - #[test] - fn to_string() { - let output = format!( - "{} {} {} {} {}", - Version::Http0_9, - Version::Http1_0, - Version::Http1_1, - Version::Http2_0, - Version::Http3_0 - ); - assert_eq!("HTTP/0.9 HTTP/1.0 HTTP/1.1 HTTP/2 HTTP/3", output); - } - - #[test] - fn ord() { - use Version::*; - assert!(Http3_0 > Http2_0); - assert!(Http2_0 > Http1_1); - assert!(Http1_1 > Http1_0); - assert!(Http1_0 > Http0_9); - } - - #[test] - fn serde() -> Result<(), serde_json::Error> { - assert_eq!("\"HTTP/3\"", serde_json::to_string(&Version::Http3_0)?); - assert_eq!(Version::Http1_1, serde_json::from_str("\"HTTP/1.1\"")?); - Ok(()) - } -} +use serde::{de::Error, de::Visitor, Deserialize, Deserializer, Serialize, Serializer}; +/// The version of the HTTP protocol in use. +#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +#[non_exhaustive] +pub enum Version { + /// HTTP/0.9 + Http0_9, + + /// HTTP/1.0 + Http1_0, + + /// HTTP/1.1 + Http1_1, + + /// HTTP/2.0 + Http2_0, + + /// HTTP/3.0 + Http3_0, +} + +impl Serialize for Version { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(self.as_ref()) + } +} + +struct VersionVisitor; + +impl<'de> Visitor<'de> for VersionVisitor { + type Value = Version; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "a HTTP version as &str") + } + + fn visit_str(self, v: &str) -> Result + where + E: Error, + { + match v { + "HTTP/0.9" => Ok(Version::Http0_9), + "HTTP/1.0" => Ok(Version::Http1_0), + "HTTP/1.1" => Ok(Version::Http1_1), + "HTTP/2" => Ok(Version::Http2_0), + "HTTP/3" => Ok(Version::Http3_0), + _ => Err(Error::invalid_value(serde::de::Unexpected::Str(v), &self)), + } + } + + fn visit_string(self, v: String) -> Result + where + E: Error, + { + self.visit_str(&v) + } +} + +impl<'de> Deserialize<'de> for Version { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(VersionVisitor) + } +} + +impl AsRef for Version { + fn as_ref(&self) -> &'static str { + match self { + Version::Http0_9 => "HTTP/0.9", + Version::Http1_0 => "HTTP/1.0", + Version::Http1_1 => "HTTP/1.1", + Version::Http2_0 => "HTTP/2", + Version::Http3_0 => "HTTP/3", + } + } +} + +impl std::fmt::Display for Version { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_ref()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn as_ref() { + assert_eq!(Version::Http0_9.as_ref(), "HTTP/0.9"); + assert_eq!(Version::Http1_0.as_ref(), "HTTP/1.0"); + assert_eq!(Version::Http1_1.as_ref(), "HTTP/1.1"); + assert_eq!(Version::Http2_0.as_ref(), "HTTP/2"); + assert_eq!(Version::Http3_0.as_ref(), "HTTP/3"); + } + + #[test] + fn to_string() { + let output = format!( + "{} {} {} {} {}", + Version::Http0_9, + Version::Http1_0, + Version::Http1_1, + Version::Http2_0, + Version::Http3_0 + ); + assert_eq!("HTTP/0.9 HTTP/1.0 HTTP/1.1 HTTP/2 HTTP/3", output); + } + + #[test] + fn ord() { + use Version::*; + assert!(Http3_0 > Http2_0); + assert!(Http2_0 > Http1_1); + assert!(Http1_1 > Http1_0); + assert!(Http1_0 > Http0_9); + } + + #[test] + fn serde() -> Result<(), serde_json::Error> { + assert_eq!("\"HTTP/3\"", serde_json::to_string(&Version::Http3_0)?); + assert_eq!(Version::Http1_1, serde_json::from_str("\"HTTP/1.1\"")?); + Ok(()) + } +}