Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide attribute macro for multiple HTTP methods #1674

Merged
merged 14 commits into from
Sep 16, 2020

Conversation

mattgathu
Copy link
Contributor

@mattgathu mattgathu commented Sep 12, 2020

PR Type

Feature

PR Checklist

  • Tests for the changes have been added / updated.
  • Documentation comments have been added / updated.
  • A changelog entry has been made for the appropriate packages.
  • Format code with the latest stable rustfmt

Overview

Provide attribute macro for multiple HTTP methods

Define a new route attribute macro that supports defining multiple
HTTP methods to be routed to (handled by) a single handler.

The attribute macro syntax looks like this

use actix_web::route;

#[route("/multi", method = "GET", method = "POST")]
async fn multi_methods() -> &'static str {
    "Hello world!\r\n"
}

This implementation extends the GuardType enum in actix-web-codegen to have a
new GuardType::Multi variant that denotes when multiple method guards
are used.

A new method attribute in the route attribute macro provides an HTTP method to provide a guard for.
This attribute can be used multiple times for multiple HTTP methods.

The code matches the methods to the respective
GuardType and uses the AnyGuard struct to combine them together.

The generated code looks like this:

pub struct multi_methods;
impl actix_web::dev::HttpServiceFactory for multi_methods {
    fn register(self, __config: &mut actix_web::dev::AppService) {
        async fn multi_methods() -> &'static str {
            "Hello world!\r\n"
        }
        let __resource = actix_web::Resource::new("/multi")
            .name("multi_methods")
            .guard(actix_web::guard::Any(actix_web::guard::Get()).or(actix_web::guard::Post()))
            .to(multi_methods);
        actix_web::dev::HttpServiceFactory::register(__resource, __config)
    }
}

This Closes #1360

@codecov-commenter
Copy link

codecov-commenter commented Sep 12, 2020

Codecov Report

Merging #1674 into master will decrease coverage by 0.19%.
The diff coverage is 0.00%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1674      +/-   ##
==========================================
- Coverage   53.51%   53.31%   -0.20%     
==========================================
  Files         123      123              
  Lines       11876    11919      +43     
==========================================
  Hits         6355     6355              
- Misses       5521     5564      +43     
Impacted Files Coverage Δ
actix-web-codegen/src/route.rs 0.00% <0.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update d707704...64a0339. Read the comment docs.

@robjtede robjtede changed the title [Feature] Provide attribute macro for multiple HTTP methods Provide attribute macro for multiple HTTP methods Sep 13, 2020
@robjtede robjtede added C-feature Category: new functionality A-codegen project: actix-web-codegen B-semver-minor labels Sep 13, 2020
@Silentdoer
Copy link
Contributor

Silentdoer commented Sep 13, 2020

I think this api should be:

#[route("/multi", method = "GET", method = "POST")]
async fn multi_methods() -> &'static str {
    "Hello world!\r\n"
}

otherwise it's not consistent with:

#[get("/index2", wrap = "middleware::SayHi", wrap = "middleware::SayHi2")]

@robjtede robjtede removed the C-feature Category: new functionality label Sep 13, 2020
@Silentdoer
Copy link
Contributor

Silentdoer commented Sep 13, 2020

or may be:

#[route("/multi", methods = ["GET","POST"])]
async fn multi_methods() -> &'static str {
    "Hello world!\r\n"
}

@mattgathu
Copy link
Contributor Author

mattgathu commented Sep 13, 2020

Thanks for the feedback @Silentdoer

My initial approach was #[route("/multi", methods = ["GET","POST"])] but unfortunately only literal arguments are allowed in this position, and that ruled out the [...] syntax.

I can reimplement the macro to have the #[route("/multi", method = "GET", method = "POST")] syntax. If the core contributor(s) can affirm that this is the preferred syntax. maybe @robjtede or @JohnTitor

@mattgathu
Copy link
Contributor Author

I've updated the syntax to be #[route("/multi", method = "GET", method = "POST")]

actix-web-codegen/src/route.rs Outdated Show resolved Hide resolved
actix-web-codegen/src/route.rs Outdated Show resolved Hide resolved
actix-web-codegen/src/route.rs Outdated Show resolved Hide resolved
actix-web-codegen/src/route.rs Outdated Show resolved Hide resolved
actix-web-codegen/src/lib.rs Outdated Show resolved Hide resolved
actix-web-codegen/src/route.rs Outdated Show resolved Hide resolved
actix-web-codegen/src/route.rs Show resolved Hide resolved
@robjtede
Copy link
Member

Hey @mattgathu, it's great to see you tackling this!

I'd like to land #1677 first so we can test some failure cases in CI and make sure the compile error outputs make sense.

Copy link
Member

@popzxc popzxc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks really nice. Thanks!

@robjtede
Copy link
Member

@mattgathu #1677 is merged so it'd be great to get some trybuild tests added as part of this. Try to get in as many weird edge case failures as you can think of.

@robjtede robjtede requested review from a team September 13, 2020 17:38
src/guard.rs Outdated Show resolved Hide resolved
@mattgathu
Copy link
Contributor Author

@robjtede I got an issue with tests passing on rustc 1.42.0 but failing on the current stable.
Here is the diff:

EXPECTED:
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
error: The #[route(..)] macro requires at least one `method` attribute
##[error] --> $DIR/route-missing-method-fail.rs:3:1
  |
3 | #[route("/")]
  | ^^^^^^^^^^^^^

error[E0425]: cannot find value `index` in this scope
##[error]  --> $DIR/route-missing-method-fail.rs:10:49
   |
10 |     let srv = test::start(|| App::new().service(index));
   |                                                 ^^^^^ not found in this scope
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
ACTUAL OUTPUT:
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
error: The #[route(..)] macro requires at least one `method` attribute
##[error] --> $DIR/route-missing-method-fail.rs:3:1
  |
3 | #[route("/")]
  | ^^^^^^^^^^^^^
  |
  = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)

error[E0425]: cannot find value `index` in this scope
##[error]  --> $DIR/route-missing-method-fail.rs:10:49
   |
10 |     let srv = test::start(|| App::new().service(index));
   |                                                 ^^^^^ not found in this scope
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
note: If the actual output is the correct output you can bless it by rerunning
      your test with the environment variable TRYBUILD=overwrite

note the = note: this error originates in an attribute macro... line.

Do you have any suggestions on how to handle this?

@robjtede
Copy link
Member

Do you have any suggestions on how to handle this?

Hmm... I'll have a look.

In the mean time, I feel that the macro should be called "resource" as it is more consistent with the manual routing methods. A resource can define multiple methods on a path where a route is a single method.

@mattgathu
Copy link
Contributor Author

In the mean time, I feel that the macro should be called "resource" as it is more consistent with the manual routing methods. A resource can define multiple methods on a path where a route is a single method.

I would argue that route is more fitting as it expresses the intention: to route one or more http methods to the same handler. It is also, I would argue, more consistent with the HTTP verbs get, put etc as it is also a verb.

resource is a noun and doesn't capture/express the user's intent.

I'm viewing route as a verb and not its noun meaning

@robjtede
Copy link
Member

Alright, fair argument. Would like to get more opinons from @actix/contributors then.

When you read it as a verb it makes sense even though you'd have to sort of read it as #[route_to("/abc")]. I think there's value in keeping the nomenclature consitent with existing routing methods. But, again, more input here will be good.

@robjtede
Copy link
Member

I'm coming around to #[route(...)] for brevity sake.

In other news, I think I've found a solution to the compile error difference while letting us test on stable too. Great find with that rustversion macro; using that you can do this:

#[test]
fn compile_macros() {
    let t = trybuild::TestCases::new();

    t.pass("tests/trybuild/simple.rs");
    t.compile_fail("tests/trybuild/simple-fail.rs");

    t.pass("tests/trybuild/route-ok.rs");
    t.compile_fail("tests/trybuild/route-duplicate-method-fail.rs");
    t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs");
    missing_method_fail_hack(&t);
}

#[rustversion::stable(1.42)]
fn missing_method_fail_hack(t: &trybuild::TestCases) {
    t.compile_fail("tests/trybuild/route-missing-method-fail-msrv.rs");
}

#[rustversion::not(stable(1.42))]
fn missing_method_fail_hack(t: &trybuild::TestCases) {
    t.compile_fail("tests/trybuild/route-missing-method-fail.rs");
}

Then have two versions of the test file...:

  • route-missing-method-fail.rs
  • route-missing-method-fail-msrv.rs

(Even cleaner, you can symlink the -msrv version to the original.)

...and two versions of the compiler output:

  • route-missing-method-fail.stderr
  • route-missing-method-fail-msrv.stderr

@fakeshadow
Copy link
Contributor

I'm not against this poll but would like to share my two cents.

I feel attributes should follow the rust std style. For example:
#[route(method = "")]
#[route(any(method = "", method = "" ))]
#[route(not(method = ""))]

@mattgathu
Copy link
Contributor Author

@robjtede regarding the trybuild tests, the Actix-web's CI matrix has 1.42, current stable and nightly tests with the compilers never guaranteed to produce similar outputs. There will be a probability that each time nightly or the current stable is updated the tests will break. Pinning the tests to 1.42 avoids this likely breakage.

Having multiple tests for different compilers is great but might not be maintainable IMHO.

@robjtede
Copy link
Member

robjtede commented Sep 15, 2020

I think the maintenance cost is small enough using this approach, testing for MSRV and latest stable is enough; we should not run any trybuild tests on nightly.

Worst case we will need to update the stderr files every 6 weeks. But I think theres value in knowing when compiler output changes for our macro errors, anyway.

mattgathu and others added 12 commits September 15, 2020 20:26
What
--
Define a new `route` attribute macro that supports defining multiple
HTTP methods to routed to (handled by) a single handler.

The attribute macro syntax looks like this
```rust
use actix_web::route;

async fn multi_methods() -> &'static str {
    "Hello world!\r\n"
}
```

How
--
This implementation extends the [`GuardType`][1] enum in actix-web-codegen to have a
new `GuardType::Multi` variant that denotes when multiple method guards
are used.

A new `methods` attribute in the `route` attribute macro provides a
comma-separated list of HTTP methods to provide guard for.

The code parses the methods list, matches them to the respective
`GuardType` and uses the `AnyGuard` struct to combine them together.

A constructor method for [`AnyGuard`][2] is added to support this.

The generated code looks like this:
```rust
pub struct multi_methods;
impl actix_web::dev::HttpServiceFactory for multi_methods {
    fn register(self, __config: &mut actix_web::dev::AppService) {
    ¦   async fn multi_methods() -> &'static str {
    ¦   ¦   "Hello world!\r\n"
    ¦   }
    ¦   let __resource = actix_web::Resource::new("/multi")
    ¦   ¦   .name("multi_methods")
    ¦   ¦   .guard(actix_web::guard::AnyGuard::new(<[_]>::into_vec(box [
    ¦   ¦   ¦   Box::new(actix_web::guard::Get()),
    ¦   ¦   ¦   Box::new(actix_web::guard::Post()),
    ¦   ¦   ])))
    ¦   ¦   .to(multi_methods);
    ¦   actix_web::dev::HttpServiceFactory::register(__resource, __config)
    }
}
```

**NOTE: This is my first attempt that implementing this feature.
Feedback and mentorship is highly welcome to improve it :-)**

Why
--
This fixes actix#1360

[1]: https://github.com/actix/actix-web/blob/master/actix-web-codegen/src/route.rs#L21
[2]: https://github.com/actix/actix-web/blob/master/src/guard.rs#L104s
* assume HTTP methods always specified in uppercase e.g `GET`
* simplify code generation by quote!
Refactor code to detect when the same HTTP method is specified more than
once. eg. `#[route("/multi", method = "GET", method = "GET")]`
and return a compile time error.
Try converting String literal to GuardType
Co-authored-by: Rob Ede <robjtede@icloud.com>
@mattgathu
Copy link
Contributor Author

mattgathu commented Sep 15, 2020

@robjtede I haven't found a way to differentiate between the 1.42 stable and other stable versions using the rustversion macros :-(

You have #[rustversion::not(stable(1.42))] in your example, but it doesn't eliminate nightly runs.

Edit: I ended up building a dummy shim and stacking the macros 🤞

@robjtede
Copy link
Member

Great! I'm happy to go ahead with that test set up.

Check out this commit (02e16c6) for how to do the multi guard without requiring changes to actix-web. The generated code looks like this for the example in that commit:

let __resource = actix_web::Resource::new("/")
    .name("index")
    .guard(
        actix_web::guard::Any(actix_web::guard::Get())
            .or(actix_web::guard::Trace())
            .or(actix_web::guard::Post())
            .or(actix_web::guard::Head()),
    )
    .to(index);

Copy link
Member

@robjtede robjtede left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks so much for working on this 👍 great job!

@robjtede
Copy link
Member

@fakeshadow RE your comment on attribute style: I don't think there's a use case for not(...) here and I think it's safe to assume the syntax is any(...) without having to explicitly write that out. This is also inkeeping with current wrap="..." style.

Speak now if you still feel strongly on it.

@fakeshadow
Copy link
Contributor

I'm fine with the current attributes. You guys did a good job.

@robjtede
Copy link
Member

And awaaayyyyy we go...

@robjtede robjtede merged commit 509b2e6 into actix:master Sep 16, 2020
@mattgathu mattgathu deleted the multi-methods-attribute-macro branch September 17, 2020 10:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-codegen project: actix-web-codegen B-semver-minor
Projects
None yet
Development

Successfully merging this pull request may close these issues.

attributes macros for multiple methods
6 participants