Skip to content

Commit

Permalink
Add 'Method' variants for all registered methods.
Browse files Browse the repository at this point in the history
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.

# Please enter the commit message for your changes. Lines starting
# with '#' will be kept; you may remove them yourself if you want to.
# An empty message aborts the commit.
#
# Date:      Wed Apr 24 18:31:39 2024 -0700
#
# On branch http-methods
# Your branch is ahead of 'origin/http-methods' by 1 commit.
#   (use "git push" to publish your local commits)
#
# Changes to be committed:
#	modified:   benchmarks/src/routing.rs
#	modified:   core/codegen/src/attribute/route/parse.rs
#	modified:   core/codegen/src/http_codegen.rs
#	modified:   core/http/src/method.rs
#	modified:   core/lib/src/lifecycle.rs
#	modified:   core/lib/src/request/atomic_method.rs
#	modified:   core/lib/src/request/request.rs
#	modified:   core/lib/src/router/collider.rs
#	modified:   core/lib/src/router/matcher.rs
#	modified:   core/lib/tests/form_method-issue-45.rs
#	modified:   core/lib/tests/http_serde.rs
#
  • Loading branch information
SergioBenitez committed Apr 25, 2024
1 parent 7c50a58 commit 274366e
Show file tree
Hide file tree
Showing 11 changed files with 450 additions and 206 deletions.
2 changes: 1 addition & 1 deletion benchmarks/src/routing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
Expand Down
19 changes: 13 additions & 6 deletions core/codegen/src/attribute/route/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,19 @@ 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)
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();
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 */ },
}
}

Expand Down
53 changes: 20 additions & 33 deletions core/codegen/src/http_codegen.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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}");

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))?;

if !VALID_METHODS.contains(&method) {
return Err(span.error("invalid HTTP method for route handlers")
.help(&*help_text));
}

return Ok(Method(method));
}
let help = format!("known methods: {}", http::Method::ALL.join(", "));

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 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))
}

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));
}
}
Expand Down
Loading

0 comments on commit 274366e

Please sign in to comment.