use crate::ByteStr;
use std::fmt;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Method {
Get,
Head,
Post,
Put,
Delete,
Connect,
Options,
Trace,
Patch,
Query,
Other(ByteStr),
}
impl Method {
#[inline]
pub fn from_bytes(token: &[u8]) -> Option<Method> {
match token.len() {
3 => match token {
b"GET" => Some(Method::Get),
b"PUT" => Some(Method::Put),
_ => None,
},
4 => match token {
b"HEAD" => Some(Method::Head),
b"POST" => Some(Method::Post),
_ => None,
},
5 => match token {
b"PATCH" => Some(Method::Patch),
b"TRACE" => Some(Method::Trace),
b"QUERY" => Some(Method::Query),
_ => None,
},
6 => match token {
b"DELETE" => Some(Method::Delete),
_ => None,
},
7 => match token {
b"CONNECT" => Some(Method::Connect),
b"OPTIONS" => Some(Method::Options),
_ => None,
},
_ => None,
}
}
#[inline]
pub fn as_str(&self) -> &str {
match self {
Method::Get => "GET",
Method::Head => "HEAD",
Method::Post => "POST",
Method::Put => "PUT",
Method::Delete => "DELETE",
Method::Connect => "CONNECT",
Method::Options => "OPTIONS",
Method::Trace => "TRACE",
Method::Patch => "PATCH",
Method::Query => "QUERY",
Method::Other(s) => s.as_str(),
}
}
#[inline]
pub fn is_safe(&self) -> bool {
matches!(
self,
Method::Get | Method::Head | Method::Options | Method::Trace | Method::Query
)
}
#[inline]
pub fn expects_response_body(&self) -> bool {
!matches!(self, Method::Head)
}
}
impl From<&str> for Method {
#[inline]
fn from(token: &str) -> Self {
Method::from_bytes(token.as_bytes()).unwrap_or_else(|| Method::Other(ByteStr::from(token)))
}
}
impl From<String> for Method {
#[inline]
fn from(token: String) -> Self {
Method::from_bytes(token.as_bytes()).unwrap_or_else(|| Method::Other(ByteStr::from(token)))
}
}
impl PartialEq<str> for Method {
#[inline]
fn eq(&self, other: &str) -> bool {
self.as_str() == other
}
}
impl PartialEq<&str> for Method {
#[inline]
fn eq(&self, other: &&str) -> bool {
self.as_str() == *other
}
}
impl fmt::Display for Method {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Version {
Http10,
Http11,
}
impl Version {
#[inline]
pub fn from_httparse(minor: u8) -> Option<Version> {
match minor {
0 => Some(Version::Http10),
1 => Some(Version::Http11),
_ => None,
}
}
#[inline]
pub fn as_bytes(&self) -> &'static [u8] {
match self {
Version::Http10 => b"HTTP/1.0",
Version::Http11 => b"HTTP/1.1",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn well_known_methods_parse() {
assert_eq!(Method::from_bytes(b"GET"), Some(Method::Get));
assert_eq!(Method::from_bytes(b"HEAD"), Some(Method::Head));
assert_eq!(Method::from_bytes(b"POST"), Some(Method::Post));
assert_eq!(Method::from_bytes(b"PUT"), Some(Method::Put));
assert_eq!(Method::from_bytes(b"DELETE"), Some(Method::Delete));
assert_eq!(Method::from_bytes(b"CONNECT"), Some(Method::Connect));
assert_eq!(Method::from_bytes(b"OPTIONS"), Some(Method::Options));
assert_eq!(Method::from_bytes(b"TRACE"), Some(Method::Trace));
assert_eq!(Method::from_bytes(b"PATCH"), Some(Method::Patch));
assert_eq!(Method::from_bytes(b"QUERY"), Some(Method::Query));
}
#[test]
fn methods_are_case_sensitive() {
assert_eq!(Method::from_bytes(b"get"), None);
assert_eq!(Method::from_bytes(b"Get"), None);
}
#[test]
fn unknown_method_is_not_well_known() {
assert_eq!(Method::from_bytes(b"PROPFIND"), None);
assert_eq!(Method::from_bytes(b""), None);
assert_eq!(Method::from_bytes(b"GETX"), None);
assert_eq!(Method::from_bytes(b"GE"), None);
}
#[test]
fn as_str_round_trips() {
for m in [
Method::Get,
Method::Head,
Method::Post,
Method::Put,
Method::Delete,
Method::Connect,
Method::Options,
Method::Trace,
Method::Patch,
Method::Query,
] {
assert_eq!(Method::from_bytes(m.as_str().as_bytes()), Some(m.clone()));
}
assert_eq!(
Method::Other(ByteStr::from_static("PROPFIND")).as_str(),
"PROPFIND"
);
}
#[test]
fn head_expects_no_response_body() {
assert!(!Method::Head.expects_response_body());
assert!(Method::Get.expects_response_body());
}
#[test]
fn safe_methods_classified() {
assert!(Method::Get.is_safe());
assert!(Method::Head.is_safe());
assert!(Method::Options.is_safe());
assert!(Method::Trace.is_safe());
assert!(Method::Query.is_safe());
assert!(!Method::Post.is_safe());
assert!(!Method::Delete.is_safe());
assert!(!Method::Other(ByteStr::from_static("PROPFIND")).is_safe());
}
#[test]
fn versions_map_from_httparse() {
assert_eq!(Version::from_httparse(0), Some(Version::Http10));
assert_eq!(Version::from_httparse(1), Some(Version::Http11));
assert_eq!(Version::from_httparse(2), None);
assert_eq!(Version::Http11.as_bytes(), b"HTTP/1.1");
assert_eq!(Version::Http10.as_bytes(), b"HTTP/1.0");
}
#[test]
fn from_str_maps_well_known_and_preserves_unknown_case() {
assert_eq!(Method::from("GET"), Method::Get);
assert_eq!(Method::from("QUERY"), Method::Query);
assert_eq!(
Method::from("get"),
Method::Other(ByteStr::from_static("get"))
);
assert_eq!(
Method::from("PURGE".to_string()),
Method::Other(ByteStr::from_static("PURGE"))
);
}
#[test]
fn compares_against_str_and_displays_as_its_token() {
assert!(Method::Delete == "DELETE");
assert!(Method::Delete != "GET");
let unknown = Method::from("PURGE");
assert!(unknown == "PURGE");
assert_eq!(format!("{}", Method::Patch), "PATCH");
assert_eq!(format!("{unknown}"), "PURGE");
}
}