diff --git a/CHANGES.md b/CHANGES.md index 2e4d23ba1..5974ee69a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,13 +1,39 @@ # Changes +## [1.0.0] - 2019-05-xx + ### Add +* Add `ServiceRequest::set_payload()` method. + +* Add `test::TestRequest::set_json()` convenience method to automatically + serialize data and set header in test requests. + +### Changes + +* Drop an unnecessary `Option<_>` indirection around `ServerBuilder` from `HttpServer`. #863 + +### Fixed + +* Clear http requests pool on app service drop #860 + + +## [1.0.0-rc] - 2019-05-18 + +### Add + +* Add `Query::from_query()` to extract parameters from a query string. #846 * `QueryConfig`, similar to `JsonConfig` for customizing error handling of query extractors. ### Changes * `JsonConfig` is now `Send + Sync`, this implies that `error_handler` must be `Send + Sync` too. +### Fixed + +* Codegen with parameters in the path only resolves the first registered endpoint #841 + + ## [1.0.0-beta.4] - 2019-05-12 ### Add @@ -19,6 +45,7 @@ * `App::configure` take an `FnOnce` instead of `Fn` * Upgrade actix-net crates + ## [1.0.0-beta.3] - 2019-05-04 ### Added diff --git a/Cargo.toml b/Cargo.toml index 9c670ab16..0bf6e7c9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "1.0.0-beta.5" +version = "1.0.0-rc" authors = ["Nikolay Kim "] description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust." readme = "README.md" @@ -41,7 +41,7 @@ members = [ ] [features] -default = ["brotli", "flate2-zlib", "secure-cookies", "client"] +default = ["brotli", "flate2-zlib", "secure-cookies", "client", "fail"] # http client client = ["awc"] @@ -58,6 +58,8 @@ flate2-rust = ["actix-http/flate2-rust"] # sessions feature, session require "ring" crate and c compiler secure-cookies = ["actix-http/secure-cookies"] +fail = ["actix-http/fail"] + # openssl ssl = ["openssl", "actix-server/ssl", "awc/ssl"] @@ -67,21 +69,20 @@ rust-tls = ["rustls", "actix-server/rust-tls"] [dependencies] actix-codec = "0.1.2" actix-service = "0.4.0" -actix-utils = "0.4.0" -actix-router = "0.1.3" +actix-utils = "0.4.1" +actix-router = "0.1.5" actix-rt = "0.2.2" -actix-web-codegen = "0.1.0-beta.1" -actix-http = { version = "0.2.0", features=["fail"] } -actix-server = "0.5.0" +actix-web-codegen = "0.1.0" +actix-http = "0.2.0" +actix-server = "0.5.1" actix-server-config = "0.1.1" actix-threadpool = "0.1.0" -actix = { version = "0.8.1", features=["http"], optional = true } awc = { version = "0.2.0", optional = true } bytes = "0.4" derive_more = "0.14" encoding = "0.2" -futures = "0.1" +futures = "0.1.25" hashbrown = "0.3.0" log = "0.4" mime = "0.3" @@ -96,7 +97,7 @@ url = { version="1.7", features=["query_encoding"] } # ssl support openssl = { version="0.10", optional = true } -rustls = { version = "^0.15", optional = true } +rustls = { version = "0.15", optional = true } [dev-dependencies] actix-http = { version = "0.2.0", features=["ssl", "brotli", "flate2-zlib"] } diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index 6b4ab57b8..e8457f42f 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,5 +1,10 @@ # Changes +## [0.1.0] - 2019-05-xx + +* NamedFile last-modified check always fails due to nano-seconds + in file modified date #820 + ## [0.1.0-beta.4] - 2019-05-12 * Update actix-web to beta.4 diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 41a7cf1f9..2298e35af 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -337,7 +337,12 @@ impl Responder for NamedFile { } else if let (Some(ref m), Some(header::IfUnmodifiedSince(ref since))) = (last_modified, req.get_header()) { - m > since + let t1: SystemTime = m.clone().into(); + let t2: SystemTime = since.clone().into(); + match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) { + (Ok(t1), Ok(t2)) => t1 > t2, + _ => false, + } } else { false }; @@ -350,7 +355,12 @@ impl Responder for NamedFile { } else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) = (last_modified, req.get_header()) { - m <= since + let t1: SystemTime = m.clone().into(); + let t2: SystemTime = since.clone().into(); + match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) { + (Ok(t1), Ok(t2)) => t1 <= t2, + _ => false, + } } else { false }; diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index 5ee1d6201..3a959890a 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -1,5 +1,9 @@ # Changes +## [0.1.0] - 2019-05-18 + +* Release + ## [0.1.0-beta.4] - 2019-05-12 * Handle cancellation of uploads #834 #736 diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 7d19ef9fd..ca1ff9c91 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-multipart" -version = "0.1.0-beta.4" +version = "0.1.0" authors = ["Nikolay Kim "] description = "Multipart support for actix web framework." readme = "README.md" @@ -18,7 +18,7 @@ name = "actix_multipart" path = "src/lib.rs" [dependencies] -actix-web = "1.0.0-beta.5" +actix-web = "1.0.0-rc" actix-service = "0.4.0" bytes = "0.4" derive_more = "0.14" diff --git a/actix-session/CHANGES.md b/actix-session/CHANGES.md index 5f2bdadd1..10727ae35 100644 --- a/actix-session/CHANGES.md +++ b/actix-session/CHANGES.md @@ -1,5 +1,9 @@ # Changes +## [0.1.0] - 2019-05-18 + +* Use actix-web 1.0.0-rc + ## [0.1.0-beta.4] - 2019-05-12 * Use actix-web 1.0.0-beta.4 diff --git a/actix-session/Cargo.toml b/actix-session/Cargo.toml index 5bebb9a19..b0ef1e2b4 100644 --- a/actix-session/Cargo.toml +++ b/actix-session/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-session" -version = "0.1.0-beta.4" +version = "0.1.0" authors = ["Nikolay Kim "] description = "Session for actix web framework." readme = "README.md" @@ -24,7 +24,7 @@ default = ["cookie-session"] cookie-session = ["actix-web/secure-cookies"] [dependencies] -actix-web = "1.0.0-beta.5" +actix-web = "1.0.0-rc" actix-service = "0.4.0" bytes = "0.4" derive_more = "0.14" diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index fe24d4c30..0341641a3 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -18,7 +18,7 @@ name = "actix_web_actors" path = "src/lib.rs" [dependencies] -actix = "0.8.0" +actix = "0.8.2" actix-web = "1.0.0-beta.5" actix-http = "0.2.0" actix-codec = "0.1.2" diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index 3173ff1f0..4f2d1c865 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -1,5 +1,9 @@ # Changes +## [0.1.0] - 2019-05-18 + +* Release + ## [0.1.0-beta.1] - 2019-04-20 * Gen code for actix-web 1.0.0-beta.1 diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 7ca7912fa..5ca9f416d 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-codegen" -version = "0.1.0-beta.1" +version = "0.1.0" description = "Actix web proc macros" readme = "README.md" authors = ["Nikolay Kim "] diff --git a/actix-web-codegen/tests/test_macro.rs b/actix-web-codegen/tests/test_macro.rs index 9b9ec6f3b..cd899d48d 100644 --- a/actix-web-codegen/tests/test_macro.rs +++ b/actix-web-codegen/tests/test_macro.rs @@ -1,7 +1,7 @@ use actix_http::HttpService; use actix_http_test::TestServer; -use actix_web::{http, App, HttpResponse, Responder}; -use actix_web_codegen::{get, post, put}; +use actix_web::{http, web::Path, App, HttpResponse, Responder}; +use actix_web_codegen::{delete, get, post, put}; use futures::{future, Future}; #[get("/test")] @@ -29,6 +29,45 @@ fn auto_sync() -> impl Future { future::ok(HttpResponse::Ok().finish()) } +#[put("/test/{param}")] +fn put_param_test(_: Path) -> impl Responder { + HttpResponse::Created() +} + +#[delete("/test/{param}")] +fn delete_param_test(_: Path) -> impl Responder { + HttpResponse::NoContent() +} + +#[get("/test/{param}")] +fn get_param_test(_: Path) -> impl Responder { + HttpResponse::Ok() +} + +#[test] +fn test_params() { + let mut srv = TestServer::new(|| { + HttpService::new( + App::new() + .service(get_param_test) + .service(put_param_test) + .service(delete_param_test), + ) + }); + + let request = srv.request(http::Method::GET, srv.url("/test/it")); + let response = srv.block_on(request.send()).unwrap(); + assert_eq!(response.status(), http::StatusCode::OK); + + let request = srv.request(http::Method::PUT, srv.url("/test/it")); + let response = srv.block_on(request.send()).unwrap(); + assert_eq!(response.status(), http::StatusCode::CREATED); + + let request = srv.request(http::Method::DELETE, srv.url("/test/it")); + let response = srv.block_on(request.send()).unwrap(); + assert_eq!(response.status(), http::StatusCode::NO_CONTENT); +} + #[test] fn test_body() { let mut srv = TestServer::new(|| { diff --git a/src/app_service.rs b/src/app_service.rs index f34389840..88e97de1e 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -183,7 +183,7 @@ where } /// Service to convert `Request` to a `ServiceRequest` -pub struct AppInitService +pub struct AppInitService where T: Service, Error = Error>, { @@ -215,19 +215,31 @@ where inner.path.get_mut().update(&head.uri); inner.path.reset(); inner.head = head; + inner.payload = payload; inner.app_data = self.data.clone(); req } else { HttpRequest::new( Path::new(Url::new(head.uri.clone())), head, + payload, self.rmap.clone(), self.config.clone(), self.data.clone(), self.pool, ) }; - self.service.call(ServiceRequest::from_parts(req, payload)) + self.service.call(ServiceRequest::new(req)) + } +} + +impl Drop for AppInitService +where + T: Service, Error = Error>, +{ + fn drop(&mut self) { + self.pool.clear(); + println!("DROP: APP-INIT-ENTRY"); } } @@ -408,3 +420,38 @@ impl NewService for AppEntry { self.factory.borrow_mut().as_mut().unwrap().new_service(&()) } } + +#[cfg(test)] +mod tests { + use actix_service::Service; + use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }; + + use crate::{test, web, App, HttpResponse}; + + struct DropData(Arc); + + impl Drop for DropData { + fn drop(&mut self) { + self.0.store(true, Ordering::Relaxed); + println!("Dropping!"); + } + } + + #[test] + fn drop_data() { + let data = Arc::new(AtomicBool::new(false)); + { + let mut app = test::init_service( + App::new() + .data(DropData(data.clone())) + .service(web::resource("/test").to(|| HttpResponse::Ok())), + ); + let req = test::TestRequest::with_uri("/test").to_request(); + let _ = test::block_on(app.call(req)).unwrap(); + } + assert!(data.load(Ordering::Relaxed)); + } +} diff --git a/src/error.rs b/src/error.rs index a3062b586..e1cc79845 100644 --- a/src/error.rs +++ b/src/error.rs @@ -104,7 +104,9 @@ pub enum QueryPayloadError { impl ResponseError for QueryPayloadError { fn error_response(&self) -> HttpResponse { match *self { - QueryPayloadError::Deserialize(_) => HttpResponse::new(StatusCode::BAD_REQUEST), + QueryPayloadError::Deserialize(_) => { + HttpResponse::new(StatusCode::BAD_REQUEST) + } } } } @@ -163,7 +165,10 @@ mod tests { #[test] fn test_query_payload_error() { - let resp: HttpResponse = QueryPayloadError::Deserialize(serde_urlencoded::from_str::("bad query").unwrap_err()).error_response(); + let resp: HttpResponse = QueryPayloadError::Deserialize( + serde_urlencoded::from_str::("bad query").unwrap_err(), + ) + .error_response(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } diff --git a/src/handler.rs b/src/handler.rs index b53d16389..bd0b35517 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,7 +1,7 @@ use std::convert::Infallible; use std::marker::PhantomData; -use actix_http::{Error, Payload, Response}; +use actix_http::{Error, Response}; use actix_service::{NewService, Service}; use futures::future::{ok, FutureResult}; use futures::{try_ready, Async, Future, IntoFuture, Poll}; @@ -331,15 +331,15 @@ where ExtractResponse { fut, + req, fut_s: None, - req: Some((req, payload)), service: self.service.clone(), } } } pub struct ExtractResponse { - req: Option<(HttpRequest, Payload)>, + req: HttpRequest, service: S, fut: ::Future, fut_s: Option, @@ -362,12 +362,11 @@ where } let item = try_ready!(self.fut.poll().map_err(|e| { - let (req, payload) = self.req.take().unwrap(); - let req = ServiceRequest::from_parts(req, payload); + let req = ServiceRequest::new(self.req.clone()); (e.into(), req) })); - self.fut_s = Some(self.service.call((item, self.req.take().unwrap().0))); + self.fut_s = Some(self.service.call((item, self.req.clone()))); self.poll() } } diff --git a/src/lib.rs b/src/lib.rs index b578d87c9..f84dbd5be 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -151,6 +151,7 @@ pub mod dev { }; pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; pub use actix_server::Server; + pub use actix_service::{Service, Transform}; pub(crate) fn insert_slash(path: &str) -> String { let mut path = path.to_owned(); diff --git a/src/request.rs b/src/request.rs index 7b3ab04a2..07aac8cf7 100644 --- a/src/request.rs +++ b/src/request.rs @@ -20,6 +20,7 @@ pub struct HttpRequest(pub(crate) Rc); pub(crate) struct HttpRequestInner { pub(crate) head: Message, pub(crate) path: Path, + pub(crate) payload: Payload, pub(crate) app_data: Rc, rmap: Rc, config: AppConfig, @@ -31,6 +32,7 @@ impl HttpRequest { pub(crate) fn new( path: Path, head: Message, + payload: Payload, rmap: Rc, config: AppConfig, app_data: Rc, @@ -39,6 +41,7 @@ impl HttpRequest { HttpRequest(Rc::new(HttpRequestInner { head, path, + payload, rmap, config, app_data, @@ -325,6 +328,10 @@ impl HttpRequestPool { None } } + + pub(crate) fn clear(&self) { + self.0.borrow_mut().clear() + } } #[cfg(test)] diff --git a/src/resource.rs b/src/resource.rs index 7f76e0f5c..ad08a15ff 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -606,7 +606,7 @@ mod tests { use crate::http::{header, HeaderValue, Method, StatusCode}; use crate::service::{ServiceRequest, ServiceResponse}; use crate::test::{call_service, init_service, TestRequest}; - use crate::{web, App, Error, HttpResponse}; + use crate::{guard, web, App, Error, HttpResponse}; fn md( req: ServiceRequest, @@ -723,4 +723,45 @@ mod tests { let resp = call_service(&mut srv, req); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } + + #[test] + fn test_resource_guards() { + let mut srv = init_service( + App::new() + .service( + web::resource("/test/{p}") + .guard(guard::Get()) + .to(|| HttpResponse::Ok()), + ) + .service( + web::resource("/test/{p}") + .guard(guard::Put()) + .to(|| HttpResponse::Created()), + ) + .service( + web::resource("/test/{p}") + .guard(guard::Delete()) + .to(|| HttpResponse::NoContent()), + ), + ); + + let req = TestRequest::with_uri("/test/it") + .method(Method::GET) + .to_request(); + let resp = call_service(&mut srv, req); + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/test/it") + .method(Method::PUT) + .to_request(); + let resp = call_service(&mut srv, req); + assert_eq!(resp.status(), StatusCode::CREATED); + + let req = TestRequest::with_uri("/test/it") + .method(Method::DELETE) + .to_request(); + let resp = call_service(&mut srv, req); + assert_eq!(resp.status(), StatusCode::NO_CONTENT); + } + } diff --git a/src/server.rs b/src/server.rs index 3cb139976..353f29ba9 100644 --- a/src/server.rs +++ b/src/server.rs @@ -64,7 +64,7 @@ where config: Arc>, backlog: i32, sockets: Vec, - builder: Option, + builder: ServerBuilder, _t: PhantomData<(S, B)>, } @@ -91,7 +91,7 @@ where })), backlog: 1024, sockets: Vec::new(), - builder: Some(ServerBuilder::default()), + builder: ServerBuilder::default(), _t: PhantomData, } } @@ -101,7 +101,7 @@ where /// By default http server uses number of available logical cpu as threads /// count. pub fn workers(mut self, num: usize) -> Self { - self.builder = Some(self.builder.take().unwrap().workers(num)); + self.builder = self.builder.workers(num); self } @@ -117,7 +117,7 @@ where /// This method should be called before `bind()` method call. pub fn backlog(mut self, backlog: i32) -> Self { self.backlog = backlog; - self.builder = Some(self.builder.take().unwrap().backlog(backlog)); + self.builder = self.builder.backlog(backlog); self } @@ -128,7 +128,7 @@ where /// /// By default max connections is set to a 25k. pub fn maxconn(mut self, num: usize) -> Self { - self.builder = Some(self.builder.take().unwrap().maxconn(num)); + self.builder = self.builder.maxconn(num); self } @@ -139,7 +139,7 @@ where /// /// By default max connections is set to a 256. pub fn maxconnrate(mut self, num: usize) -> Self { - self.builder = Some(self.builder.take().unwrap().maxconnrate(num)); + self.builder = self.builder.maxconnrate(num); self } @@ -190,13 +190,13 @@ where /// Stop actix system. pub fn system_exit(mut self) -> Self { - self.builder = Some(self.builder.take().unwrap().system_exit()); + self.builder = self.builder.system_exit(); self } /// Disable signal handling pub fn disable_signals(mut self) -> Self { - self.builder = Some(self.builder.take().unwrap().disable_signals()); + self.builder = self.builder.disable_signals(); self } @@ -207,8 +207,8 @@ where /// dropped. /// /// By default shutdown timeout sets to 30 seconds. - pub fn shutdown_timeout(mut self, sec: u16) -> Self { - self.builder = Some(self.builder.take().unwrap().shutdown_timeout(sec)); + pub fn shutdown_timeout(mut self, sec: u64) -> Self { + self.builder = self.builder.shutdown_timeout(sec); self } @@ -240,7 +240,7 @@ where scheme: "http", }); - self.builder = Some(self.builder.take().unwrap().listen( + self.builder = self.builder.listen( format!("actix-web-service-{}", addr), lst, move || { @@ -250,8 +250,7 @@ where .client_timeout(c.client_timeout) .finish(factory()) }, - )?); - + )?; Ok(self) } @@ -260,20 +259,19 @@ where /// /// This method sets alpn protocols to "h2" and "http/1.1" pub fn listen_ssl( - mut self, + self, lst: net::TcpListener, builder: SslAcceptorBuilder, ) -> io::Result { - self.listen_ssl_inner(lst, openssl_acceptor(builder)?)?; - Ok(self) + self.listen_ssl_inner(lst, openssl_acceptor(builder)?) } #[cfg(feature = "ssl")] fn listen_ssl_inner( - &mut self, + mut self, lst: net::TcpListener, acceptor: SslAcceptor, - ) -> io::Result<()> { + ) -> io::Result { use actix_server::ssl::{OpensslAcceptor, SslError}; let acceptor = OpensslAcceptor::new(acceptor); @@ -285,7 +283,7 @@ where scheme: "https", }); - self.builder = Some(self.builder.take().unwrap().listen( + self.builder = self.builder.listen( format!("actix-web-service-{}", addr), lst, move || { @@ -300,8 +298,8 @@ where .map_init_err(|_| ()), ) }, - )?); - Ok(()) + )?; + Ok(self) } #[cfg(feature = "rust-tls")] @@ -309,20 +307,19 @@ where /// /// This method sets alpn protocols to "h2" and "http/1.1" pub fn listen_rustls( - mut self, + self, lst: net::TcpListener, config: RustlsServerConfig, ) -> io::Result { - self.listen_rustls_inner(lst, config)?; - Ok(self) + self.listen_rustls_inner(lst, config) } #[cfg(feature = "rust-tls")] fn listen_rustls_inner( - &mut self, + mut self, lst: net::TcpListener, mut config: RustlsServerConfig, - ) -> io::Result<()> { + ) -> io::Result { use actix_server::ssl::{RustlsAcceptor, SslError}; let protos = vec!["h2".to_string().into(), "http/1.1".to_string().into()]; @@ -337,7 +334,7 @@ where scheme: "https", }); - self.builder = Some(self.builder.take().unwrap().listen( + self.builder = self.builder.listen( format!("actix-web-service-{}", addr), lst, move || { @@ -352,8 +349,8 @@ where .map_init_err(|_| ()), ) }, - )?); - Ok(()) + )?; + Ok(self) } /// The socket address to bind @@ -416,7 +413,7 @@ where let acceptor = openssl_acceptor(builder)?; for lst in sockets { - self.listen_ssl_inner(lst, acceptor.clone())?; + self = self.listen_ssl_inner(lst, acceptor.clone())?; } Ok(self) @@ -433,7 +430,7 @@ where ) -> io::Result { let sockets = self.bind2(addr)?; for lst in sockets { - self.listen_rustls_inner(lst, config.clone())?; + self = self.listen_rustls_inner(lst, config.clone())?; } Ok(self) } @@ -473,8 +470,8 @@ where /// sys.run() // <- Run actix system, this method starts all async processes /// } /// ``` - pub fn start(mut self) -> Server { - self.builder.take().unwrap().start() + pub fn start(self) -> Server { + self.builder.start() } /// Spawn new thread and start listening for incoming connections. diff --git a/src/service.rs b/src/service.rs index eee8b0adb..722813a9f 100644 --- a/src/service.rs +++ b/src/service.rs @@ -50,45 +50,46 @@ where } } -pub struct ServiceRequest { - req: HttpRequest, - payload: Payload, -} +/// An service http request +/// +/// ServiceRequest allows mutable access to request's internal structures +pub struct ServiceRequest(HttpRequest); impl ServiceRequest { - /// Construct service request from parts - pub(crate) fn from_parts(req: HttpRequest, payload: Payload) -> Self { - ServiceRequest { req, payload } + /// Construct service request + pub(crate) fn new(req: HttpRequest) -> Self { + ServiceRequest(req) } /// Deconstruct request into parts - pub fn into_parts(self) -> (HttpRequest, Payload) { - (self.req, self.payload) + pub fn into_parts(mut self) -> (HttpRequest, Payload) { + let pl = Rc::get_mut(&mut (self.0).0).unwrap().payload.take(); + (self.0, pl) } /// Create service response #[inline] pub fn into_response>>(self, res: R) -> ServiceResponse { - ServiceResponse::new(self.req, res.into()) + ServiceResponse::new(self.0, res.into()) } /// Create service response for error #[inline] pub fn error_response>(self, err: E) -> ServiceResponse { let res: Response = err.into().into(); - ServiceResponse::new(self.req, res.into_body()) + ServiceResponse::new(self.0, res.into_body()) } /// This method returns reference to the request head #[inline] pub fn head(&self) -> &RequestHead { - &self.req.head() + &self.0.head() } /// This method returns reference to the request head #[inline] pub fn head_mut(&mut self) -> &mut RequestHead { - self.req.head_mut() + self.0.head_mut() } /// Request's uri. @@ -164,34 +165,39 @@ impl ServiceRequest { /// access the matched value for that segment. #[inline] pub fn match_info(&self) -> &Path { - self.req.match_info() + self.0.match_info() } #[inline] pub fn match_info_mut(&mut self) -> &mut Path { - self.req.match_info_mut() + self.0.match_info_mut() } /// Service configuration #[inline] pub fn app_config(&self) -> &AppConfig { - self.req.app_config() + self.0.app_config() } /// Get an application data stored with `App::data()` method during /// application configuration. pub fn app_data(&self) -> Option> { - if let Some(st) = self.req.0.app_data.get::>() { + if let Some(st) = (self.0).0.app_data.get::>() { Some(st.clone()) } else { None } } + /// Set request payload. + pub fn set_payload(&mut self, payload: Payload) { + Rc::get_mut(&mut (self.0).0).unwrap().payload = payload; + } + #[doc(hidden)] /// Set new app data container pub fn set_data_container(&mut self, extensions: Rc) { - Rc::get_mut(&mut self.req.0).unwrap().app_data = extensions; + Rc::get_mut(&mut (self.0).0).unwrap().app_data = extensions; } } @@ -213,18 +219,18 @@ impl HttpMessage for ServiceRequest { /// Request extensions #[inline] fn extensions(&self) -> Ref { - self.req.extensions() + self.0.extensions() } /// Mutable reference to a the request's extensions #[inline] fn extensions_mut(&self) -> RefMut { - self.req.extensions_mut() + self.0.extensions_mut() } #[inline] fn take_payload(&mut self) -> Payload { - std::mem::replace(&mut self.payload, Payload::None) + Rc::get_mut(&mut (self.0).0).unwrap().payload.take() } } diff --git a/src/test.rs b/src/test.rs index 3b3aac67d..89c1a1268 100644 --- a/src/test.rs +++ b/src/test.rs @@ -2,7 +2,7 @@ use std::cell::RefCell; use std::rc::Rc; -use actix_http::http::header::{Header, HeaderName, IntoHeaderValue}; +use actix_http::http::header::{ContentType, Header, HeaderName, IntoHeaderValue}; use actix_http::http::{HttpTryFrom, Method, StatusCode, Uri, Version}; use actix_http::test::TestRequest as HttpTestRequest; use actix_http::{cookie::Cookie, Extensions, Request}; @@ -14,6 +14,7 @@ use bytes::{Bytes, BytesMut}; use futures::future::{lazy, ok, Future, IntoFuture}; use futures::Stream; use serde::de::DeserializeOwned; +use serde::Serialize; use serde_json; pub use actix_http::test::TestBuffer; @@ -248,7 +249,7 @@ where /// .header(header::CONTENT_TYPE, "application/json") /// .to_request(); /// -/// let resp = call_service(&mut srv, req); +/// let resp = test::call_service(&mut app, req); /// let result = test::read_body(resp); /// assert_eq!(result, Bytes::from_static(b"welcome!")); /// } @@ -477,6 +478,16 @@ impl TestRequest { self } + /// Serialize `data` to JSON and set it as the request payload. The `Content-Type` header is + /// set to `application/json`. + pub fn set_json(mut self, data: &T) -> Self { + let bytes = + serde_json::to_string(data).expect("Failed to serialize test data to json"); + self.req.set_payload(bytes); + self.req.set(ContentType::json()); + self + } + /// Set application data. This is equivalent of `App::data()` method /// for testing purpose. pub fn data(mut self, data: T) -> Self { @@ -501,16 +512,15 @@ impl TestRequest { let (head, payload) = self.req.finish().into_parts(); self.path.get_mut().update(&head.uri); - let req = HttpRequest::new( + ServiceRequest::new(HttpRequest::new( self.path, head, + payload, Rc::new(self.rmap), AppConfig::new(self.config), Rc::new(self.app_data), HttpRequestPool::create(), - ); - - ServiceRequest::from_parts(req, payload) + )) } /// Complete request creation and generate `ServiceResponse` instance @@ -520,12 +530,13 @@ impl TestRequest { /// Complete request creation and generate `HttpRequest` instance pub fn to_http_request(mut self) -> HttpRequest { - let (head, _) = self.req.finish().into_parts(); + let (head, payload) = self.req.finish().into_parts(); self.path.get_mut().update(&head.uri); HttpRequest::new( self.path, head, + payload, Rc::new(self.rmap), AppConfig::new(self.config), Rc::new(self.app_data), @@ -541,6 +552,7 @@ impl TestRequest { let req = HttpRequest::new( self.path, head, + Payload::None, Rc::new(self.rmap), AppConfig::new(self.config), Rc::new(self.app_data), @@ -553,6 +565,7 @@ impl TestRequest { #[cfg(test)] mod tests { + use actix_http::httpmessage::HttpMessage; use serde::{Deserialize, Serialize}; use std::time::SystemTime; @@ -657,6 +670,31 @@ mod tests { assert_eq!(&result.id, "12345"); } + #[test] + fn test_request_response_json() { + let mut app = init_service(App::new().service(web::resource("/people").route( + web::post().to(|person: web::Json| { + HttpResponse::Ok().json(person.into_inner()) + }), + ))); + + let payload = Person { + id: "12345".to_string(), + name: "User name".to_string(), + }; + + let req = TestRequest::post() + .uri("/people") + .set_json(&payload) + .to_request(); + + assert_eq!(req.content_type(), "application/json"); + + let result: Person = read_response_json(&mut app, req); + assert_eq!(&result.id, "12345"); + assert_eq!(&result.name, "User name"); + } + #[test] fn test_async_with_block() { fn async_with_block() -> impl Future { diff --git a/src/types/query.rs b/src/types/query.rs index 24225dad2..2c07edfbd 100644 --- a/src/types/query.rs +++ b/src/types/query.rs @@ -8,9 +8,9 @@ use serde::de; use serde_urlencoded; use crate::dev::Payload; +use crate::error::QueryPayloadError; use crate::extract::FromRequest; use crate::request::HttpRequest; -use crate::error::QueryPayloadError; #[derive(PartialEq, Eq, PartialOrd, Ord)] /// Extract typed information from from the request's query. @@ -52,6 +52,16 @@ impl Query { pub fn into_inner(self) -> T { self.0 } + + /// Get query parameters from the path + pub fn from_query(query_str: &str) -> Result + where + T: de::DeserializeOwned, + { + serde_urlencoded::from_str::(query_str) + .map(|val| Ok(Query(val))) + .unwrap_or_else(move |e| Err(QueryPayloadError::Deserialize(e))) + } } impl ops::Deref for Query { @@ -188,8 +198,8 @@ pub struct QueryConfig { impl QueryConfig { /// Set custom error handler pub fn error_handler(mut self, f: F) -> Self - where - F: Fn(QueryPayloadError, &HttpRequest) -> Error + Send + Sync + 'static, + where + F: Fn(QueryPayloadError, &HttpRequest) -> Error + Send + Sync + 'static, { self.ehandler = Some(Arc::new(f)); self @@ -198,21 +208,19 @@ impl QueryConfig { impl Default for QueryConfig { fn default() -> Self { - QueryConfig { - ehandler: None, - } + QueryConfig { ehandler: None } } } #[cfg(test)] mod tests { + use actix_http::http::StatusCode; use derive_more::Display; use serde_derive::Deserialize; - use actix_http::http::StatusCode; use super::*; - use crate::test::TestRequest; use crate::error::InternalError; + use crate::test::TestRequest; use crate::HttpResponse; #[derive(Deserialize, Debug, Display)] @@ -220,6 +228,22 @@ mod tests { id: String, } + #[test] + fn test_service_request_extract() { + let req = TestRequest::with_uri("/name/user1/").to_srv_request(); + assert!(Query::::from_query(&req.query_string()).is_err()); + + let req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request(); + let mut s = Query::::from_query(&req.query_string()).unwrap(); + + assert_eq!(s.id, "test"); + assert_eq!(format!("{}, {:?}", s, s), "test, Id { id: \"test\" }"); + + s.id = "test1".to_string(); + let s = s.into_inner(); + assert_eq!(s.id, "test1"); + } + #[test] fn test_request_extract() { let req = TestRequest::with_uri("/name/user1/").to_srv_request(); @@ -244,12 +268,20 @@ mod tests { .data(QueryConfig::default().error_handler(|e, _| { let resp = HttpResponse::UnprocessableEntity().finish(); InternalError::from_response(e, resp).into() - })).to_srv_request(); + })) + .to_srv_request(); let (req, mut pl) = req.into_parts(); let query = Query::::from_request(&req, &mut pl); assert!(query.is_err()); - assert_eq!(query.unwrap_err().as_response_error().error_response().status(), StatusCode::UNPROCESSABLE_ENTITY); + assert_eq!( + query + .unwrap_err() + .as_response_error() + .error_response() + .status(), + StatusCode::UNPROCESSABLE_ENTITY + ); } }