diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 128f51ffd..2df863ae8 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,6 +1,6 @@ --- -name: bug report -about: create a bug report +name: Bug Report +about: Create a bug report. --- Your issue may already be reported! diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..6426eab65 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,28 @@ +## PR Type +What kind of change does this PR make? + + + +- [ ] Bug fix +- [ ] Feature +- [ ] Refactor / code style change (no functional or public API changes) +- [ ] Other + + +## PR Checklist +Check your PR fulfills the following: + + + +- [ ] Tests for the changes have been added / updated. +- [ ] Documentation comments have been added / updated. +- [ ] A changelog entry has been made for the appropriate packages. + + +## Overview + + + + + + diff --git a/CHANGES.md b/CHANGES.md index 58cddf78d..22a389d10 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,8 @@ ### Added * Re-export `actix_rt::main` as `actix_web::main`. +* `HttpRequest::match_pattern` and `ServiceRequest::match_pattern` for extracting the matched + resource pattern. ### Changed diff --git a/Cargo.toml b/Cargo.toml index de7222f1f..8c4719b92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,8 +97,8 @@ serde_json = "1.0" serde_urlencoded = "0.6.1" time = { version = "0.2.7", default-features = false, features = ["std"] } url = "2.1" -open-ssl = { version="0.10", package = "openssl", optional = true } -rust-tls = { version = "0.17.0", package = "rustls", optional = true } +open-ssl = { package = "openssl", version = "0.10", optional = true } +rust-tls = { package = "rustls", version = "0.17.0", optional = true } tinyvec = { version = "0.3", features = ["alloc"] } [dev-dependencies] diff --git a/README.md b/README.md index 6382abd4d..ade632877 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ You may consider checking out ## Benchmarks -* [TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r18) +* [TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r19) ## License diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 356c7a413..76e528b12 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -11,7 +11,6 @@ documentation = "https://docs.rs/actix-files/" categories = ["asynchronous", "web-programming::http-server"] license = "MIT/Apache-2.0" edition = "2018" -workspace = ".." [lib] name = "actix_files" diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index b2e80591f..8259aaac2 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -1,5 +1,10 @@ # Changes +## [Unreleased] - XXXX-XX-XX + +* Add main entry-point macro that uses re-exported runtime. + + ## [0.2.2] - 2020-05-23 * Add resource middleware on actix-web-codegen [#1467] diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 60480a7a1..178eeeb7e 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -9,7 +9,6 @@ documentation = "https://docs.rs/actix-web-codegen" authors = ["Nikolay Kim "] license = "MIT/Apache-2.0" edition = "2018" -workspace = ".." [lib] proc-macro = true diff --git a/actix-web-codegen/README.md b/actix-web-codegen/README.md index c482a6b36..45eb82c2c 100644 --- a/actix-web-codegen/README.md +++ b/actix-web-codegen/README.md @@ -1,4 +1,4 @@ -# Macros for actix-web framework [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-web-codegen)](https://crates.io/crates/actix-web-codegen) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +# Helper and convenience macros for Actix-web. [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-web-codegen)](https://crates.io/crates/actix-web-codegen) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ## Documentation & Resources diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index 2a49b4714..b6df3f0dd 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -1,11 +1,12 @@ #![recursion_limit = "512"] -//! Actix-web codegen module + +//! Helper and convenience macros for Actix-web. //! -//! Generators for routes and scopes +//! ## Runtime Setup //! -//! ## Route +//! - [main](attr.main.html) //! -//! Macros: +//! ## Resource Macros: //! //! - [get](attr.get.html) //! - [post](attr.post.html) @@ -23,12 +24,12 @@ //! - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard` //! - `wrap="Middleware"` - Registers a resource middleware. //! -//! ## Notes +//! ### Notes //! //! Function name can be specified as any expression that is going to be accessible to the generate //! code (e.g `my_guard` or `my_module::my_guard`) //! -//! ## Example: +//! ### Example: //! //! ```rust //! use actix_web::HttpResponse; @@ -139,3 +140,43 @@ pub fn trace(args: TokenStream, input: TokenStream) -> TokenStream { pub fn patch(args: TokenStream, input: TokenStream) -> TokenStream { route::generate(args, input, route::GuardType::Patch) } + +/// Marks async main function as the actix system entry-point. +/// +/// ## Usage +/// +/// ```rust +/// #[actix_web::main] +/// async fn main() { +/// async { println!("Hello world"); }.await +/// } +/// ``` +#[proc_macro_attribute] +#[cfg(not(test))] // Work around for rust-lang/rust#62127 +pub fn main(_: TokenStream, item: TokenStream) -> TokenStream { + use quote::quote; + + let mut input = syn::parse_macro_input!(item as syn::ItemFn); + let attrs = &input.attrs; + let vis = &input.vis; + let sig = &mut input.sig; + let body = &input.block; + let name = &sig.ident; + + if sig.asyncness.is_none() { + return syn::Error::new_spanned(sig.fn_token, "only async fn is supported") + .to_compile_error() + .into(); + } + + sig.asyncness = None; + + (quote! { + #(#attrs)* + #vis #sig { + actix_web::rt::System::new(stringify!(#name)) + .block_on(async move { #body }) + } + }) + .into() +} diff --git a/rust-toolchain b/rust-toolchain new file mode 100644 index 000000000..32b7211cb --- /dev/null +++ b/rust-toolchain @@ -0,0 +1 @@ +1.40.0 diff --git a/src/app.rs b/src/app.rs index 8178d57fe..ae3d9fdf0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -42,6 +42,7 @@ pub struct App { impl App { /// Create application builder. Application can be configured with a builder-like pattern. + #[allow(clippy::new_without_default)] pub fn new() -> Self { let fref = Rc::new(RefCell::new(None)); App { diff --git a/src/info.rs b/src/info.rs index 5b506d85a..1d9b402a7 100644 --- a/src/info.rs +++ b/src/info.rs @@ -25,7 +25,7 @@ impl ConnectionInfo { Ref::map(req.extensions(), |e| e.get().unwrap()) } - #[allow(clippy::cognitive_complexity)] + #[allow(clippy::cognitive_complexity, clippy::borrow_interior_mutable_const)] fn new(req: &RequestHead, cfg: &AppConfig) -> ConnectionInfo { let mut host = None; let mut scheme = None; diff --git a/src/lib.rs b/src/lib.rs index 09642806f..844f952cc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,18 +1,11 @@ #![warn(rust_2018_idioms, warnings)] -#![allow( - clippy::needless_doctest_main, - clippy::type_complexity, - clippy::borrow_interior_mutable_const -)] +#![allow(clippy::needless_doctest_main, clippy::type_complexity)] + //! Actix web is a small, pragmatic, and extremely fast web framework //! for Rust. //! //! ## Example //! -//! The `#[actix_rt::main]` macro in the example below is provided by the Actix runtime -//! crate, [`actix-rt`](https://crates.io/crates/actix-rt). You will need to include -//! `actix-rt` in your dependencies for it to run. -//! //! ```rust,no_run //! use actix_web::{web, App, Responder, HttpServer}; //! @@ -20,7 +13,7 @@ //! format!("Hello {}! id:{}", info.0, info.1) //! } //! -//! #[actix_rt::main] +//! #[actix_web::main] //! async fn main() -> std::io::Result<()> { //! HttpServer::new(|| App::new().service( //! web::resource("/{name}/{id}/index.html").to(index)) @@ -80,9 +73,7 @@ //! * `compress` - enables content encoding compression support (default enabled) //! * `openssl` - enables ssl support via `openssl` crate, supports `http/2` //! * `rustls` - enables ssl support via `rustls` crate, supports `http/2` -//! * `secure-cookies` - enables secure cookies support, includes `ring` crate as -//! dependency -#![allow(clippy::type_complexity, clippy::new_without_default)] +//! * `secure-cookies` - enables secure cookies support mod app; mod app_service; @@ -106,13 +97,12 @@ pub mod test; mod types; pub mod web; -#[doc(hidden)] pub use actix_web_codegen::*; +pub use actix_rt as rt; // re-export for convenience pub use actix_http::Response as HttpResponse; pub use actix_http::{body, cookie, http, Error, HttpMessage, ResponseError, Result}; -pub use actix_macros::{main, test as test_rt}; pub use crate::app::App; pub use crate::extract::FromRequest; @@ -230,6 +220,7 @@ pub mod client { //! println!("Response: {:?}", response); //! } //! ``` + pub use awc::error::{ ConnectError, InvalidUrl, PayloadError, SendRequestError, WsClientError, }; diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index 6de451c84..fe3ba841c 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -90,6 +90,7 @@ where self.service.poll_ready(cx) } + #[allow(clippy::borrow_interior_mutable_const)] fn call(&mut self, req: ServiceRequest) -> Self::Future { // negotiate content-encoding let encoding = if let Some(val) = req.headers().get(&ACCEPT_ENCODING) { diff --git a/src/middleware/defaultheaders.rs b/src/middleware/defaultheaders.rs index ef2e56e69..6d43aba95 100644 --- a/src/middleware/defaultheaders.rs +++ b/src/middleware/defaultheaders.rs @@ -128,6 +128,7 @@ where self.service.poll_ready(cx) } + #[allow(clippy::borrow_interior_mutable_const)] fn call(&mut self, req: ServiceRequest) -> Self::Future { let inner = self.inner.clone(); let fut = self.service.call(req); diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index 8b881c0a4..57b640bdd 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -478,7 +478,7 @@ impl FormatText { } FormatText::RemoteAddr => { let s = if let Some(ref peer) = req.connection_info().remote_addr() { - FormatText::Str(peer.to_string()) + FormatText::Str((*peer).to_string()) } else { FormatText::Str("-".to_string()) }; diff --git a/src/request.rs b/src/request.rs index f8abeb1bb..8ca897442 100644 --- a/src/request.rs +++ b/src/request.rs @@ -126,6 +126,17 @@ impl HttpRequest { &mut Rc::get_mut(&mut self.0).unwrap().path } + /// The resource definition pattern that matched the path. Useful for logging and metrics. + /// + /// For example, when a resource with pattern `/user/{id}/profile` is defined and a call is made + /// to `/user/123/profile` this function would return `Some("/user/{id}/profile")`. + /// + /// Returns a None when no resource is fully matched, including default services. + #[inline] + pub fn match_pattern(&self) -> Option { + self.0.rmap.match_pattern(self.path()) + } + /// Request extensions #[inline] pub fn extensions(&self) -> Ref<'_, Extensions> { @@ -141,7 +152,6 @@ impl HttpRequest { /// Generate url for named resource /// /// ```rust - /// # extern crate actix_web; /// # use actix_web::{web, App, HttpRequest, HttpResponse}; /// # /// fn index(req: HttpRequest) -> HttpResponse { @@ -599,4 +609,36 @@ mod tests { assert!(tracker.borrow().dropped); } + + #[actix_rt::test] + async fn extract_path_pattern() { + let mut srv = init_service( + App::new().service( + web::scope("/user/{id}") + .service(web::resource("/profile").route(web::get().to( + move |req: HttpRequest| { + assert_eq!( + req.match_pattern(), + Some("/user/{id}/profile".to_owned()) + ); + + HttpResponse::Ok().finish() + }, + ))) + .default_service(web::to(move |req: HttpRequest| { + assert!(req.match_pattern().is_none()); + HttpResponse::Ok().finish() + })), + ), + ) + .await; + + let req = TestRequest::get().uri("/user/22/profile").to_request(); + let res = call_service(&mut srv, req).await; + assert_eq!(res.status(), StatusCode::OK); + + let req = TestRequest::get().uri("/user/22/not-exist").to_request(); + let res = call_service(&mut srv, req).await; + assert_eq!(res.status(), StatusCode::OK); + } } diff --git a/src/rmap.rs b/src/rmap.rs index 47092608c..0a0c96777 100644 --- a/src/rmap.rs +++ b/src/rmap.rs @@ -43,9 +43,7 @@ impl ResourceMap { } } } -} -impl ResourceMap { /// Generate url for named resource /// /// Check [`HttpRequest::url_for()`](../struct.HttpRequest.html#method. @@ -95,6 +93,45 @@ impl ResourceMap { false } + /// Returns the full resource pattern matched against a path or None if no full match + /// is possible. + pub fn match_pattern(&self, path: &str) -> Option { + let path = if path.is_empty() { "/" } else { path }; + + // ensure a full match exists + if !self.has_resource(path) { + return None; + } + + Some(self.traverse_resource_pattern(path)) + } + + /// Takes remaining path and tries to match it up against a resource definition within the + /// current resource map recursively, returning a concatenation of all resource prefixes and + /// patterns matched in the tree. + /// + /// Should only be used after checking the resource exists in the map so that partial match + /// patterns are not returned. + fn traverse_resource_pattern(&self, remaining: &str) -> String { + for (pattern, rmap) in &self.patterns { + if let Some(ref rmap) = rmap { + if let Some(prefix_len) = pattern.is_prefix_match(remaining) { + let prefix = pattern.pattern().to_owned(); + + return [ + prefix, + rmap.traverse_resource_pattern(&remaining[prefix_len..]), + ] + .concat(); + } + } else if pattern.is_match(remaining) { + return pattern.pattern().to_owned(); + } + } + + String::new() + } + fn patterns_for( &self, name: &str, @@ -188,3 +225,81 @@ impl ResourceMap { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn extract_matched_pattern() { + let mut root = ResourceMap::new(ResourceDef::root_prefix("")); + + let mut user_map = ResourceMap::new(ResourceDef::root_prefix("")); + user_map.add(&mut ResourceDef::new("/"), None); + user_map.add(&mut ResourceDef::new("/profile"), None); + user_map.add(&mut ResourceDef::new("/article/{id}"), None); + user_map.add(&mut ResourceDef::new("/post/{post_id}"), None); + user_map.add( + &mut ResourceDef::new("/post/{post_id}/comment/{comment_id}"), + None, + ); + + root.add(&mut ResourceDef::new("/info"), None); + root.add(&mut ResourceDef::new("/v{version:[[:digit:]]{1}}"), None); + root.add( + &mut ResourceDef::root_prefix("/user/{id}"), + Some(Rc::new(user_map)), + ); + + let root = Rc::new(root); + root.finish(Rc::clone(&root)); + + // sanity check resource map setup + + assert!(root.has_resource("/info")); + assert!(!root.has_resource("/bar")); + + assert!(root.has_resource("/v1")); + assert!(root.has_resource("/v2")); + assert!(!root.has_resource("/v33")); + + assert!(root.has_resource("/user/22")); + assert!(root.has_resource("/user/22/")); + assert!(root.has_resource("/user/22/profile")); + + // extract patterns from paths + + assert!(root.match_pattern("/bar").is_none()); + assert!(root.match_pattern("/v44").is_none()); + + assert_eq!(root.match_pattern("/info"), Some("/info".to_owned())); + assert_eq!( + root.match_pattern("/v1"), + Some("/v{version:[[:digit:]]{1}}".to_owned()) + ); + assert_eq!( + root.match_pattern("/v2"), + Some("/v{version:[[:digit:]]{1}}".to_owned()) + ); + assert_eq!( + root.match_pattern("/user/22/profile"), + Some("/user/{id}/profile".to_owned()) + ); + assert_eq!( + root.match_pattern("/user/602CFB82-7709-4B17-ADCF-4C347B6F2203/profile"), + Some("/user/{id}/profile".to_owned()) + ); + assert_eq!( + root.match_pattern("/user/22/article/44"), + Some("/user/{id}/article/{id}".to_owned()) + ); + assert_eq!( + root.match_pattern("/user/22/post/my-post"), + Some("/user/{id}/post/{post_id}".to_owned()) + ); + assert_eq!( + root.match_pattern("/user/22/post/other-post/comment/42"), + Some("/user/{id}/post/{post_id}/comment/{comment_id}".to_owned()) + ); + } +} diff --git a/src/route.rs b/src/route.rs index 2763f3b1a..b17fa9b06 100644 --- a/src/route.rs +++ b/src/route.rs @@ -46,6 +46,7 @@ pub struct Route { impl Route { /// Create new route which matches any request. + #[allow(clippy::new_without_default)] pub fn new() -> Route { Route { service: Box::new(RouteNewService::new(Extract::new(Handler::new(|| { diff --git a/src/service.rs b/src/service.rs index 232a2f132..f7e201779 100644 --- a/src/service.rs +++ b/src/service.rs @@ -195,6 +195,12 @@ impl ServiceRequest { pub fn match_info(&self) -> &Path { self.0.match_info() } + + /// Counterpart to [`HttpRequest::match_pattern`](../struct.HttpRequest.html#method.match_pattern). + #[inline] + pub fn match_pattern(&self) -> Option { + self.0.match_pattern() + } #[inline] /// Get a mutable reference to the Path parameters. diff --git a/src/types/form.rs b/src/types/form.rs index ca1a4b103..c10ed4649 100644 --- a/src/types/form.rs +++ b/src/types/form.rs @@ -252,6 +252,7 @@ pub struct UrlEncoded { fut: Option>>, } +#[allow(clippy::borrow_interior_mutable_const)] impl UrlEncoded { /// Create a new future to URL encode a request pub fn new(req: &HttpRequest, payload: &mut Payload) -> UrlEncoded { diff --git a/src/types/json.rs b/src/types/json.rs index f746fd432..6de9e0d86 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -319,6 +319,7 @@ where U: DeserializeOwned + 'static, { /// Create `JsonBody` for request. + #[allow(clippy::borrow_interior_mutable_const)] pub fn new( req: &HttpRequest, payload: &mut Payload, diff --git a/src/types/payload.rs b/src/types/payload.rs index bad33bfc6..0efdc2c09 100644 --- a/src/types/payload.rs +++ b/src/types/payload.rs @@ -315,6 +315,7 @@ pub struct HttpMessageBody { impl HttpMessageBody { /// Create `MessageBody` for request. + #[allow(clippy::borrow_interior_mutable_const)] pub fn new(req: &HttpRequest, payload: &mut dev::Payload) -> HttpMessageBody { let mut len = None; if let Some(l) = req.headers().get(&header::CONTENT_LENGTH) { diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index f90cef0dd..6265cd415 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -14,7 +14,6 @@ categories = ["network-programming", "asynchronous", license = "MIT/Apache-2.0" exclude = [".gitignore", ".cargo/config"] edition = "2018" -workspace = ".." [package.metadata.docs.rs] features = []