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 uri = format!("/{}?{}", path, query);
|
||||||
let mut req = client.req(route.method, uri);
|
let mut req = client.req(route.method, uri);
|
||||||
if let Some(ref format) = route.format {
|
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()));
|
req.add_header(ContentType::from(format.clone()));
|
||||||
} else {
|
} else {
|
||||||
req.add_header(Accept::from(format.clone()));
|
req.add_header(Accept::from(format.clone()));
|
||||||
|
|
|
@ -129,12 +129,19 @@ impl Route {
|
||||||
// Emit a warning if a `data` param was supplied for non-payload methods.
|
// Emit a warning if a `data` param was supplied for non-payload methods.
|
||||||
if let Some(ref data) = attr.data {
|
if let Some(ref data) = attr.data {
|
||||||
let lint = Lint::DubiousPayload;
|
let lint = Lint::DubiousPayload;
|
||||||
if !attr.method.0.supports_payload() && lint.enabled(handler.span()) {
|
match attr.method.0.allows_request_body() {
|
||||||
// FIXME(diag: warning)
|
None if lint.enabled(handler.span()) => {
|
||||||
data.full_span.warning("`data` used with non-payload-supporting method")
|
data.full_span.warning("`data` used with non-payload-supporting method")
|
||||||
.note(format!("'{}' does not typically support payloads", attr.method.0))
|
.note(format!("'{}' does not typically support payloads", attr.method.0))
|
||||||
.note(lint.how_to_suppress())
|
.note(lint.how_to_suppress())
|
||||||
.emit_as_item_tokens();
|
.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 */ },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use quote::ToTokens;
|
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 proc_macro2::{TokenStream, Span};
|
||||||
|
|
||||||
use crate::{http, attribute::suppress::Lint};
|
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 {
|
impl FromMeta for Method {
|
||||||
fn from_meta(meta: &MetaItem) -> Result<Self> {
|
fn from_meta(meta: &MetaItem) -> Result<Self> {
|
||||||
let span = meta.value_span();
|
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 {
|
let string = meta.path().ok()
|
||||||
if let Some(ident) = path.last_ident() {
|
.and_then(|p| p.get_ident().cloned())
|
||||||
let method = ident.to_string().parse()
|
.map(|ident| (ident.span(), ident.to_string()))
|
||||||
.map_err(|_| span.error("invalid HTTP method").help(&*help_text))?;
|
.or_else(|| match meta.lit() {
|
||||||
|
Ok(syn::Lit::Str(s)) => Some((s.span(), s.value())),
|
||||||
|
_ => None
|
||||||
|
});
|
||||||
|
|
||||||
if !VALID_METHODS.contains(&method) {
|
if let Some((span, string)) = string {
|
||||||
return Err(span.error("invalid HTTP method for route handlers")
|
string.to_ascii_uppercase()
|
||||||
.help(&*help_text));
|
.parse()
|
||||||
}
|
.map(Method)
|
||||||
|
.map_err(|_| span.error("invalid or unknown HTTP method").help(help))
|
||||||
return Ok(Method(method));
|
} else {
|
||||||
}
|
let err = format!("expected method ident or string, found {}", meta.description());
|
||||||
|
Err(span.error(err).help(help))
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(span.error(format!("expected identifier, found {}", meta.description()))
|
|
||||||
.help(&*help_text))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToTokens for Method {
|
impl ToTokens for Method {
|
||||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||||
let mut chars = self.0.as_str().chars();
|
let variant = syn::Ident::new(self.0.variant_str(), Span::call_site());
|
||||||
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());
|
|
||||||
tokens.extend(quote!(::rocket::http::Method::#variant));
|
tokens.extend(quote!(::rocket::http::Method::#variant));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,138 +1,361 @@
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
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?
|
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]
|
||||||
|
}
|
||||||
|
|
||||||
/// Representation of HTTP methods.
|
#[doc(hidden)]
|
||||||
///
|
#[macro_export]
|
||||||
/// # (De)serialization
|
macro_rules! define_methods {
|
||||||
///
|
($($V:ident $name:tt $body:ident $safe:ident $idem:ident $([$n:expr,$s:expr])+)*) => {
|
||||||
/// `Method` is both `Serialize` and `Deserialize`, represented as an
|
/// An HTTP method.
|
||||||
/// [uncased](crate::uncased) string. For example, [`Method::Get`] serializes to
|
///
|
||||||
/// `"GET"` and deserializes from any casing of `"GET"` including `"get"`,
|
/// Each variant corresponds to a method in the [HTTP Method Registry].
|
||||||
/// `"GeT"`, and `"GET"`.
|
/// The string form of the method can be obtained via
|
||||||
///
|
/// [`Method::as_str()`] and parsed via the `FromStr` or
|
||||||
/// ```rust
|
/// `TryFrom<&[u8]>` implementations. The parse implementations parse
|
||||||
/// # #[cfg(feature = "serde")] mod serde {
|
/// both the case-sensitive string form as well as a lowercase version
|
||||||
/// # use serde_ as serde;
|
/// of the string, but _not_ mixed-case versions.
|
||||||
/// use serde::{Serialize, Deserialize};
|
///
|
||||||
/// use rocket::http::Method;
|
/// [HTTP Method Registry]: https://www.iana.org/assignments/http-methods/http-methods.xhtml
|
||||||
///
|
///
|
||||||
/// #[derive(Deserialize, Serialize)]
|
/// # (De)Serialization
|
||||||
/// # #[serde(crate = "serde_")]
|
///
|
||||||
/// struct Foo {
|
/// `Method` is both `Serialize` and `Deserialize`.
|
||||||
/// method: Method,
|
///
|
||||||
/// }
|
/// - `Method` _serializes_ as the specification-defined string form
|
||||||
/// # }
|
/// of the method, equivalent to the value returned from
|
||||||
/// ```
|
/// [`Method::as_str()`].
|
||||||
#[repr(u8)]
|
/// - `Method` _deserializes_ from method's string form _or_ from a
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
/// lowercased string, equivalent to the `FromStr` implementation.
|
||||||
pub enum Method {
|
///
|
||||||
/// The `GET` variant.
|
/// For example, [`Method::Get`] serializes to `"GET"` and deserializes
|
||||||
Get,
|
/// from either `"GET"` or `"get"` but not `"GeT"`.
|
||||||
/// The `PUT` variant.
|
///
|
||||||
Put,
|
/// ```rust
|
||||||
/// The `POST` variant.
|
/// # #[cfg(feature = "serde")] mod serde {
|
||||||
Post,
|
/// # use serde_ as serde;
|
||||||
/// The `DELETE` variant.
|
/// use serde::{Serialize, Deserialize};
|
||||||
Delete,
|
/// use rocket::http::Method;
|
||||||
/// The `OPTIONS` variant.
|
///
|
||||||
Options,
|
/// #[derive(Deserialize, Serialize)]
|
||||||
/// The `HEAD` variant.
|
/// # #[serde(crate = "serde_")]
|
||||||
Head,
|
/// struct Foo {
|
||||||
/// The `TRACE` variant.
|
/// method: Method,
|
||||||
Trace,
|
/// }
|
||||||
/// The `CONNECT` variant.
|
/// # }
|
||||||
Connect,
|
/// ```
|
||||||
/// The `PATCH` variant.
|
#[non_exhaustive]
|
||||||
Patch
|
#[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
|
||||||
|
/// use rocket::http::Method;
|
||||||
|
///
|
||||||
|
/// assert!(Method::Get.is_safe());
|
||||||
|
/// assert!(Method::Head.is_safe());
|
||||||
|
///
|
||||||
|
/// assert!(!Method::Put.is_safe());
|
||||||
|
/// assert!(!Method::Post.is_safe());
|
||||||
|
/// ```
|
||||||
|
pub const fn is_safe(&self) -> bool {
|
||||||
|
const yes: bool = true;
|
||||||
|
const no: bool = false;
|
||||||
|
|
||||||
|
match self {
|
||||||
|
$(Self::$V => $safe),*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
/// 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");
|
||||||
|
/// ```
|
||||||
|
pub const fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
$(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 {
|
impl Method {
|
||||||
/// Returns `true` if an HTTP request with the method represented by `self`
|
/// Deprecated. Returns `self.allows_request_body() == Some(true)`.
|
||||||
/// always supports a payload.
|
|
||||||
///
|
///
|
||||||
/// The following methods always support payloads:
|
/// Use [`Method::allows_request_body()`] instead.
|
||||||
///
|
#[deprecated(since = "0.6", note = "use Self::allows_request_body()")]
|
||||||
/// * `PUT`, `POST`, `DELETE`, `PATCH`
|
pub const fn supports_payload(self) -> bool {
|
||||||
///
|
match self.allows_request_body() {
|
||||||
/// The following methods _do not_ always support payloads:
|
Some(v) => v,
|
||||||
///
|
None => false,
|
||||||
/// * `GET`, `HEAD`, `CONNECT`, `TRACE`, `OPTIONS`
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # extern crate rocket;
|
|
||||||
/// use rocket::http::Method;
|
|
||||||
///
|
|
||||||
/// assert_eq!(Method::Get.supports_payload(), false);
|
|
||||||
/// assert_eq!(Method::Post.supports_payload(), true);
|
|
||||||
/// ```
|
|
||||||
#[inline]
|
|
||||||
pub fn supports_payload(self) -> bool {
|
|
||||||
match self {
|
|
||||||
Put | Post | Delete | Patch => true,
|
|
||||||
Get | Head | Connect | Trace | Options => false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the string representation of `self`.
|
use define_methods as define_methods;
|
||||||
///
|
|
||||||
/// # Example
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
///
|
pub struct ParseMethodError;
|
||||||
/// ```rust
|
|
||||||
/// # extern crate rocket;
|
impl std::error::Error for ParseMethodError { }
|
||||||
/// use rocket::http::Method;
|
|
||||||
///
|
impl fmt::Display for ParseMethodError {
|
||||||
/// assert_eq!(Method::Get.as_str(), "GET");
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
/// ```
|
f.write_str("invalid HTTP method")
|
||||||
#[inline]
|
|
||||||
pub 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",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for Method {
|
impl FromStr for Method {
|
||||||
type Err = ();
|
type Err = ParseMethodError;
|
||||||
|
|
||||||
// According to the RFC, method names are case-sensitive. But some old
|
#[inline(always)]
|
||||||
// clients don't follow this, so we just do a case-insensitive match here.
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
fn from_str(s: &str) -> Result<Method, ()> {
|
Self::try_from(s.as_bytes())
|
||||||
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),
|
impl AsRef<str> for Method {
|
||||||
x if uncased::eq(x, Delete.as_str()) => Ok(Delete),
|
fn as_ref(&self) -> &str {
|
||||||
x if uncased::eq(x, Options.as_str()) => Ok(Options),
|
self.as_str()
|
||||||
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(()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Method {
|
impl fmt::Display for Method {
|
||||||
#[inline(always)]
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
self.as_str().fmt(f)
|
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")]
|
#[cfg(feature = "serde")]
|
||||||
mod serde {
|
mod serde {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
@ -59,11 +59,8 @@ impl Rocket<Orbit> {
|
||||||
) -> RequestToken {
|
) -> RequestToken {
|
||||||
// Check if this is a form and if the form contains the special _method
|
// Check if this is a form and if the form contains the special _method
|
||||||
// field which we use to reinterpret the request's method.
|
// field which we use to reinterpret the request's method.
|
||||||
let (min_len, max_len) = ("_method=get".len(), "_method=delete".len());
|
if req.method() == Method::Post && req.content_type().map_or(false, |v| v.is_form()) {
|
||||||
let peek_buffer = data.peek(max_len).await;
|
let peek_buffer = data.peek(32).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 {
|
|
||||||
let method = std::str::from_utf8(peek_buffer).ok()
|
let method = std::str::from_utf8(peek_buffer).ok()
|
||||||
.and_then(|raw_form| Form::values(raw_form).next())
|
.and_then(|raw_form| Form::values(raw_form).next())
|
||||||
.filter(|field| field.name == "_method")
|
.filter(|field| field.name == "_method")
|
||||||
|
|
|
@ -2,36 +2,25 @@ use crate::http::Method;
|
||||||
|
|
||||||
pub struct AtomicMethod(ref_swap::RefSwap<'static, 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 {
|
impl AtomicMethod {
|
||||||
|
#[inline]
|
||||||
pub fn new(value: Method) -> Self {
|
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 {
|
pub fn load(&self) -> Method {
|
||||||
*self.0.load(std::sync::atomic::Ordering::Acquire)
|
*self.0.load(std::sync::atomic::Ordering::Acquire)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
pub fn set(&mut self, new: Method) {
|
pub fn set(&mut self, new: Method) {
|
||||||
*self = Self::new(new);
|
*self = Self::new(new);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
pub fn store(&self, new: Method) {
|
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.
|
/// Returns the media type "format" of the request.
|
||||||
///
|
///
|
||||||
/// The "format" of a request is either the Content-Type, if the request
|
/// The returned `MediaType` is derived from either the `Content-Type` or
|
||||||
/// methods indicates support for a payload, or the preferred media type in
|
/// the `Accept` header of the request, based on whether the request's
|
||||||
/// the Accept header otherwise. If the method indicates no payload and no
|
/// method allows a body (see [`Method::allows_request_body()`]). The table
|
||||||
/// Accept header is specified, a media type of `Any` is returned.
|
/// 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
|
/// The media type returned from this method is used to match against the
|
||||||
/// `format` route attribute.
|
/// `format` route attribute.
|
||||||
|
@ -691,7 +701,7 @@ impl<'r> Request<'r> {
|
||||||
/// ```
|
/// ```
|
||||||
pub fn format(&self) -> Option<&MediaType> {
|
pub fn format(&self) -> Option<&MediaType> {
|
||||||
static ANY: MediaType = MediaType::Any;
|
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())
|
self.content_type().map(|ct| ct.media_type())
|
||||||
} else {
|
} else {
|
||||||
// TODO: Should we be using `accept_first` or `preferred`? Or
|
// 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 {
|
fn formats_collide(route: &Route, other: &Route) -> bool {
|
||||||
// If the routes' method doesn't support a payload, then format matching
|
match (route.method.allows_request_body(), other.method.allows_request_body()) {
|
||||||
// considers the `Accept` header. The client can always provide a media type
|
// Payload supporting methods match against `Content-Type` which must be
|
||||||
// that will cause a collision through non-specificity, i.e, `*/*`.
|
// fully specified, so the request cannot contain a format that matches
|
||||||
if !route.method.supports_payload() && !other.method.supports_payload() {
|
// more than one route format as long as those formats don't collide.
|
||||||
return true;
|
(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.
|
||||||
// Payload supporting methods match against `Content-Type`. We only
|
_ => true
|
||||||
// consider requests as having a `Content-Type` if they're fully
|
},
|
||||||
// specified. A route without a `format` accepts all `Content-Type`s. A
|
// When a request method may not support a payload, the `Accept` header
|
||||||
// request without a format only matches routes without a format.
|
// is considered during matching. The header can always be `*/*`, which
|
||||||
match (route.format.as_ref(), other.format.as_ref()) {
|
// would match any format. Thus two such routes would always collide.
|
||||||
(Some(a), Some(b)) => a.collides_with(b),
|
_ => true,
|
||||||
_ => true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -199,13 +199,12 @@ fn formats_match(route: &Route, req: &Request<'_>) -> bool {
|
||||||
None => return true,
|
None => return true,
|
||||||
};
|
};
|
||||||
|
|
||||||
if route.method.supports_payload() {
|
match route.method.allows_request_body() {
|
||||||
match req.format() {
|
Some(true) => match req.format() {
|
||||||
Some(f) if f.specificity() == 2 => route_format.collides_with(f),
|
Some(f) if f.specificity() == 2 => route_format.collides_with(f),
|
||||||
_ => false
|
_ => false
|
||||||
}
|
},
|
||||||
} else {
|
_ => match req.format() {
|
||||||
match req.format() {
|
|
||||||
Some(f) => route_format.collides_with(f),
|
Some(f) => route_format.collides_with(f),
|
||||||
None => true
|
None => true
|
||||||
}
|
}
|
||||||
|
@ -287,7 +286,7 @@ mod tests {
|
||||||
let client = Client::debug_with(vec![]).expect("client");
|
let client = Client::debug_with(vec![]).expect("client");
|
||||||
let mut req = client.req(m, "/");
|
let mut req = client.req(m, "/");
|
||||||
if let Some(mt_str) = mt1.into() {
|
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());
|
req.replace_header(mt_str.parse::<ContentType>().unwrap());
|
||||||
} else {
|
} else {
|
||||||
req.replace_header(mt_str.parse::<Accept>().unwrap());
|
req.replace_header(mt_str.parse::<Accept>().unwrap());
|
||||||
|
|
|
@ -8,30 +8,63 @@ struct FormData {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[patch("/", data = "<form_data>")]
|
#[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);
|
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 {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use rocket::local::blocking::Client;
|
use rocket::local::blocking::Client;
|
||||||
use rocket::http::{Status, ContentType};
|
use rocket::http::{Status, ContentType, Method};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn method_eval() {
|
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("/")
|
let response = client.post("/")
|
||||||
.header(ContentType::Form)
|
.header(ContentType::Form)
|
||||||
.body("_method=patch&form_data=Form+data")
|
.body("_method=patch&form_data=Form+data")
|
||||||
.dispatch();
|
.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]
|
#[test]
|
||||||
fn get_passes_through() {
|
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("/")
|
let response = client.get("/")
|
||||||
.header(ContentType::Form)
|
.header(ContentType::Form)
|
||||||
.body("_method=patch&form_data=Form+data")
|
.body("_method=patch&form_data=Form+data")
|
||||||
|
|
|
@ -133,7 +133,7 @@ fn method_serde() {
|
||||||
jail.create_file("Rocket.toml", r#"
|
jail.create_file("Rocket.toml", r#"
|
||||||
[default]
|
[default]
|
||||||
mget = "GET"
|
mget = "GET"
|
||||||
mput = "PuT"
|
mput = "PUT"
|
||||||
mpost = "post"
|
mpost = "post"
|
||||||
"#)?;
|
"#)?;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue