use core::{fmt, str::FromStr};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "UPPERCASE"))]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(feature = "proptest", derive(proptest_derive::Arbitrary))]
pub enum HttpMethod {
Get,
Head,
Post,
Put,
Delete,
Connect,
Options,
Trace,
Patch,
}
impl HttpMethod {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Get => "GET",
Self::Head => "HEAD",
Self::Post => "POST",
Self::Put => "PUT",
Self::Delete => "DELETE",
Self::Connect => "CONNECT",
Self::Options => "OPTIONS",
Self::Trace => "TRACE",
Self::Patch => "PATCH",
}
}
#[must_use]
pub const fn is_safe(&self) -> bool {
matches!(self, Self::Get | Self::Head | Self::Options | Self::Trace)
}
#[must_use]
pub const fn is_idempotent(&self) -> bool {
matches!(
self,
Self::Get | Self::Head | Self::Put | Self::Delete | Self::Options | Self::Trace
)
}
}
impl fmt::Display for HttpMethod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseHttpMethodError;
impl fmt::Display for ParseHttpMethodError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("unknown HTTP method")
}
}
#[cfg(feature = "std")]
impl std::error::Error for ParseHttpMethodError {}
impl FromStr for HttpMethod {
type Err = ParseHttpMethodError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim() {
"GET" => Ok(Self::Get),
"HEAD" => Ok(Self::Head),
"POST" => Ok(Self::Post),
"PUT" => Ok(Self::Put),
"DELETE" => Ok(Self::Delete),
"CONNECT" => Ok(Self::Connect),
"OPTIONS" => Ok(Self::Options),
"TRACE" => Ok(Self::Trace),
"PATCH" => Ok(Self::Patch),
_ => Err(ParseHttpMethodError),
}
}
}
#[cfg(feature = "http")]
mod http_interop {
use super::HttpMethod;
impl From<HttpMethod> for http::Method {
fn from(m: HttpMethod) -> Self {
match m {
HttpMethod::Get => Self::GET,
HttpMethod::Head => Self::HEAD,
HttpMethod::Post => Self::POST,
HttpMethod::Put => Self::PUT,
HttpMethod::Delete => Self::DELETE,
HttpMethod::Connect => Self::CONNECT,
HttpMethod::Options => Self::OPTIONS,
HttpMethod::Trace => Self::TRACE,
HttpMethod::Patch => Self::PATCH,
}
}
}
impl TryFrom<http::Method> for HttpMethod {
type Error = super::ParseHttpMethodError;
fn try_from(m: http::Method) -> Result<Self, Self::Error> {
m.as_str().parse()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn as_str_round_trips() {
let methods = [
HttpMethod::Get,
HttpMethod::Head,
HttpMethod::Post,
HttpMethod::Put,
HttpMethod::Delete,
HttpMethod::Connect,
HttpMethod::Options,
HttpMethod::Trace,
HttpMethod::Patch,
];
for m in methods {
let s = m.as_str();
let parsed: HttpMethod = s.parse().expect("should parse");
assert_eq!(parsed, m, "round-trip failed for {s}");
}
}
#[test]
fn display_equals_as_str() {
assert_eq!(HttpMethod::Get.to_string(), HttpMethod::Get.as_str());
assert_eq!(HttpMethod::Patch.to_string(), "PATCH");
}
#[test]
fn is_safe() {
assert!(HttpMethod::Get.is_safe());
assert!(HttpMethod::Head.is_safe());
assert!(HttpMethod::Options.is_safe());
assert!(HttpMethod::Trace.is_safe());
assert!(!HttpMethod::Post.is_safe());
assert!(!HttpMethod::Put.is_safe());
assert!(!HttpMethod::Delete.is_safe());
assert!(!HttpMethod::Connect.is_safe());
assert!(!HttpMethod::Patch.is_safe());
}
#[test]
fn is_idempotent() {
assert!(HttpMethod::Get.is_idempotent());
assert!(HttpMethod::Head.is_idempotent());
assert!(HttpMethod::Put.is_idempotent());
assert!(HttpMethod::Delete.is_idempotent());
assert!(HttpMethod::Options.is_idempotent());
assert!(HttpMethod::Trace.is_idempotent());
assert!(!HttpMethod::Post.is_idempotent());
assert!(!HttpMethod::Connect.is_idempotent());
assert!(!HttpMethod::Patch.is_idempotent());
}
#[test]
fn parse_unknown_errors() {
assert!("BREW".parse::<HttpMethod>().is_err());
assert!("get".parse::<HttpMethod>().is_err());
}
#[cfg(feature = "serde")]
#[test]
fn serde_round_trip() {
let m = HttpMethod::Patch;
let json = serde_json::to_string(&m).unwrap();
assert_eq!(json, r#""PATCH""#);
let back: HttpMethod = serde_json::from_str(&json).unwrap();
assert_eq!(back, m);
}
#[cfg(feature = "http")]
#[test]
fn http_crate_round_trip() {
let pairs = [
(HttpMethod::Get, http::Method::GET),
(HttpMethod::Head, http::Method::HEAD),
(HttpMethod::Post, http::Method::POST),
(HttpMethod::Put, http::Method::PUT),
(HttpMethod::Delete, http::Method::DELETE),
(HttpMethod::Connect, http::Method::CONNECT),
(HttpMethod::Options, http::Method::OPTIONS),
(HttpMethod::Trace, http::Method::TRACE),
(HttpMethod::Patch, http::Method::PATCH),
];
for (our, theirs) in pairs {
let converted: http::Method = our.into();
assert_eq!(converted, theirs);
let back: HttpMethod = converted.try_into().unwrap();
assert_eq!(back, our);
}
}
#[test]
fn parse_error_display() {
let err = "BREW".parse::<HttpMethod>().unwrap_err();
assert_eq!(err.to_string(), "unknown HTTP method");
}
}