mirror of https://github.com/rwf2/Rocket.git
Add 'Method' variants for all registered methods.
This commit allow routes to be declared for methods outside of the standard HTTP method set. Specifically, it enables declaring routes for any method in the IANA Method Registry: ```rust #[route(LINK, uri = "/<foo>")] fn link() { ... } #[route("VERSION-CONTROL", uri = "/<foo>")] fn version_control() { ... } ``` The `Method` type has gained variants for each registered method. Breaking changes: - `Method::from_str()` no longer parses mixed-case method names. - `Method` is marked as non-exhaustive. - `Method::supports_payload()` removed in favor of `Method::allows_request_body()`. Resolves #232.
This commit is contained in:
parent
7c50a58c99
commit
b34085392d
|
@ -62,7 +62,7 @@ fn generate_matching_requests<'c>(client: &'c Client, routes: &[Route]) -> Vec<L
|
|||
let uri = format!("/{}?{}", path, query);
|
||||
let mut req = client.req(route.method, uri);
|
||||
if let Some(ref format) = route.format {
|
||||
if route.method.supports_payload() {
|
||||
if let Some(true) = route.method.allows_request_body() {
|
||||
req.add_header(ContentType::from(format.clone()));
|
||||
} else {
|
||||
req.add_header(Accept::from(format.clone()));
|
||||
|
|
|
@ -129,13 +129,20 @@ impl Route {
|
|||
// Emit a warning if a `data` param was supplied for non-payload methods.
|
||||
if let Some(ref data) = attr.data {
|
||||
let lint = Lint::DubiousPayload;
|
||||
if !attr.method.0.supports_payload() && lint.enabled(handler.span()) {
|
||||
// FIXME(diag: warning)
|
||||
match attr.method.0.allows_request_body() {
|
||||
None if lint.enabled(handler.span()) => {
|
||||
data.full_span.warning("`data` used with non-payload-supporting method")
|
||||
.note(format!("'{}' does not typically support payloads", attr.method.0))
|
||||
.note(lint.how_to_suppress())
|
||||
.emit_as_item_tokens();
|
||||
}
|
||||
Some(false) => {
|
||||
diags.push(data.full_span
|
||||
.error("`data` cannot be used on this route")
|
||||
.span_note(attr.method.span, "method does not support request payloads"))
|
||||
}
|
||||
_ => { /* okay */ },
|
||||
}
|
||||
}
|
||||
|
||||
// Check the validity of function arguments.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use quote::ToTokens;
|
||||
use devise::{FromMeta, MetaItem, Result, ext::{Split2, PathExt, SpanDiagnosticExt}};
|
||||
use devise::{FromMeta, MetaItem, Result, ext::{Split2, SpanDiagnosticExt}};
|
||||
use proc_macro2::{TokenStream, Span};
|
||||
|
||||
use crate::{http, attribute::suppress::Lint};
|
||||
|
@ -97,47 +97,34 @@ impl ToTokens for MediaType {
|
|||
}
|
||||
}
|
||||
|
||||
const VALID_METHODS_STR: &str = "`GET`, `PUT`, `POST`, `DELETE`, `HEAD`, \
|
||||
`PATCH`, `OPTIONS`";
|
||||
|
||||
const VALID_METHODS: &[http::Method] = &[
|
||||
http::Method::Get, http::Method::Put, http::Method::Post,
|
||||
http::Method::Delete, http::Method::Head, http::Method::Patch,
|
||||
http::Method::Options,
|
||||
];
|
||||
|
||||
impl FromMeta for Method {
|
||||
fn from_meta(meta: &MetaItem) -> Result<Self> {
|
||||
let span = meta.value_span();
|
||||
let help_text = format!("method must be one of: {VALID_METHODS_STR}");
|
||||
let help = format!("known methods: {}", http::Method::ALL.join(", "));
|
||||
|
||||
if let MetaItem::Path(path) = meta {
|
||||
if let Some(ident) = path.last_ident() {
|
||||
let method = ident.to_string().parse()
|
||||
.map_err(|_| span.error("invalid HTTP method").help(&*help_text))?;
|
||||
let string = meta.path().ok()
|
||||
.and_then(|p| p.get_ident().cloned())
|
||||
.map(|ident| (ident.span(), ident.to_string()))
|
||||
.or_else(|| match meta.lit() {
|
||||
Ok(syn::Lit::Str(s)) => Some((s.span(), s.value())),
|
||||
_ => None
|
||||
});
|
||||
|
||||
if !VALID_METHODS.contains(&method) {
|
||||
return Err(span.error("invalid HTTP method for route handlers")
|
||||
.help(&*help_text));
|
||||
if let Some((span, string)) = string {
|
||||
string.to_ascii_uppercase()
|
||||
.parse()
|
||||
.map(Method)
|
||||
.map_err(|_| span.error("invalid or unknown HTTP method").help(help))
|
||||
} else {
|
||||
let err = format!("expected method ident or string, found {}", meta.description());
|
||||
Err(span.error(err).help(help))
|
||||
}
|
||||
|
||||
return Ok(Method(method));
|
||||
}
|
||||
}
|
||||
|
||||
Err(span.error(format!("expected identifier, found {}", meta.description()))
|
||||
.help(&*help_text))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Method {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let mut chars = self.0.as_str().chars();
|
||||
let variant_str = chars.next()
|
||||
.map(|c| c.to_ascii_uppercase().to_string() + &chars.as_str().to_lowercase())
|
||||
.unwrap_or_default();
|
||||
|
||||
let variant = syn::Ident::new(&variant_str, Span::call_site());
|
||||
let variant = syn::Ident::new(self.0.variant_str(), Span::call_site());
|
||||
tokens.extend(quote!(::rocket::http::Method::#variant));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,138 +1,361 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use self::Method::*;
|
||||
self::define_methods! {
|
||||
// enum variant method name body safe idempotent [RFC,section]
|
||||
Get "GET" maybe yes yes [9110,9.3.1]
|
||||
Head "HEAD" maybe yes yes [9110,9.3.2]
|
||||
Post "POST" yes no no [9110,9.3.3]
|
||||
Put "PUT" yes no yes [9110,9.3.4]
|
||||
Delete "DELETE" maybe no yes [9110,9.3.5]
|
||||
Connect "CONNECT" maybe no no [9110,9.3.6]
|
||||
Options "OPTIONS" maybe yes yes [9110,9.3.7]
|
||||
Trace "TRACE" no yes yes [9110,9.3.8]
|
||||
Patch "PATCH" yes no no [5789,2]
|
||||
|
||||
// TODO: Support non-standard methods, here and in codegen?
|
||||
|
||||
/// Representation of HTTP methods.
|
||||
///
|
||||
/// # (De)serialization
|
||||
///
|
||||
/// `Method` is both `Serialize` and `Deserialize`, represented as an
|
||||
/// [uncased](crate::uncased) string. For example, [`Method::Get`] serializes to
|
||||
/// `"GET"` and deserializes from any casing of `"GET"` including `"get"`,
|
||||
/// `"GeT"`, and `"GET"`.
|
||||
///
|
||||
/// ```rust
|
||||
/// # #[cfg(feature = "serde")] mod serde {
|
||||
/// # use serde_ as serde;
|
||||
/// use serde::{Serialize, Deserialize};
|
||||
/// use rocket::http::Method;
|
||||
///
|
||||
/// #[derive(Deserialize, Serialize)]
|
||||
/// # #[serde(crate = "serde_")]
|
||||
/// struct Foo {
|
||||
/// method: Method,
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
#[repr(u8)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
pub enum Method {
|
||||
/// The `GET` variant.
|
||||
Get,
|
||||
/// The `PUT` variant.
|
||||
Put,
|
||||
/// The `POST` variant.
|
||||
Post,
|
||||
/// The `DELETE` variant.
|
||||
Delete,
|
||||
/// The `OPTIONS` variant.
|
||||
Options,
|
||||
/// The `HEAD` variant.
|
||||
Head,
|
||||
/// The `TRACE` variant.
|
||||
Trace,
|
||||
/// The `CONNECT` variant.
|
||||
Connect,
|
||||
/// The `PATCH` variant.
|
||||
Patch
|
||||
Acl "ACL" yes no yes [3744,8.1]
|
||||
BaselineControl "BASELINE-CONTROL" yes no yes [3253,12.6]
|
||||
Bind "BIND" yes no yes [5842,4]
|
||||
CheckIn "CHECKIN" yes no yes [3253,4.4]
|
||||
CheckOut "CHECKOUT" maybe no yes [3253,4.3]
|
||||
Copy "COPY" maybe no yes [4918,9.8]
|
||||
Label "LABEL" yes no yes [3253,8.2]
|
||||
Link "LINK" maybe no yes [2068,19.6.1.2]
|
||||
Lock "LOCK" yes no no [4918,9.10]
|
||||
Merge "MERGE" yes no yes [3253,11.2]
|
||||
MkActivity "MKACTIVITY" yes no yes [3253,13.5]
|
||||
MkCalendar "MKCALENDAR" yes no yes [4791,5.3.1][8144,2.3]
|
||||
MkCol "MKCOL" yes no yes [4918,9.3][5689,3][8144,2.3]
|
||||
MkRedirectRef "MKREDIRECTREF" yes no yes [4437,6]
|
||||
MkWorkspace "MKWORKSPACE" yes no yes [3253,6.3]
|
||||
Move "MOVE" maybe no yes [4918,9.9]
|
||||
OrderPatch "ORDERPATCH" yes no yes [3648,7]
|
||||
PropFind "PROPFIND" yes yes yes [4918,9.1][8144,2.1]
|
||||
PropPatch "PROPPATCH" yes no yes [4918,9.2][8144,2.2]
|
||||
Rebind "REBIND" yes no yes [5842,6]
|
||||
Report "REPORT" yes yes yes [3253,3.6][8144,2.1]
|
||||
Search "SEARCH" yes yes yes [5323,2]
|
||||
Unbind "UNBIND" yes no yes [5842,5]
|
||||
Uncheckout "UNCHECKOUT" maybe no yes [3253,4.5]
|
||||
Unlink "UNLINK" maybe no yes [2068,19.6.1.3]
|
||||
Unlock "UNLOCK" maybe no yes [4918,9.11]
|
||||
Update "UPDATE" yes no yes [3253,7.1]
|
||||
UpdateRedirectRef "UPDATEREDIRECTREF" yes no yes [4437,7]
|
||||
VersionControl "VERSION-CONTROL" yes no yes [3253,3.5]
|
||||
}
|
||||
|
||||
impl Method {
|
||||
/// Returns `true` if an HTTP request with the method represented by `self`
|
||||
/// always supports a payload.
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! define_methods {
|
||||
($($V:ident $name:tt $body:ident $safe:ident $idem:ident $([$n:expr,$s:expr])+)*) => {
|
||||
/// An HTTP method.
|
||||
///
|
||||
/// The following methods always support payloads:
|
||||
/// Each variant corresponds to a method in the [HTTP Method Registry].
|
||||
/// The string form of the method can be obtained via
|
||||
/// [`Method::as_str()`] and parsed via the `FromStr` or
|
||||
/// `TryFrom<&[u8]>` implementations. The parse implementations parse
|
||||
/// both the case-sensitive string form as well as a lowercase version
|
||||
/// of the string, but _not_ mixed-case versions.
|
||||
///
|
||||
/// * `PUT`, `POST`, `DELETE`, `PATCH`
|
||||
/// [HTTP Method Registry]: https://www.iana.org/assignments/http-methods/http-methods.xhtml
|
||||
///
|
||||
/// The following methods _do not_ always support payloads:
|
||||
/// # (De)Serialization
|
||||
///
|
||||
/// * `GET`, `HEAD`, `CONNECT`, `TRACE`, `OPTIONS`
|
||||
/// `Method` is both `Serialize` and `Deserialize`.
|
||||
///
|
||||
/// - `Method` _serializes_ as the specification-defined string form
|
||||
/// of the method, equivalent to the value returned from
|
||||
/// [`Method::as_str()`].
|
||||
/// - `Method` _deserializes_ from method's string form _or_ from a
|
||||
/// lowercased string, equivalent to the `FromStr` implementation.
|
||||
///
|
||||
/// For example, [`Method::Get`] serializes to `"GET"` and deserializes
|
||||
/// from either `"GET"` or `"get"` but not `"GeT"`.
|
||||
///
|
||||
/// ```rust
|
||||
/// # #[cfg(feature = "serde")] mod serde {
|
||||
/// # use serde_ as serde;
|
||||
/// use serde::{Serialize, Deserialize};
|
||||
/// use rocket::http::Method;
|
||||
///
|
||||
/// #[derive(Deserialize, Serialize)]
|
||||
/// # #[serde(crate = "serde_")]
|
||||
/// struct Foo {
|
||||
/// method: Method,
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Method {$(
|
||||
#[doc = concat!("The `", $name, "` method.")]
|
||||
#[doc = concat!("Defined in" $(,
|
||||
" [RFC", stringify!($n), " §", stringify!($s), "]",
|
||||
"(https://www.rfc-editor.org/rfc/rfc", stringify!($n), ".html",
|
||||
"#section-", stringify!($s), ")",
|
||||
)","+ ".")]
|
||||
///
|
||||
#[doc = concat!("* safe: `", stringify!($safe), "`")]
|
||||
#[doc = concat!("* idempotent: `", stringify!($idem), "`")]
|
||||
#[doc = concat!("* request body: `", stringify!($body), "`")]
|
||||
$V
|
||||
),*}
|
||||
|
||||
macro_rules! lowercase {
|
||||
($str:literal) => {{
|
||||
const BYTES: [u8; $str.len()] = {
|
||||
let mut i = 0;
|
||||
let _: &str = $str;
|
||||
let mut result = [0; $str.len()];
|
||||
while i < $str.len() {
|
||||
result[i] = $str.as_bytes()[i].to_ascii_lowercase();
|
||||
i += 1;
|
||||
}
|
||||
|
||||
result
|
||||
};
|
||||
|
||||
unsafe { std::str::from_utf8_unchecked(&BYTES) }
|
||||
}};
|
||||
}
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
impl Method {
|
||||
/// A slice containing every defined method string.
|
||||
#[doc(hidden)]
|
||||
pub const ALL: &'static [&'static str] = &[$($name),*];
|
||||
|
||||
/// Whether the method is considered "safe".
|
||||
///
|
||||
/// From [RFC9110 §9.2.1](https://www.rfc-editor.org/rfc/rfc9110#section-9.2.1):
|
||||
///
|
||||
/// > Request methods are considered "safe" if their defined
|
||||
/// semantics are essentially read-only; i.e., the client does not
|
||||
/// request, and does not expect, any state change on the origin server
|
||||
/// as a result of applying a safe method to a target resource.
|
||||
/// Likewise, reasonable use of a safe method is not expected to cause
|
||||
/// any harm, loss of property, or unusual burden on the origin server.
|
||||
/// Of the request methods defined by this specification, the GET,
|
||||
/// HEAD, OPTIONS, and TRACE methods are defined to be safe.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate rocket;
|
||||
/// use rocket::http::Method;
|
||||
///
|
||||
/// assert_eq!(Method::Get.supports_payload(), false);
|
||||
/// assert_eq!(Method::Post.supports_payload(), true);
|
||||
/// assert!(Method::Get.is_safe());
|
||||
/// assert!(Method::Head.is_safe());
|
||||
///
|
||||
/// assert!(!Method::Put.is_safe());
|
||||
/// assert!(!Method::Post.is_safe());
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn supports_payload(self) -> bool {
|
||||
pub const fn is_safe(&self) -> bool {
|
||||
const yes: bool = true;
|
||||
const no: bool = false;
|
||||
|
||||
match self {
|
||||
Put | Post | Delete | Patch => true,
|
||||
Get | Head | Connect | Trace | Options => false,
|
||||
$(Self::$V => $safe),*
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the string representation of `self`.
|
||||
/// Whether the method is considered "idempotent".
|
||||
///
|
||||
/// From [RFC9110 §9.2.2](https://www.rfc-editor.org/rfc/rfc9110#section-9.2.2):
|
||||
///
|
||||
/// > A request method is considered "idempotent" if the intended
|
||||
/// effect on the server of multiple identical requests with that method
|
||||
/// is the same as the effect for a single such request. Of the request
|
||||
/// methods defined by this specification, PUT, DELETE, and safe request
|
||||
/// methods are idempotent.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::http::Method;
|
||||
///
|
||||
/// assert!(Method::Get.is_idempotent());
|
||||
/// assert!(Method::Head.is_idempotent());
|
||||
/// assert!(Method::Put.is_idempotent());
|
||||
///
|
||||
/// assert!(!Method::Post.is_idempotent());
|
||||
/// assert!(!Method::Patch.is_idempotent());
|
||||
/// ```
|
||||
pub const fn is_idempotent(&self) -> bool {
|
||||
const yes: bool = true;
|
||||
const no: bool = false;
|
||||
|
||||
match self {
|
||||
$(Self::$V => $idem),*
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether requests with this method are allowed to have a body.
|
||||
///
|
||||
/// Returns:
|
||||
/// * `Some(true)` if a request body is _always_ allowed.
|
||||
/// * `Some(false)` if a request body is **never** allowed.
|
||||
/// * `None` if a request body is discouraged or has no defined semantics.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::http::Method;
|
||||
///
|
||||
/// assert_eq!(Method::Post.allows_request_body(), Some(true));
|
||||
/// assert_eq!(Method::Put.allows_request_body(), Some(true));
|
||||
///
|
||||
/// assert_eq!(Method::Trace.allows_request_body(), Some(false));
|
||||
///
|
||||
/// assert_eq!(Method::Get.allows_request_body(), None);
|
||||
/// assert_eq!(Method::Head.allows_request_body(), None);
|
||||
/// ```
|
||||
pub const fn allows_request_body(self) -> Option<bool> {
|
||||
const yes: Option<bool> = Some(true);
|
||||
const no: Option<bool> = Some(false);
|
||||
const maybe: Option<bool> = None;
|
||||
|
||||
match self {
|
||||
$(Self::$V => $body),*
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the method's string representation.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate rocket;
|
||||
/// use rocket::http::Method;
|
||||
///
|
||||
/// assert_eq!(Method::Get.as_str(), "GET");
|
||||
/// assert_eq!(Method::Put.as_str(), "PUT");
|
||||
/// assert_eq!(Method::BaselineControl.as_str(), "BASELINE-CONTROL");
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn as_str(self) -> &'static str {
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Get => "GET",
|
||||
Put => "PUT",
|
||||
Post => "POST",
|
||||
Delete => "DELETE",
|
||||
Options => "OPTIONS",
|
||||
Head => "HEAD",
|
||||
Trace => "TRACE",
|
||||
Connect => "CONNECT",
|
||||
Patch => "PATCH",
|
||||
$(Self::$V => $name),*
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a static reference to the method.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::http::Method;
|
||||
///
|
||||
/// assert_eq!(Method::Get.as_ref(), &Method::Get);
|
||||
/// ```
|
||||
pub const fn as_ref(self) -> &'static Method {
|
||||
match self {
|
||||
$(Self::$V => &Self::$V),*
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub const fn variant_str(self) -> &'static str {
|
||||
match self {
|
||||
$(Self::$V => stringify!($V)),*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[allow(non_upper_case_globals)]
|
||||
fn test_properties_and_parsing() {
|
||||
const yes: bool = true;
|
||||
const no: bool = false;
|
||||
|
||||
$(
|
||||
assert_eq!(Method::$V.is_idempotent(), $idem);
|
||||
assert_eq!(Method::$V.is_safe(), $safe);
|
||||
assert_eq!(Method::from_str($name).unwrap(), Method::$V);
|
||||
assert_eq!(Method::from_str(lowercase!($name)).unwrap(), Method::$V);
|
||||
assert_eq!(Method::$V.as_ref(), Method::$V);
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for Method {
|
||||
type Error = ParseMethodError;
|
||||
|
||||
#[inline]
|
||||
#[allow(non_upper_case_globals)]
|
||||
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
|
||||
mod upper { $(pub const $V: &[u8] = $name.as_bytes();)* }
|
||||
mod lower { $(pub const $V: &[u8] = lowercase!($name).as_bytes();)* }
|
||||
|
||||
match value {
|
||||
$(upper::$V | lower::$V => Ok(Self::$V),)*
|
||||
_ => Err(ParseMethodError)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl Method {
|
||||
/// Deprecated. Returns `self.allows_request_body() == Some(true)`.
|
||||
///
|
||||
/// Use [`Method::allows_request_body()`] instead.
|
||||
#[deprecated(since = "0.6", note = "use Self::allows_request_body()")]
|
||||
pub const fn supports_payload(self) -> bool {
|
||||
match self.allows_request_body() {
|
||||
Some(v) => v,
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use define_methods as define_methods;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct ParseMethodError;
|
||||
|
||||
impl std::error::Error for ParseMethodError { }
|
||||
|
||||
impl fmt::Display for ParseMethodError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("invalid HTTP method")
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Method {
|
||||
type Err = ();
|
||||
type Err = ParseMethodError;
|
||||
|
||||
// According to the RFC, method names are case-sensitive. But some old
|
||||
// clients don't follow this, so we just do a case-insensitive match here.
|
||||
fn from_str(s: &str) -> Result<Method, ()> {
|
||||
match s {
|
||||
x if uncased::eq(x, Get.as_str()) => Ok(Get),
|
||||
x if uncased::eq(x, Put.as_str()) => Ok(Put),
|
||||
x if uncased::eq(x, Post.as_str()) => Ok(Post),
|
||||
x if uncased::eq(x, Delete.as_str()) => Ok(Delete),
|
||||
x if uncased::eq(x, Options.as_str()) => Ok(Options),
|
||||
x if uncased::eq(x, Head.as_str()) => Ok(Head),
|
||||
x if uncased::eq(x, Trace.as_str()) => Ok(Trace),
|
||||
x if uncased::eq(x, Connect.as_str()) => Ok(Connect),
|
||||
x if uncased::eq(x, Patch.as_str()) => Ok(Patch),
|
||||
_ => Err(()),
|
||||
#[inline(always)]
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Self::try_from(s.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for Method {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Method {
|
||||
#[inline(always)]
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.as_str().fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<&Method> for Method {
|
||||
fn eq(&self, other: &&Method) -> bool {
|
||||
self == *other
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<Method> for &Method {
|
||||
fn eq(&self, other: &Method) -> bool {
|
||||
*self == other
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
mod serde {
|
||||
use super::*;
|
||||
|
|
|
@ -59,11 +59,8 @@ impl Rocket<Orbit> {
|
|||
) -> RequestToken {
|
||||
// Check if this is a form and if the form contains the special _method
|
||||
// field which we use to reinterpret the request's method.
|
||||
let (min_len, max_len) = ("_method=get".len(), "_method=delete".len());
|
||||
let peek_buffer = data.peek(max_len).await;
|
||||
let is_form = req.content_type().map_or(false, |ct| ct.is_form());
|
||||
|
||||
if is_form && req.method() == Method::Post && peek_buffer.len() >= min_len {
|
||||
if req.method() == Method::Post && req.content_type().map_or(false, |v| v.is_form()) {
|
||||
let peek_buffer = data.peek(32).await;
|
||||
let method = std::str::from_utf8(peek_buffer).ok()
|
||||
.and_then(|raw_form| Form::values(raw_form).next())
|
||||
.filter(|field| field.name == "_method")
|
||||
|
|
|
@ -2,36 +2,25 @@ use crate::http::Method;
|
|||
|
||||
pub struct AtomicMethod(ref_swap::RefSwap<'static, Method>);
|
||||
|
||||
#[inline(always)]
|
||||
const fn makeref(method: Method) -> &'static Method {
|
||||
match method {
|
||||
Method::Get => &Method::Get,
|
||||
Method::Put => &Method::Put,
|
||||
Method::Post => &Method::Post,
|
||||
Method::Delete => &Method::Delete,
|
||||
Method::Options => &Method::Options,
|
||||
Method::Head => &Method::Head,
|
||||
Method::Trace => &Method::Trace,
|
||||
Method::Connect => &Method::Connect,
|
||||
Method::Patch => &Method::Patch,
|
||||
}
|
||||
}
|
||||
|
||||
impl AtomicMethod {
|
||||
#[inline]
|
||||
pub fn new(value: Method) -> Self {
|
||||
Self(ref_swap::RefSwap::new(makeref(value)))
|
||||
Self(ref_swap::RefSwap::new(value.as_ref()))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn load(&self) -> Method {
|
||||
*self.0.load(std::sync::atomic::Ordering::Acquire)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set(&mut self, new: Method) {
|
||||
*self = Self::new(new);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn store(&self, new: Method) {
|
||||
self.0.store(makeref(new), std::sync::atomic::Ordering::Release)
|
||||
self.0.store(new.as_ref(), std::sync::atomic::Ordering::Release)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -656,10 +656,20 @@ impl<'r> Request<'r> {
|
|||
|
||||
/// Returns the media type "format" of the request.
|
||||
///
|
||||
/// The "format" of a request is either the Content-Type, if the request
|
||||
/// methods indicates support for a payload, or the preferred media type in
|
||||
/// the Accept header otherwise. If the method indicates no payload and no
|
||||
/// Accept header is specified, a media type of `Any` is returned.
|
||||
/// The returned `MediaType` is derived from either the `Content-Type` or
|
||||
/// the `Accept` header of the request, based on whether the request's
|
||||
/// method allows a body (see [`Method::allows_request_body()`]). The table
|
||||
/// below summarized this:
|
||||
///
|
||||
/// | Method Allows Body | Returned Format |
|
||||
/// |--------------------|---------------------------------|
|
||||
/// | Always | `Option<ContentType>` |
|
||||
/// | Maybe or Never | `Some(Preferred Accept or Any)` |
|
||||
///
|
||||
/// In short, if the request's method indicates support for a payload, the
|
||||
/// request's `Content-Type` header value, if any, is returned. Otherwise
|
||||
/// the [preferred](Accept::preferred()) `Accept` header value is returned,
|
||||
/// or if none is present, [`Accept::Any`].
|
||||
///
|
||||
/// The media type returned from this method is used to match against the
|
||||
/// `format` route attribute.
|
||||
|
@ -691,7 +701,7 @@ impl<'r> Request<'r> {
|
|||
/// ```
|
||||
pub fn format(&self) -> Option<&MediaType> {
|
||||
static ANY: MediaType = MediaType::Any;
|
||||
if self.method().supports_payload() {
|
||||
if self.method().allows_request_body().unwrap_or(false) {
|
||||
self.content_type().map(|ct| ct.media_type())
|
||||
} else {
|
||||
// TODO: Should we be using `accept_first` or `preferred`? Or
|
||||
|
|
|
@ -191,20 +191,19 @@ impl Collide for MediaType {
|
|||
}
|
||||
|
||||
fn formats_collide(route: &Route, other: &Route) -> bool {
|
||||
// If the routes' method doesn't support a payload, then format matching
|
||||
// considers the `Accept` header. The client can always provide a media type
|
||||
// that will cause a collision through non-specificity, i.e, `*/*`.
|
||||
if !route.method.supports_payload() && !other.method.supports_payload() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Payload supporting methods match against `Content-Type`. We only
|
||||
// consider requests as having a `Content-Type` if they're fully
|
||||
// specified. A route without a `format` accepts all `Content-Type`s. A
|
||||
// request without a format only matches routes without a format.
|
||||
match (route.format.as_ref(), other.format.as_ref()) {
|
||||
match (route.method.allows_request_body(), other.method.allows_request_body()) {
|
||||
// Payload supporting methods match against `Content-Type` which must be
|
||||
// fully specified, so the request cannot contain a format that matches
|
||||
// more than one route format as long as those formats don't collide.
|
||||
(Some(true), Some(true)) => match (route.format.as_ref(), other.format.as_ref()) {
|
||||
(Some(a), Some(b)) => a.collides_with(b),
|
||||
// A route without a `format` accepts all `Content-Type`s.
|
||||
_ => true
|
||||
},
|
||||
// When a request method may not support a payload, the `Accept` header
|
||||
// is considered during matching. The header can always be `*/*`, which
|
||||
// would match any format. Thus two such routes would always collide.
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -199,13 +199,12 @@ fn formats_match(route: &Route, req: &Request<'_>) -> bool {
|
|||
None => return true,
|
||||
};
|
||||
|
||||
if route.method.supports_payload() {
|
||||
match req.format() {
|
||||
match route.method.allows_request_body() {
|
||||
Some(true) => match req.format() {
|
||||
Some(f) if f.specificity() == 2 => route_format.collides_with(f),
|
||||
_ => false
|
||||
}
|
||||
} else {
|
||||
match req.format() {
|
||||
},
|
||||
_ => match req.format() {
|
||||
Some(f) => route_format.collides_with(f),
|
||||
None => true
|
||||
}
|
||||
|
@ -287,7 +286,7 @@ mod tests {
|
|||
let client = Client::debug_with(vec![]).expect("client");
|
||||
let mut req = client.req(m, "/");
|
||||
if let Some(mt_str) = mt1.into() {
|
||||
if m.supports_payload() {
|
||||
if m.allows_request_body() == Some(true) {
|
||||
req.replace_header(mt_str.parse::<ContentType>().unwrap());
|
||||
} else {
|
||||
req.replace_header(mt_str.parse::<Accept>().unwrap());
|
||||
|
|
|
@ -8,30 +8,63 @@ struct FormData {
|
|||
}
|
||||
|
||||
#[patch("/", data = "<form_data>")]
|
||||
fn bug(form_data: Form<FormData>) -> &'static str {
|
||||
fn patch(form_data: Form<FormData>) -> &'static str {
|
||||
assert_eq!("Form data", form_data.into_inner().form_data);
|
||||
"OK"
|
||||
"PATCH OK"
|
||||
}
|
||||
|
||||
#[route(UPDATEREDIRECTREF, uri = "/", data = "<form_data>")]
|
||||
fn urr(form_data: Form<FormData>) -> &'static str {
|
||||
assert_eq!("Form data", form_data.into_inner().form_data);
|
||||
"UPDATEREDIRECTREF OK"
|
||||
}
|
||||
|
||||
#[route("VERSION-CONTROL", uri = "/", data = "<form_data>")]
|
||||
fn vc(form_data: Form<FormData>) -> &'static str {
|
||||
assert_eq!("Form data", form_data.into_inner().form_data);
|
||||
"VERSION-CONTROL OK"
|
||||
}
|
||||
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rocket::local::blocking::Client;
|
||||
use rocket::http::{Status, ContentType};
|
||||
use rocket::http::{Status, ContentType, Method};
|
||||
|
||||
#[test]
|
||||
fn method_eval() {
|
||||
let client = Client::debug_with(routes![bug]).unwrap();
|
||||
let client = Client::debug_with(routes![patch, urr, vc]).unwrap();
|
||||
let response = client.post("/")
|
||||
.header(ContentType::Form)
|
||||
.body("_method=patch&form_data=Form+data")
|
||||
.dispatch();
|
||||
|
||||
assert_eq!(response.into_string(), Some("OK".into()));
|
||||
assert_eq!(response.into_string(), Some("PATCH OK".into()));
|
||||
|
||||
let response = client.post("/")
|
||||
.header(ContentType::Form)
|
||||
.body("_method=updateredirectref&form_data=Form+data")
|
||||
.dispatch();
|
||||
|
||||
assert_eq!(response.into_string(), Some("UPDATEREDIRECTREF OK".into()));
|
||||
|
||||
let response = client.req(Method::UpdateRedirectRef, "/")
|
||||
.header(ContentType::Form)
|
||||
.body("form_data=Form+data")
|
||||
.dispatch();
|
||||
|
||||
assert_eq!(response.into_string(), Some("UPDATEREDIRECTREF OK".into()));
|
||||
|
||||
let response = client.post("/")
|
||||
.header(ContentType::Form)
|
||||
.body("_method=version-control&form_data=Form+data")
|
||||
.dispatch();
|
||||
|
||||
assert_eq!(response.into_string(), Some("VERSION-CONTROL OK".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_passes_through() {
|
||||
let client = Client::debug_with(routes![bug]).unwrap();
|
||||
let client = Client::debug_with(routes![patch, urr, vc]).unwrap();
|
||||
let response = client.get("/")
|
||||
.header(ContentType::Form)
|
||||
.body("_method=patch&form_data=Form+data")
|
||||
|
|
|
@ -133,7 +133,7 @@ fn method_serde() {
|
|||
jail.create_file("Rocket.toml", r#"
|
||||
[default]
|
||||
mget = "GET"
|
||||
mput = "PuT"
|
||||
mput = "PUT"
|
||||
mpost = "post"
|
||||
"#)?;
|
||||
|
||||
|
|
Loading…
Reference in New Issue