From 6482fa2fba4acd8c3a83da36aab206433b6aad80 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 2 Jul 2020 21:20:28 -0700 Subject: [PATCH] Rework 'local' module. Add 'LocalResponse' methods. This completes the 'local' blocking client implementation. --- core/lib/src/local/asynchronous/client.rs | 54 ++- core/lib/src/local/asynchronous/mod.rs | 6 +- core/lib/src/local/asynchronous/request.rs | 49 +- core/lib/src/local/asynchronous/response.rs | 139 +++++- core/lib/src/local/blocking/client.rs | 40 +- core/lib/src/local/blocking/mod.rs | 8 +- core/lib/src/local/blocking/request.rs | 22 +- core/lib/src/local/blocking/response.rs | 72 ++- core/lib/src/local/client.rs | 299 ++++++------ core/lib/src/local/mod.rs | 228 +++++---- core/lib/src/local/request.rs | 498 +++++++++----------- core/lib/src/local/response.rs | 135 +++--- core/lib/src/request/request.rs | 2 +- core/lib/src/rocket.rs | 123 ++--- examples/session/src/tests.rs | 5 +- 15 files changed, 939 insertions(+), 741 deletions(-) diff --git a/core/lib/src/local/asynchronous/client.rs b/core/lib/src/local/asynchronous/client.rs index ca3f9b52..72e01010 100644 --- a/core/lib/src/local/asynchronous/client.rs +++ b/core/lib/src/local/asynchronous/client.rs @@ -1,21 +1,26 @@ use std::sync::RwLock; use std::borrow::Cow; -use crate::local::asynchronous::LocalRequest; +use crate::local::asynchronous::{LocalRequest, LocalResponse}; use crate::rocket::{Rocket, Cargo}; use crate::http::{Method, private::CookieJar}; use crate::error::LaunchError; -struct_client! { [ +/// An `async` client to construct and dispatch local requests. /// -/// ### Synchronization +/// For details, see [the top-level documentation](../index.html#client). +/// For the `blocking` version, see +/// [`blocking::Client`](crate::local::blocking::Client). /// -/// While `Client` implements `Sync`, using it in a multithreaded environment -/// while tracking cookies can result in surprising, non-deterministic behavior. -/// This is because while cookie modifications are serialized, the exact -/// ordering depends on when requests are dispatched. Specifically, when cookie -/// tracking is enabled, all request dispatches are serialized, which in-turn -/// serializes modifications to the internally tracked cookies. +/// ## Multithreaded Syncronization Pitfalls +/// +/// Unlike its [`blocking`](crate::local::blocking) variant, this `async` `Client` +/// implements `Sync`. However, using it in a multithreaded environment while +/// tracking cookies can result in surprising, non-deterministic behavior. This +/// is because while cookie modifications are serialized, the exact ordering +/// depends on when requests are dispatched. Specifically, when cookie tracking +/// is enabled, all request dispatches are serialized, which in-turn serializes +/// modifications to the internally tracked cookies. /// /// If possible, refrain from sharing a single instance of `Client` across /// multiple threads. Instead, prefer to create a unique instance of `Client` @@ -26,7 +31,7 @@ struct_client! { [ /// ## Example /// /// The following snippet creates a `Client` from a `Rocket` instance and -/// dispatches a local request to `POST /` with a body of `Hello, world!`. +/// dispatches a local `POST /` request with a body of `Hello, world!`. /// /// ```rust /// use rocket::local::asynchronous::Client; @@ -36,14 +41,13 @@ struct_client! { [ /// let client = Client::new(rocket).await.expect("valid rocket"); /// let response = client.post("/") /// .body("Hello, world!") -/// .dispatch().await; +/// .dispatch() +/// .await; /// # }); /// ``` -] pub struct Client { cargo: Cargo, - pub(crate) cookies: Option>, -} + pub(in super) cookies: Option>, } impl Client { @@ -61,13 +65,17 @@ impl Client { Ok(Client { cargo: rocket.into_cargo().await, cookies }) } + // WARNING: This is unstable! Do not use this method outside of Rocket! #[doc(hidden)] - /// WARNING: This is unstable! Do not use this method outside of Rocket! - pub fn _test T + Send>(f: F) -> T { + pub fn _test(f: F) -> T + where F: FnOnce(&Self, LocalRequest<'_>, LocalResponse<'_>) -> T + Send + { crate::async_test(async { let rocket = crate::ignite(); let client = Client::new(rocket).await.expect("valid rocket"); - f(client) + let request = client.get("/"); + let response = request.clone().dispatch().await; + f(&client, request, response) }) } @@ -82,18 +90,16 @@ impl Client { { LocalRequest::new(self, method, uri.into()) } -} -impl_client!("use rocket::local::asynchronous::Client;" @async await Client); + // Generates the public API methods, which call the private methods above. + pub_client_impl!("use rocket::local::asynchronous::Client;" @async await); +} #[cfg(test)] mod test { - use super::Client; - - fn assert_sync_send() {} - #[test] fn test_local_client_impl_send_sync() { - assert_sync_send::(); + fn assert_sync_send() {} + assert_sync_send::(); } } diff --git a/core/lib/src/local/asynchronous/mod.rs b/core/lib/src/local/asynchronous/mod.rs index ddb737fe..dc7dbee1 100644 --- a/core/lib/src/local/asynchronous/mod.rs +++ b/core/lib/src/local/asynchronous/mod.rs @@ -1,8 +1,10 @@ -//! Structures for asynchronous local dispatching of requests, primarily for -//! testing. +//! Asynchronous local dispatching of requests. //! //! This module contains the `asynchronous` variant of the `local` API: it can //! be used with `#[rocket::async_test]` or another asynchronous test harness. +//! For the blocking variant, see [`blocking`](super::blocking). +//! +//! See the [top-level documentation](super) for more usage details. mod client; mod request; diff --git a/core/lib/src/local/asynchronous/request.rs b/core/lib/src/local/asynchronous/request.rs index ed06f640..373cd9af 100644 --- a/core/lib/src/local/asynchronous/request.rs +++ b/core/lib/src/local/asynchronous/request.rs @@ -5,11 +5,14 @@ use crate::http::{Status, Method, uri::Origin, ext::IntoOwned}; use super::{Client, LocalResponse}; -struct_request! { [ +/// An `async` local request as returned by [`Client`](super::Client). +/// +/// For details, see [the top-level documentation](../index.html#localrequest). +/// /// ## Example /// -/// The following snippet uses the available builder methods to construct a -/// `POST` request to `/` with a JSON body: +/// The following snippet uses the available builder methods to construct and +/// dispatch a `POST` request to `/` with a JSON body: /// /// ```rust /// use rocket::local::asynchronous::{Client, LocalRequest}; @@ -22,16 +25,16 @@ struct_request! { [ /// .remote("127.0.0.1:8000".parse().unwrap()) /// .cookie(Cookie::new("name", "value")) /// .body(r#"{ "value": 42 }"#); +/// +/// let response = req.dispatch().await; /// # }); /// ``` -] pub struct LocalRequest<'c> { client: &'c Client, request: Request<'c>, data: Vec, uri: Cow<'c, str>, } -} impl<'c> LocalRequest<'c> { pub(crate) fn new( @@ -65,33 +68,26 @@ impl<'c> LocalRequest<'c> { &mut self.data } - // This method should _never_ be publicly exposed! - #[inline(always)] - fn long_lived_request<'a>(&mut self) -> &'a mut Request<'c> { - // FIXME: Whatever. I'll kill this. - unsafe { &mut *(&mut self.request as *mut _) } - } - // Performs the actual dispatch. - // TODO.async: @jebrosen suspects there might be actual UB in here after all, - // and now we just went and mixed threads into it async fn _dispatch(mut self) -> LocalResponse<'c> { // First, validate the URI, returning an error response (generated from // an error catcher) immediately if it's invalid. + let rocket = self.client.rocket(); if let Ok(uri) = Origin::parse(&self.uri) { self.request.set_uri(uri.into_owned()); } else { error!("Malformed request URI: {}", self.uri); - let res = self.client.rocket() - .handle_error(Status::BadRequest, self.long_lived_request()); - - return LocalResponse { _request: self.request, inner: res.await }; + return LocalResponse::new(self.request, move |req| { + rocket.handle_error(Status::BadRequest, req) + }).await } // Actually dispatch the request. - let response = self.client.rocket() - .dispatch(self.long_lived_request(), Data::local(self.data)) - .await; + let data = Data::local(self.data); + let token = rocket.preprocess_request(&mut self.request, &data).await; + let response = LocalResponse::new(self.request, move |request| { + rocket.dispatch(token, request, data) + }).await; // If the client is tracking cookies, updates the internal cookie jar // with the changes reflected by `response`. @@ -110,8 +106,11 @@ impl<'c> LocalRequest<'c> { } } - LocalResponse { _request: self.request, inner: response } + response } + + pub_request_impl!("# use rocket::local::asynchronous::Client; + use rocket::local::asynchronous::LocalRequest;" async await); } impl<'c> Clone for LocalRequest<'c> { @@ -125,4 +124,8 @@ impl<'c> Clone for LocalRequest<'c> { } } -impl_request!("use rocket::local::asynchronous::Client;" @async await LocalRequest); +impl std::fmt::Debug for LocalRequest<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self._request().fmt(f) + } +} diff --git a/core/lib/src/local/asynchronous/response.rs b/core/lib/src/local/asynchronous/response.rs index 22b66c0b..c23a841d 100644 --- a/core/lib/src/local/asynchronous/response.rs +++ b/core/lib/src/local/asynchronous/response.rs @@ -1,24 +1,133 @@ +use std::io; +use std::future::Future; +use std::{pin::Pin, task::{Context, Poll}}; + +use tokio::io::AsyncRead; + use crate::{Request, Response}; -struct_response! { +/// An `async` response from a dispatched [`LocalRequest`](super::LocalRequest). +/// +/// This `LocalResponse` implements [`tokio::io::AsyncRead`]. As such, if +/// [`into_string()`](LocalResponse::into_string()) and +/// [`into_bytes()`](LocalResponse::into_bytes()) do not suffice, the response's +/// body can be read directly: +/// +/// ```rust +/// # #[macro_use] extern crate rocket; +/// use std::io; +/// +/// use rocket::local::asynchronous::Client; +/// use rocket::tokio::io::AsyncReadExt; +/// use rocket::http::Status; +/// +/// #[get("/")] +/// fn hello_world() -> &'static str { +/// "Hello, world!" +/// } +/// +/// # /* +/// #[launch] +/// # */ +/// fn rocket() -> rocket::Rocket { +/// rocket::ignite().mount("/", routes![hello_world]) +/// } +/// +/// # async fn read_body_manually() -> io::Result<()> { +/// // Dispatch a `GET /` request. +/// let client = Client::new(rocket()).await.expect("valid rocket"); +/// let mut response = client.get("/").dispatch().await; +/// +/// // Check metadata validity. +/// assert_eq!(response.status(), Status::Ok); +/// assert_eq!(response.body().unwrap().known_size(), Some(13)); +/// +/// // Read 10 bytes of the body. Note: in reality, we'd use `into_string()`. +/// let mut buffer = [0; 10]; +/// response.read(&mut buffer).await?; +/// assert_eq!(buffer, "Hello, wor".as_bytes()); +/// # Ok(()) +/// # } +/// # rocket::async_test(read_body_manually()).expect("read okay"); +/// ``` +/// +/// For more, see [the top-level documentation](../index.html#localresponse). pub struct LocalResponse<'c> { - pub(in super) _request: Request<'c>, - pub(in super) inner: Response<'c>, -} + _request: Box>, + response: Response<'c>, } impl<'c> LocalResponse<'c> { - fn _response(&self) -> &Response<'c> { - &self.inner - } - - pub(crate) async fn _into_string(mut self) -> Option { - self.inner.body_string().await - } - - pub(crate) async fn _into_bytes(mut self) -> Option> { - self.inner.body_bytes().await + pub(crate) fn new(req: Request<'c>, f: F) -> impl Future> + where F: FnOnce(&'c Request<'c>) -> O + Send, + O: Future> + Send + { + // `LocalResponse` is a self-referential structure. In particular, + // `inner` can refer to `_request` and its contents. As such, we must + // 1) Ensure `Request` has a stable address. + // + // This is done by `Box`ing the `Request`, using only the stable + // address thereafter. + // + // 2) Ensure no refs to `Request` or its contents leak with a lifetime + // extending beyond that of `&self`. + // + // We have no methods that return an `&Request`. However, we must + // also ensure that `Response` doesn't leak any such references. To + // do so, we don't expose the `Response` directly in any way; + // otherwise, methods like `.headers()` could, in conjuction with + // particular crafted `Responder`s, potentially be used to obtain a + // reference to contents of `Request`. All methods, instead, return + // references bounded by `self`. This is easily verified by nothing + // that 1) `LocalResponse` fields are private, and 2) all `impl`s + // of `LocalResponse` aside from this method abstract the lifetime + // away as `'_`, ensuring it is not used for any output value. + let boxed_req = Box::new(req); + let request: &'c Request<'c> = unsafe { &*(&*boxed_req as *const _) }; + async move { + LocalResponse { + _request: boxed_req, + response: f(request).await + } + } } } -impl_response!("use rocket::local::asynchronous::Client;" @async await LocalResponse); +impl LocalResponse<'_> { + pub(crate) fn _response(&self) -> &Response<'_> { + &self.response + } + + pub(crate) async fn _into_string(mut self) -> Option { + self.response.body_string().await + } + + pub(crate) async fn _into_bytes(mut self) -> Option> { + self.response.body_bytes().await + } + + // Generates the public API methods, which call the private methods above. + pub_response_impl!("# use rocket::local::asynchronous::Client; + use rocket::local::asynchronous::LocalResponse;" async await); +} + +impl AsyncRead for LocalResponse<'_> { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut [u8] + ) -> Poll> { + let body = match self.response.body_mut() { + Some(body) => body, + _ => return Poll::Ready(Ok(0)) + }; + + Pin::new(body.as_reader()).poll_read(cx, buf) + } +} + +impl std::fmt::Debug for LocalResponse<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self._response().fmt(f) + } +} diff --git a/core/lib/src/local/blocking/client.rs b/core/lib/src/local/blocking/client.rs index 7c296066..6fe756bf 100644 --- a/core/lib/src/local/blocking/client.rs +++ b/core/lib/src/local/blocking/client.rs @@ -1,16 +1,20 @@ use std::borrow::Cow; use std::cell::RefCell; + use crate::error::LaunchError; use crate::http::Method; -use crate::local::{asynchronous, blocking::LocalRequest}; +use crate::local::{asynchronous, blocking::{LocalRequest, LocalResponse}}; use crate::rocket::{Rocket, Cargo}; -struct_client! { [ +/// A `blocking` client to construct and dispatch local requests. +/// +/// For details, see [the top-level documentation](../index.html#client). For +/// the `async` version, see [`asynchronous::Client`]. /// /// ## Example /// /// The following snippet creates a `Client` from a `Rocket` instance and -/// dispatches a local request to `POST /` with a body of `Hello, world!`. +/// dispatches a local `POST /` request with a body of `Hello, world!`. /// /// ```rust /// use rocket::local::blocking::Client; @@ -21,12 +25,10 @@ struct_client! { [ /// .body("Hello, world!") /// .dispatch(); /// ``` -] pub struct Client { pub(crate) inner: asynchronous::Client, runtime: RefCell, } -} impl Client { fn _new(rocket: Rocket, tracked: bool) -> Result { @@ -41,12 +43,16 @@ impl Client { Ok(Self { inner, runtime: RefCell::new(runtime) }) } + // WARNING: This is unstable! Do not use this method outside of Rocket! #[doc(hidden)] - /// WARNING: This is unstable! Do not use this method outside of Rocket! - pub fn _test T + Send>(f: F) -> T { + pub fn _test(f: F) -> T + where F: FnOnce(&Self, LocalRequest<'_>, LocalResponse<'_>) -> T + Send + { let rocket = crate::ignite(); let client = Client::new(rocket).expect("valid rocket"); - f(client) + let request = client.get("/"); + let response = request.clone().dispatch(); + f(&client, request, response) } #[inline(always)] @@ -71,23 +77,13 @@ impl Client { { LocalRequest::new(self, method, uri.into()) } -} -impl_client!("use rocket::local::blocking::Client;" Client); + // Generates the public API methods, which call the private methods above. + pub_client_impl!("use rocket::local::blocking::Client;"); +} #[cfg(doctest)] mod doctest { - /// ```no_run - /// // Just to ensure we get the path/form right in the following tests. - /// use rocket::local::blocking::Client; - /// - /// fn test() {}; - /// test::(); - /// - /// fn is_send() {}; - /// is_send::(); - /// ``` - /// /// ```compile_fail /// use rocket::local::blocking::Client; /// @@ -95,5 +91,5 @@ mod doctest { /// not_sync::(); /// ``` #[allow(dead_code)] - fn test_not_sync_or_send() {} + fn test_not_sync() {} } diff --git a/core/lib/src/local/blocking/mod.rs b/core/lib/src/local/blocking/mod.rs index 3f78c47c..8195e697 100644 --- a/core/lib/src/local/blocking/mod.rs +++ b/core/lib/src/local/blocking/mod.rs @@ -1,9 +1,11 @@ -//! Structures for blocking local dispatching of requests, primarily for -//! testing. +//! Blocking local dispatching of requests. //! //! This module contains the `blocking` variant of the `local` API: it can be //! used in Rust's synchronous `#[test]` harness. This is accomplished by -//! starting and running an interal asynchronous Runtime as needed. +//! starting and running an interal asynchronous Runtime as needed. For the +//! asynchronous variant, see [`asynchronous`](super::asynchronous). +//! +//! See the [top-level documentation](super) for more usage details. mod client; mod request; diff --git a/core/lib/src/local/blocking/request.rs b/core/lib/src/local/blocking/request.rs index c0bf19c5..45efd3bd 100644 --- a/core/lib/src/local/blocking/request.rs +++ b/core/lib/src/local/blocking/request.rs @@ -4,11 +4,14 @@ use crate::{Request, http::Method, local::asynchronous}; use super::{Client, LocalResponse}; -struct_request! { [ +/// A `blocking` local request as returned by [`Client`](super::Client). +/// +/// For details, see [the top-level documentation](../index.html#localrequest). +/// /// ## Example /// -/// The following snippet uses the available builder methods to construct a -/// `POST` request to `/` with a JSON body: +/// The following snippet uses the available builder methods to construct and +/// dispatch a `POST` request to `/` with a JSON body: /// /// ```rust /// use rocket::local::blocking::{Client, LocalRequest}; @@ -20,14 +23,14 @@ struct_request! { [ /// .remote("127.0.0.1:8000".parse().unwrap()) /// .cookie(Cookie::new("name", "value")) /// .body(r#"{ "value": 42 }"#); +/// +/// let response = req.dispatch(); /// ``` -] #[derive(Clone)] pub struct LocalRequest<'c> { inner: asynchronous::LocalRequest<'c>, client: &'c Client, } -} impl<'c> LocalRequest<'c> { #[inline] @@ -58,6 +61,13 @@ impl<'c> LocalRequest<'c> { let inner = self.client.block_on(self.inner.dispatch()); LocalResponse { inner, client: self.client } } + + pub_request_impl!("# use rocket::local::blocking::Client; + use rocket::local::blocking::LocalRequest;"); } -impl_request!("use rocket::local::blocking::Client;" LocalRequest); +impl std::fmt::Debug for LocalRequest<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self._request().fmt(f) + } +} diff --git a/core/lib/src/local/blocking/response.rs b/core/lib/src/local/blocking/response.rs index 79aa82fa..68dd9fe0 100644 --- a/core/lib/src/local/blocking/response.rs +++ b/core/lib/src/local/blocking/response.rs @@ -1,17 +1,63 @@ +use std::io; +use tokio::io::AsyncReadExt; + use crate::{Response, local::asynchronous}; use super::Client; -struct_response! { +/// A `blocking` response from a dispatched [`LocalRequest`](super::LocalRequest). +/// +/// This `LocalResponse` implements [`io::Read`]. As such, if +/// [`into_string()`](LocalResponse::into_string()) and +/// [`into_bytes()`](LocalResponse::into_bytes()) do not suffice, the response's +/// body can be read directly: +/// +/// ```rust +/// # #[macro_use] extern crate rocket; +/// use std::io::{self, Read}; +/// +/// use rocket::local::blocking::Client; +/// use rocket::http::Status; +/// +/// #[get("/")] +/// fn hello_world() -> &'static str { +/// "Hello, world!" +/// } +/// +/// # /* +/// #[launch] +/// # */ +/// fn rocket() -> rocket::Rocket { +/// rocket::ignite().mount("/", routes![hello_world]) +/// } +/// +/// # fn read_body_manually() -> io::Result<()> { +/// // Dispatch a `GET /` request. +/// let client = Client::new(rocket()).expect("valid rocket"); +/// let mut response = client.get("/").dispatch(); +/// +/// // Check metadata validity. +/// assert_eq!(response.status(), Status::Ok); +/// assert_eq!(response.body().unwrap().known_size(), Some(13)); +/// +/// // Read 10 bytes of the body. Note: in reality, we'd use `into_string()`. +/// let mut buffer = [0; 10]; +/// response.read(&mut buffer)?; +/// assert_eq!(buffer, "Hello, wor".as_bytes()); +/// # Ok(()) +/// # } +/// # read_body_manually().expect("read okay"); +/// ``` +/// +/// For more, see [the top-level documentation](../index.html#localresponse). pub struct LocalResponse<'c> { pub(in super) inner: asynchronous::LocalResponse<'c>, pub(in super) client: &'c Client, } -} -impl<'c> LocalResponse<'c> { - fn _response(&self) -> &Response<'c> { - &*self.inner +impl LocalResponse<'_> { + fn _response(&self) -> &Response<'_> { + &self.inner._response() } fn _into_string(self) -> Option { @@ -21,6 +67,20 @@ impl<'c> LocalResponse<'c> { fn _into_bytes(self) -> Option> { self.client.block_on(self.inner._into_bytes()) } + + // Generates the public API methods, which call the private methods above. + pub_response_impl!("# use rocket::local::blocking::Client; + use rocket::local::blocking::LocalResponse;"); } -impl_response!("use rocket::local::blocking::Client;" LocalResponse); +impl io::Read for LocalResponse<'_> { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.client.block_on(self.inner.read(buf)) + } +} + +impl std::fmt::Debug for LocalResponse<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self._response().fmt(f) + } +} diff --git a/core/lib/src/local/client.rs b/core/lib/src/local/client.rs index 40ea24d1..798486ae 100644 --- a/core/lib/src/local/client.rs +++ b/core/lib/src/local/client.rs @@ -1,41 +1,3 @@ -macro_rules! struct_client { - ([$(#[$attr:meta])*] $item:item) => -{ - /// A structure to construct requests for local dispatching. - /// - /// # Usage - /// - /// A `Client` is constructed via the [`new()`] or [`untracked()`] methods from - /// an already constructed `Rocket` instance. Once a value of `Client` has been - /// constructed, the [`LocalRequest`] constructor methods ([`get()`], [`put()`], - /// [`post()`], and so on) can be used to create a `LocalRequest` for - /// dispatching. - /// - /// See the [top-level documentation](crate::local) for more usage information. - /// - /// ## Cookie Tracking - /// - /// A `Client` constructed using [`new()`] propagates cookie changes made by - /// responses to previously dispatched requests. In other words, if a previously - /// dispatched request resulted in a response that adds a cookie, any future - /// requests will contain that cookie. Similarly, cookies removed by a response - /// won't be propagated further. - /// - /// This is typically the desired mode of operation for a `Client` as it removes - /// the burden of manually tracking cookies. Under some circumstances, however, - /// disabling this tracking may be desired. In these cases, use the - /// [`untracked()`](Client::untracked()) constructor to create a `Client` that - /// _will not_ track cookies. - $(#[$attr])* - /// - /// [`new()`]: #method.new - /// [`untracked()`]: #method.untracked - /// [`get()`]: #method.get - /// [`put()`]: #method.put - /// [`post()`]: #method.post - $item -}} - macro_rules! req_method { ($import:literal, $NAME:literal, $f:ident, $method:expr) => ( req_method!(@ @@ -62,8 +24,8 @@ macro_rules! req_method { /// ```rust,no_run #[doc = $import] /// - /// # Client::_test(|client| { - /// let client: Client = client; + /// # Client::_test(|client, _, _| { + /// let client: &Client = client; #[doc = $use_it] /// # }); /// ``` @@ -76,137 +38,142 @@ macro_rules! req_method { ) } -macro_rules! impl_client { - ($import:literal $(@$prefix:tt $suffix:tt)? $name:ident) => +macro_rules! pub_client_impl { + ($import:literal $(@$prefix:tt $suffix:tt)?) => { - impl $name { - /// Construct a new `Client` from an instance of `Rocket` with cookie - /// tracking. - /// - /// # Cookie Tracking - /// - /// By default, a `Client` propagates cookie changes made by responses - /// to previously dispatched requests. In other words, if a previously - /// dispatched request resulted in a response that adds a cookie, any - /// future requests will contain the new cookies. Similarly, cookies - /// removed by a response won't be propagated further. - /// - /// This is typically the desired mode of operation for a `Client` as it - /// removes the burden of manually tracking cookies. Under some - /// circumstances, however, disabling this tracking may be desired. The - /// [`untracked()`](Client::untracked()) method creates a `Client` that - /// _will not_ track cookies. - /// - /// # Errors - /// - /// If launching the `Rocket` instance would fail, excepting network errors, - /// the `LaunchError` is returned. - /// - /// ```rust,no_run - #[doc = $import] - /// - /// let rocket = rocket::ignite(); - /// let client = Client::new(rocket); - /// ``` - #[inline(always)] - pub $($prefix)? fn new(rocket: Rocket) -> Result { - Self::_new(rocket, true) $(.$suffix)? - } + /// Construct a new `Client` from an instance of `Rocket` with cookie + /// tracking. + /// + /// # Cookie Tracking + /// + /// By default, a `Client` propagates cookie changes made by responses + /// to previously dispatched requests. In other words, if a previously + /// dispatched request resulted in a response that adds a cookie, any + /// future requests will contain the new cookies. Similarly, cookies + /// removed by a response won't be propagated further. + /// + /// This is typically the desired mode of operation for a `Client` as it + /// removes the burden of manually tracking cookies. Under some + /// circumstances, however, disabling this tracking may be desired. The + /// [`untracked()`](Client::untracked()) method creates a `Client` that + /// _will not_ track cookies. + /// + /// # Errors + /// + /// If launching the `Rocket` instance would fail, excepting network errors, + /// the `LaunchError` is returned. + /// + /// ```rust,no_run + #[doc = $import] + /// + /// let rocket = rocket::ignite(); + /// let client = Client::new(rocket); + /// ``` + #[inline(always)] + pub $($prefix)? fn new(rocket: Rocket) -> Result { + Self::_new(rocket, true) $(.$suffix)? + } - /// Construct a new `Client` from an instance of `Rocket` _without_ - /// cookie tracking. - /// - /// # Cookie Tracking - /// - /// Unlike the [`new()`](Client::new()) constructor, a `Client` returned - /// from this method _does not_ automatically propagate cookie changes. - /// - /// # Errors - /// - /// If launching the `Rocket` instance would fail, excepting network - /// errors, the `LaunchError` is returned. - /// - /// ```rust,no_run - #[doc = $import] - /// - /// let rocket = rocket::ignite(); - /// let client = Client::untracked(rocket); - /// ``` - pub $($prefix)? fn untracked(rocket: Rocket) -> Result { - Self::_new(rocket, true) $(.$suffix)? - } + /// Construct a new `Client` from an instance of `Rocket` _without_ + /// cookie tracking. + /// + /// # Cookie Tracking + /// + /// Unlike the [`new()`](Client::new()) constructor, a `Client` returned + /// from this method _does not_ automatically propagate cookie changes. + /// + /// # Errors + /// + /// If launching the `Rocket` instance would fail, excepting network + /// errors, the `LaunchError` is returned. + /// + /// ```rust,no_run + #[doc = $import] + /// + /// let rocket = rocket::ignite(); + /// let client = Client::untracked(rocket); + /// ``` + pub $($prefix)? fn untracked(rocket: Rocket) -> Result { + Self::_new(rocket, true) $(.$suffix)? + } - /// Returns a reference to the `Rocket` this client is creating requests - /// for. - /// - /// # Example - /// - /// ```rust,no_run - #[doc = $import] - /// - /// # Client::_test(|client| { - /// let client: Client = client; - /// let rocket = client.rocket(); - /// # }); - /// ``` - #[inline(always)] - pub fn rocket(&self) -> &Rocket { - &*self._cargo() - } + /// Returns a reference to the `Rocket` this client is creating requests + /// for. + /// + /// # Example + /// + /// ```rust,no_run + #[doc = $import] + /// + /// # Client::_test(|client, _, _| { + /// let client: &Client = client; + /// let rocket = client.rocket(); + /// # }); + /// ``` + #[inline(always)] + pub fn rocket(&self) -> &Rocket { + &*self._cargo() + } - /// Returns a reference to the `Cargo` of the `Rocket` this client is - /// creating requests for. - /// - /// # Example - /// - /// ```rust - #[doc = $import] - /// - /// # Client::_test(|client| { - /// let client: Client = client; - /// let cargo = client.cargo(); - /// # }); - /// ``` - #[inline(always)] - pub fn cargo(&self) -> &Cargo { - self._cargo() - } + /// Returns a reference to the `Cargo` of the `Rocket` this client is + /// creating requests for. + /// + /// # Example + /// + /// ```rust + #[doc = $import] + /// + /// # Client::_test(|client, _, _| { + /// let client: &Client = client; + /// let cargo = client.cargo(); + /// # }); + /// ``` + #[inline(always)] + pub fn cargo(&self) -> &Cargo { + self._cargo() + } - req_method!($import, "GET", get, Method::Get); - req_method!($import, "PUT", put, Method::Put); - req_method!($import, "POST", post, Method::Post); - req_method!($import, "DELETE", delete, Method::Delete); - req_method!($import, "OPTIONS", options, Method::Options); - req_method!($import, "HEAD", head, Method::Head); - req_method!($import, "PATCH", patch, Method::Patch); + req_method!($import, "GET", get, Method::Get); + req_method!($import, "PUT", put, Method::Put); + req_method!($import, "POST", post, Method::Post); + req_method!($import, "DELETE", delete, Method::Delete); + req_method!($import, "OPTIONS", options, Method::Options); + req_method!($import, "HEAD", head, Method::Head); + req_method!($import, "PATCH", patch, Method::Patch); - /// Create a local `GET` request to the URI `uri`. - /// - /// When dispatched, the request will be served by the instance of - /// Rocket within `self`. The request is not dispatched automatically. - /// To actually dispatch the request, call [`LocalRequest::dispatch()`] - /// on the returned request. - /// - /// # Example - /// - /// ```rust,no_run - #[doc = $import] - /// use rocket::http::Method; - /// - /// # Client::_test(|client| { - /// let client: Client = client; - /// client.req(Method::Get, "/hello"); - /// # }); - /// ``` - #[inline(always)] - pub fn req<'c, 'u: 'c, U>( - &'c self, - method: Method, - uri: U - ) -> LocalRequest<'c> - where U: Into> - { - self._req(method, uri) - } + /// Create a local `GET` request to the URI `uri`. + /// + /// When dispatched, the request will be served by the instance of + /// Rocket within `self`. The request is not dispatched automatically. + /// To actually dispatch the request, call [`LocalRequest::dispatch()`] + /// on the returned request. + /// + /// # Example + /// + /// ```rust,no_run + #[doc = $import] + /// use rocket::http::Method; + /// + /// # Client::_test(|client, _, _| { + /// let client: &Client = client; + /// client.req(Method::Get, "/hello"); + /// # }); + /// ``` + #[inline(always)] + pub fn req<'c, 'u: 'c, U>( + &'c self, + method: Method, + uri: U + ) -> LocalRequest<'c> + where U: Into> + { + self._req(method, uri) + } + + #[cfg(test)] + #[allow(dead_code)] + fn _ensure_impls_exist() { + fn is_send() {} + is_send::(); } }} diff --git a/core/lib/src/local/mod.rs b/core/lib/src/local/mod.rs index 4a71e4bf..85df62eb 100644 --- a/core/lib/src/local/mod.rs +++ b/core/lib/src/local/mod.rs @@ -5,81 +5,33 @@ //! and integration test Rocket applications by crafting requests, dispatching //! them, and verifying the response. //! -//! # Usage +//! # Async. vs. Blocking //! -//! This module contains two variants of the local API: [`asynchronous`] and -//! [`blocking`]. The primary difference between the two is in usage: the -//! `asynchronous` API requires an asynchronous test entry point such as -//! `#[rocket::async_test]`, while the `blocking` API can be used with -//! `#[test]`. Additionally, several methods in the `asynchronous` API are -//! `async` and must therefore be `await`ed. +//! This module contains two variants, in its two submodules, of the same local +//! API: [`asynchronous`], and [`blocking`]. As their names imply, the +//! `asynchronous` API is `async`, returning a `Future` for operations that +//! would otherwise block, while `blocking` blocks for the same operations. //! -//! Both APIs include a [`Client`] structure that is used to create -//! [`LocalRequest`] structures that can be dispatched against a given -//! [`Rocket`](crate::Rocket) instance. Usage is straightforward: +//! Unless your request handling requires concurrency to make progress, or +//! you're making use of a `Client` in an environment that necessitates or would +//! benefit from concurrency, you should use the `blocking` set of APIs due +//! their ease-of-use. If your request handling _does_ require concurrency to +//! make progress, for instance by having one handler `await` a response +//! generated from a request to another handler, use the `asynchronous` set of +//! APIs. //! -//! 1. Construct a `Rocket` instance that represents the application. -//! -//! ```rust -//! let rocket = rocket::ignite(); -//! # let _ = rocket; -//! ``` -//! -//! 2. Construct a `Client` using the `Rocket` instance. -//! -//! ```rust -//! # use rocket::local::asynchronous::Client; -//! # let rocket = rocket::ignite(); -//! # rocket::async_test(async { -//! let client = Client::new(rocket).await.expect("valid rocket instance"); -//! # let _ = client; -//! # }); -//! ``` -//! -//! 3. Construct requests using the `Client` instance. -//! -//! ```rust -//! # use rocket::local::asynchronous::Client; -//! # let rocket = rocket::ignite(); -//! # rocket::async_test(async { -//! # let client = Client::new(rocket).await.unwrap(); -//! let req = client.get("/"); -//! # let _ = req; -//! # }); -//! ``` -//! -//! 3. Dispatch the request to retrieve the response. -//! -//! ```rust -//! # use rocket::local::asynchronous::Client; -//! # let rocket = rocket::ignite(); -//! # rocket::async_test(async { -//! # let client = Client::new(rocket).await.unwrap(); -//! # let req = client.get("/"); -//! let response = req.dispatch().await; -//! # let _ = response; -//! # }); -//! ``` -//! -//! All together and in idiomatic fashion, this might look like: -//! -//! ```rust -//! use rocket::local::asynchronous::Client; -//! -//! # rocket::async_test(async { -//! let client = Client::new(rocket::ignite()).await.expect("valid rocket"); -//! let response = client.post("/") -//! .body("Hello, world!") -//! .dispatch().await; -//! # let _ = response; -//! # }); -//! ``` +//! Both APIs include a `Client` structure that is used to create `LocalRequest` +//! structures that can be dispatched against a given [`Rocket`](crate::Rocket) +//! instance to yield a `LocalResponse` structure. The APIs are identical except +//! in that the `asynchronous` APIs return `Future`s for otherwise blocking +//! operations. //! //! # Unit/Integration Testing //! -//! This module can be used to test a Rocket application by constructing -//! requests via `Client` and validating the resulting response. As an example, -//! consider the following complete "Hello, world!" application, with testing. +//! This module is primarily intended to be used to test a Rocket application by +//! constructing requests via `Client`, dispatching them, and validating the +//! resulting response. As a complete example, consider the following "Hello, +//! world!" application, with testing. //! //! ```rust //! #![feature(proc_macro_hygiene)] @@ -91,27 +43,143 @@ //! "Hello, world!" //! } //! -//! # fn main() { } +//! # /* +//! #[launch] +//! # */ +//! fn rocket() -> rocket::Rocket { +//! rocket::ignite().mount("/", routes![hello]) +//! } +//! //! #[cfg(test)] //! mod test { -//! use super::{rocket, hello}; -//! use rocket::local::asynchronous::Client; -//! -//! #[rocket::async_test] -//! fn test_hello_world() { +//! // Using the preferred `blocking` API. +//! #[test] +//! fn test_hello_world_blocking() { //! // Construct a client to use for dispatching requests. -//! let rocket = rocket::ignite().mount("/", routes![hello]); -//! let client = Client::new(rocket).expect("valid rocket instance"); +//! use rocket::local::blocking::Client; +//! let client = Client::new(super::rocket()) +//! .expect("valid `Rocket`"); //! //! // Dispatch a request to 'GET /' and validate the response. -//! let mut response = client.get("/").dispatch().await; -//! assert_eq!(response.into_string().await, Some("Hello, world!".into())); +//! let response = client.get("/").dispatch(); +//! assert_eq!(response.into_string().unwrap(), "Hello, world!"); +//! } +//! +//! // Using the `asynchronous` API. +//! #[rocket::async_test] +//! async fn test_hello_world_async() { +//! // Construct a client to use for dispatching requests. +//! use rocket::local::asynchronous::Client; +//! let client = Client::new(super::rocket()).await +//! .expect("valid `Rocket`"); +//! +//! // Dispatch a request to 'GET /' and validate the response. +//! let response = client.get("/").dispatch().await; +//! assert_eq!(response.into_string().await.unwrap(), "Hello, world!"); //! } //! } //! ``` //! +//! For more details on testing, see the [testing guide]. +//! +//! [testing guide]: https://rocket.rs/v0.5/guide/testing/ //! [`Client`]: crate::local::asynchronous::Client -//! [`LocalRequest`]: crate::local::asynchronous::LocalRequest +//! +//! # `Client` +//! +//! A `Client`, either [`blocking::Client`] or [`asynchronous::Client`], +//! referred to as simply [`Client`] and [`async` `Client`], respectively, +//! constructs requests for local dispatching. +//! +//! **Usage** +//! +//! A `Client` is constructed via the [`new()`] ([`async` `new()`]) or +//! [`untracked()`] ([`async` `untracked()`]) methods from an already +//! constructed `Rocket` instance. Once a value of `Client` has been +//! constructed, [`get()`], [`put()`], [`post()`], and so on ([`async` `get()`], +//! [`async` `put()`], [`async` `post()`]) can be called to create a +//! [`LocalRequest`] ([`async` `LocalRequest`]) for dispatching. +//! +//! **Cookie Tracking** +//! +//! A `Client` constructed using `new()` propagates cookie changes made by +//! responses to previously dispatched requests. In other words, if a previously +//! dispatched request resulted in a response that adds a cookie, any future +//! requests will contain that cookie. Similarly, cookies removed by a response +//! won't be propagated further. +//! +//! This is typically the desired mode of operation for a `Client` as it removes +//! the burden of manually tracking cookies. Under some circumstances, however, +//! disabling this tracking may be desired. In these cases, use the +//! `untracked()` constructor to create a `Client` that _will not_ track +//! cookies. +//! +//! [`Client`]: blocking::Client +//! [`async` `Client`]: asynchronous::Client +//! [`LocalRequest`]: blocking::LocalRequest +//! [`async` `LocalRequest`]: asynchronous::LocalRequest +//! +//! [`new()`]: blocking::Client::new() +//! [`untracked()`]: blocking::Client::untracked() +//! [`async` `new()`]: asynchronous::Client::new() +//! [`async` `untracked()`]: asynchronous::Client::untracked() +//! +//! [`get()`]: blocking::Client::get() +//! [`put()`]: blocking::Client::put() +//! [`post()`]: blocking::Client::post() +//! [`async` `get()`]: asynchronous::Client::get() +//! [`async` `put()`]: asynchronous::Client::put() +//! [`async` `post()`]: asynchronous::Client::post() +//! +//! **Example** +//! +//! For a usage example, see [`Client`] or [`async` `Client`]. +//! +//! # `LocalRequest` +//! +//! A [`LocalRequest`] ([`async` `LocalRequest`]) is constructed via a `Client`. +//! Once obtained, headers, cookies, including private cookies, the remote IP +//! address, and the request body can all be set via methods on the +//! `LocalRequest` structure. +//! +//! **Dispatching** +//! +//! A `LocalRequest` is dispatched by calling [`dispatch()`] ([`async` +//! `dispatch()`]). The `LocalRequest` is consumed and a [`LocalResponse`] +//! ([`async` `LocalResponse`]) is returned. +//! +//! Note that `LocalRequest` implements [`Clone`]. As such, if the same request +//! needs to be dispatched multiple times, the request can first be cloned and +//! then dispatched: `request.clone().dispatch()`. +//! +//! **Example** +//! +//! For a usage example, see [`LocalRequest`] or [`async` `LocalRequest`]. +//! +//! [`LocalResponse`]: blocking::LocalResponse +//! [`async` `LocalResponse`]: asynchronous::LocalResponse +//! [`dispatch()`]: blocking::LocalRequest::dispatch() +//! [`async` `dispatch()`]: asynchronous::LocalRequest::dispatch() +//! +//! # `LocalResponse` +//! +//! The result of `dispatch()`ing a `LocalRequest` is a [`LocalResponse`] +//! ([`async` `LocalResponse`]). A `LocalResponse` can be queried for response +//! metadata, including the HTTP status, headers, and cookies, via its getter +//! methods. Additionally, the body of the response can be read into a string +//! ([`into_string()`] or [`async` `into_string()`]) or a vector +//! ([`into_bytes()`] or [`async` `into_bytes()`]). +//! +//! The response body must also be read directly using standard I/O mechanisms: +//! the `blocking` `LocalResponse` implements `Read` while the `async` +//! `LocalResponse` implements `AsyncRead`. +//! +//! For a usage example, see [`LocalResponse`] or [`async` `LocalResponse`]. +//! +//! [`into_string()`]: blocking::LocalResponse::into_string() +//! [`async` `into_string()`]: asynchronous::LocalResponse::into_string() +//! [`into_bytes()`]: blocking::LocalResponse::into_bytes() +//! [`async` `into_bytes()`]: asynchronous::LocalResponse::into_bytes() #[macro_use] mod client; #[macro_use] mod request; diff --git a/core/lib/src/local/request.rs b/core/lib/src/local/request.rs index 7016e00f..62dcf0e1 100644 --- a/core/lib/src/local/request.rs +++ b/core/lib/src/local/request.rs @@ -1,286 +1,246 @@ -macro_rules! struct_request { - ([$(#[$attr:meta])*] $item:item) => +macro_rules! pub_request_impl { + ($import:literal $($prefix:tt $suffix:tt)?) => { - /// A structure representing a local request as created by [`Client`]. + /// Retrieves the inner `Request` as seen by Rocket. /// - /// # Usage + /// # Example /// - /// A `LocalRequest` value is constructed via method constructors on [`Client`]. - /// Headers can be added via the [`header`] builder method and the - /// [`add_header`] method. Cookies can be added via the [`cookie`] builder - /// method. The remote IP address can be set via the [`remote`] builder method. - /// The body of the request can be set via the [`body`] builder method or - /// [`set_body`] method. + /// ```rust + #[doc = $import] /// - $(#[$attr])* - /// - /// # Dispatching - /// - /// A `LocalRequest` is dispatched by calling [`dispatch`]. - /// The `LocalRequest` is consumed and a response is returned. - /// - /// Note that `LocalRequest` implements `Clone`. As such, if the - /// same request needs to be dispatched multiple times, the request can first be - /// cloned and then dispatched: `request.clone().dispatch()`. - /// - /// [`Client`]: super::Client - /// [`header`]: #method.header - /// [`add_header`]: #method.add_header - /// [`cookie`]: #method.cookie - /// [`remote`]: #method.remote - /// [`body`]: #method.body - /// [`set_body`]: #method.set_body - /// [`dispatch`]: #method.dispatch - $item -}} + /// # Client::_test(|_, request, _| { + /// let request: LocalRequest = request; + /// let inner: &rocket::Request = request.inner(); + /// # }); + /// ``` + #[inline(always)] + pub fn inner(&self) -> &Request<'c> { + self._request() + } -macro_rules! impl_request { - ($import:literal $(@$prefix:tt $suffix:tt)? $name:ident) => -{ - impl<'c> $name<'c> { - /// Retrieves the inner `Request` as seen by Rocket. - /// - /// # Example - /// - /// ```rust - #[doc = $import] - /// use rocket::Request; - /// - /// # Client::_test(|client| { - /// let client: Client = client; - /// let req = client.get("/"); - /// let inner: &Request = req.inner(); - /// # }); - /// ``` - #[inline(always)] - pub fn inner(&self) -> &Request<'c> { - self._request() - } + /// Add a header to this request. + /// + /// Any type that implements `Into
` can be used here. Among + /// others, this includes [`ContentType`] and [`Accept`]. + /// + /// [`ContentType`]: crate::http::ContentType + /// [`Accept`]: crate::http::Accept + /// + /// # Examples + /// + /// Add the Content-Type header: + /// + /// ```rust + #[doc = $import] + /// use rocket::http::Header; + /// use rocket::http::ContentType; + /// + /// # Client::_test(|_, request, _| { + /// let request: LocalRequest = request; + /// let req = request + /// .header(ContentType::JSON) + /// .header(Header::new("X-Custom", "custom-value")); + /// # }); + /// ``` + #[inline] + pub fn header(mut self, header: H) -> Self + where H: Into> + { + self._request_mut().add_header(header.into()); + self + } - /// Add a header to this request. - /// - /// Any type that implements `Into
` can be used here. Among - /// others, this includes [`ContentType`] and [`Accept`]. - /// - /// [`ContentType`]: crate::http::ContentType - /// [`Accept`]: crate::http::Accept - /// - /// # Examples - /// - /// Add the Content-Type header: - /// - /// ```rust - #[doc = $import] - /// use rocket::http::ContentType; - /// - /// # Client::_test(|client| { - /// let client: Client = client; - /// let req = client.get("/").header(ContentType::JSON); - /// # }); - /// ``` - #[inline] - pub fn header(mut self, header: H) -> Self - where H: Into> - { - self._request_mut().add_header(header.into()); - self - } + /// Adds a header to this request without consuming `self`. + /// + /// # Examples + /// + /// Add the Content-Type header: + /// + /// ```rust + #[doc = $import] + /// use rocket::http::ContentType; + /// + /// # Client::_test(|_, mut request, _| { + /// let mut request: LocalRequest = request; + /// request.add_header(ContentType::JSON); + /// # }); + /// ``` + #[inline] + pub fn add_header(&mut self, header: H) + where H: Into> + { + self._request_mut().add_header(header.into()); + } - /// Adds a header to this request without consuming `self`. - /// - /// # Examples - /// - /// Add the Content-Type header: - /// - /// ```rust - #[doc = $import] - /// use rocket::http::ContentType; - /// - /// # Client::_test(|client| { - /// let client: Client = client; - /// let mut req = client.get("/"); - /// req.add_header(ContentType::JSON); - /// # }); - /// ``` - #[inline] - pub fn add_header(&mut self, header: H) - where H: Into> - { - self._request_mut().add_header(header.into()); - } + /// Set the remote address of this request. + /// + /// # Examples + /// + /// Set the remote address to "8.8.8.8:80": + /// + /// ```rust + #[doc = $import] + /// + /// # Client::_test(|_, request, _| { + /// let request: LocalRequest = request; + /// let address = "8.8.8.8:80".parse().unwrap(); + /// let req = request.remote(address); + /// # }); + /// ``` + #[inline] + pub fn remote(mut self, address: std::net::SocketAddr) -> Self { + self._request_mut().set_remote(address); + self + } - /// Set the remote address of this request. - /// - /// # Examples - /// - /// Set the remote address to "8.8.8.8:80": - /// - /// ```rust - #[doc = $import] - /// - /// # Client::_test(|client| { - /// let address = "8.8.8.8:80".parse().unwrap(); - /// - /// let client: Client = client; - /// let req = client.get("/").remote(address); - /// # }); - /// ``` - #[inline] - pub fn remote(mut self, address: std::net::SocketAddr) -> Self { - self._request_mut().set_remote(address); - self - } + /// Add a cookie to this request. + /// + /// # Examples + /// + /// Add `user_id` cookie: + /// + /// ```rust + #[doc = $import] + /// use rocket::http::Cookie; + /// + /// # Client::_test(|_, request, _| { + /// let request: LocalRequest = request; + /// let req = request + /// .cookie(Cookie::new("username", "sb")) + /// .cookie(Cookie::new("user_id", "12")); + /// # }); + /// ``` + #[inline] + pub fn cookie(self, cookie: crate::http::Cookie<'_>) -> Self { + self._request().cookies().add_original(cookie.into_owned()); + self + } - /// Add a cookie to this request. - /// - /// # Examples - /// - /// Add `user_id` cookie: - /// - /// ```rust - #[doc = $import] - /// use rocket::http::Cookie; - /// - /// # Client::_test(|client| { - /// let client: Client = client; - /// let req = client.get("/") - /// .cookie(Cookie::new("username", "sb")) - /// .cookie(Cookie::new("user_id", "12")); - /// # }); - /// ``` - #[inline] - pub fn cookie(self, cookie: crate::http::Cookie<'_>) -> Self { + /// Add all of the cookies in `cookies` to this request. + /// + /// # Examples + /// + /// Add `user_id` cookie: + /// + /// ```rust + #[doc = $import] + /// use rocket::http::Cookie; + /// + /// # Client::_test(|_, request, _| { + /// let request: LocalRequest = request; + /// let cookies = vec![Cookie::new("a", "b"), Cookie::new("c", "d")]; + /// let req = request.cookies(cookies); + /// # }); + /// ``` + #[inline] + pub fn cookies(self, cookies: Vec>) -> Self { + for cookie in cookies { self._request().cookies().add_original(cookie.into_owned()); - self } - /// Add all of the cookies in `cookies` to this request. - /// - /// # Examples - /// - /// Add `user_id` cookie: - /// - /// ```rust - #[doc = $import] - /// use rocket::http::Cookie; - /// - /// # Client::_test(|client| { - /// let client: Client = client; - /// let cookies = vec![Cookie::new("a", "b"), Cookie::new("c", "d")]; - /// let req = client.get("/").cookies(cookies); - /// # }); - /// ``` - #[inline] - pub fn cookies(self, cookies: Vec>) -> Self { - for cookie in cookies { - self._request().cookies().add_original(cookie.into_owned()); - } - - self - } - - /// Add a [private cookie] to this request. - /// - /// This method is only available when the `private-cookies` feature is - /// enabled. - /// - /// [private cookie]: crate::http::Cookies::add_private() - /// - /// # Examples - /// - /// Add `user_id` as a private cookie: - /// - /// ```rust - #[doc = $import] - /// use rocket::http::Cookie; - /// - /// # Client::_test(|client| { - /// let client: Client = client; - /// let req = client.get("/").private_cookie(Cookie::new("user_id", "sb")); - /// # }); - /// ``` - #[inline] - #[cfg(feature = "private-cookies")] - pub fn private_cookie(self, cookie: crate::http::Cookie<'static>) -> Self { - self._request().cookies().add_original_private(cookie); - self - } - - /// Set the body (data) of the request. - /// - /// # Examples - /// - /// Set the body to be a JSON structure; also sets the Content-Type. - /// - /// ```rust - #[doc = $import] - /// use rocket::http::ContentType; - /// - /// # Client::_test(|client| { - /// let client: Client = client; - /// let req = client.post("/") - /// .header(ContentType::JSON) - /// .body(r#"{ "key": "value", "array": [1, 2, 3], }"#); - /// # }); - /// ``` - #[inline] - pub fn body>(mut self, body: S) -> Self { - // TODO: For CGI, we want to be able to set the body to be stdin - // without actually reading everything into a vector. Can we allow - // that here while keeping the simplicity? Looks like it would - // require us to reintroduce a NetStream::Local(Box) or - // something like that. - *self._body_mut() = body.as_ref().into(); - self - } - - /// Set the body (data) of the request without consuming `self`. - /// - /// # Examples - /// - /// Set the body to be a JSON structure; also sets the Content-Type. - /// - /// ```rust - #[doc = $import] - /// use rocket::http::ContentType; - /// - /// # Client::_test(|client| { - /// let client: Client = client; - /// let mut req = client.post("/").header(ContentType::JSON); - /// req.set_body(r#"{ "key": "value", "array": [1, 2, 3], }"#); - /// # }); - /// ``` - #[inline] - pub fn set_body>(&mut self, body: S) { - *self._body_mut() = body.as_ref().into(); - } - - /// Dispatches the request, returning the response. - /// - /// This method consumes `self` and is the preferred mechanism for - /// dispatching. - /// - /// # Example - /// - /// ```rust - /// use rocket::local::asynchronous::Client; - /// - /// # rocket::async_test(async { - /// let client = Client::new(rocket::ignite()).await.unwrap(); - /// let response = client.get("/").dispatch(); - /// # }); - /// ``` - #[inline(always)] - pub $($prefix)? fn dispatch(self) -> LocalResponse<'c> { - self._dispatch()$(.$suffix)? - } + self } - impl std::fmt::Debug for $name<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self._request().fmt(f) - } + /// Add a [private cookie] to this request. + /// + /// This method is only available when the `private-cookies` feature is + /// enabled. + /// + /// [private cookie]: crate::http::Cookies::add_private() + /// + /// # Examples + /// + /// Add `user_id` as a private cookie: + /// + /// ```rust + #[doc = $import] + /// use rocket::http::Cookie; + /// + /// # Client::_test(|_, request, _| { + /// let request: LocalRequest = request; + /// let req = request.private_cookie(Cookie::new("user_id", "sb")); + /// # }); + /// ``` + #[inline] + #[cfg(feature = "private-cookies")] + pub fn private_cookie(self, cookie: crate::http::Cookie<'static>) -> Self { + self._request().cookies().add_original_private(cookie); + self } - // TODO: Add test to check that `LocalRequest` is `Clone`. + /// Set the body (data) of the request. + /// + /// # Examples + /// + /// Set the body to be a JSON structure; also sets the Content-Type. + /// + /// ```rust + #[doc = $import] + /// use rocket::http::ContentType; + /// + /// # Client::_test(|_, request, _| { + /// let request: LocalRequest = request; + /// let req = request + /// .header(ContentType::JSON) + /// .body(r#"{ "key": "value", "array": [1, 2, 3], }"#); + /// # }); + /// ``` + #[inline] + pub fn body>(mut self, body: S) -> Self { + // TODO: For CGI, we want to be able to set the body to be stdin + // without actually reading everything into a vector. Can we allow + // that here while keeping the simplicity? Looks like it would + // require us to reintroduce a NetStream::Local(Box) or + // something like that. + *self._body_mut() = body.as_ref().into(); + self + } + + /// Set the body (data) of the request without consuming `self`. + /// + /// # Examples + /// + /// Set the body to be a JSON structure; also sets the Content-Type. + /// + /// ```rust + #[doc = $import] + /// use rocket::http::ContentType; + /// + /// # Client::_test(|_, request, _| { + /// let request: LocalRequest = request; + /// let mut request = request.header(ContentType::JSON); + /// request.set_body(r#"{ "key": "value", "array": [1, 2, 3], }"#); + /// # }); + /// ``` + #[inline] + pub fn set_body>(&mut self, body: S) { + *self._body_mut() = body.as_ref().into(); + } + + /// Dispatches the request, returning the response. + /// + /// This method consumes `self` and is the preferred mechanism for + /// dispatching. + /// + /// # Example + /// + /// ```rust + #[doc = $import] + /// + /// # Client::_test(|_, request, _| { + /// let request: LocalRequest = request; + /// let response = request.dispatch(); + /// # }); + /// ``` + #[inline(always)] + pub $($prefix)? fn dispatch(self) -> LocalResponse<'c> { + self._dispatch()$(.$suffix)? + } + + #[cfg(test)] + #[allow(dead_code)] + fn _ensure_impls_exist() { + fn is_clone_debug() {} + is_clone_debug::(); + } }} diff --git a/core/lib/src/local/response.rs b/core/lib/src/local/response.rs index 1f81ef2b..c307ca00 100644 --- a/core/lib/src/local/response.rs +++ b/core/lib/src/local/response.rs @@ -1,74 +1,89 @@ -macro_rules! struct_response { - ($item:item) => -{ - /// A structure representing a response from dispatching a local request. - /// - /// This structure is a thin wrapper around [`Response`]. It implements no - /// methods of its own; all functionality is exposed via the [`Deref`] - /// implementation with a target of `Response`. In other words, when - /// invoking methods, a `LocalResponse` can be treated exactly as if it were - /// a (read-only) `Response`. - /// - /// [`Deref`]: std::ops::Deref - $item -}} - -macro_rules! impl_response { - ($import:literal $(@$prefix:tt $suffix:tt)? $name:ident) => -{ - impl<'c> $name<'c> { - /// Consumes `self` reads its body into a string. If `self` doesn't have - /// a body, reading fails, or string conversion (for non-UTF-8 bodies) - /// fails, returns `None`. +macro_rules! getter_method { + ($doc_prelude:literal, $desc:literal, $f:ident -> $r:ty) => ( + getter_method!(@$doc_prelude, $f, $desc, $r, + concat!("let ", stringify!($f), " = response.", stringify!($f), "();")); + ); + (@$doc_prelude:literal, $f:ident, $desc:expr, $r:ty, $use_it:expr) => ( + /// Returns the + #[doc = $desc] + /// of `self`. /// /// # Example /// - /// ```rust,ignore - #[doc = $import] + /// ```rust + #[doc = $doc_prelude] /// - /// # Client::_test(|client| { - /// let client: Client = client; - /// let response = client.get("/").body("Hello!").dispatch(); - /// assert_eq!(response.into_string().unwrap(), "Hello!"); - /// # }) + /// # Client::_test(|_, _, response| { + /// let response: LocalResponse = response; + #[doc = $use_it] + /// # }); /// ``` #[inline(always)] - pub $($prefix)? fn into_string(self) -> Option { - self._into_string() $(.$suffix)? + pub fn $f(&self) -> $r { + self._response().$f() } + ) +} - /// Consumes `self` and reads its body into a `Vec` of `u8` bytes. If - /// `self` doesn't have a body or reading fails returns `None`. Note - /// that `self`'s `body` is consumed after a call to this method. - /// - /// # Example - /// - /// ```rust,ignore - #[doc = $import] - /// - /// # Client::_test(|client| { - /// let client: Client = client; - /// let response = client.get("/").body("Hello!").dispatch(); - /// assert_eq!(response.into_bytes().unwrap(), "Hello!".as_bytes()); - /// # }) - /// ``` - #[inline(always)] - pub $($prefix)? fn into_bytes(self) -> Option> { - self._into_bytes() $(.$suffix)? - } +macro_rules! pub_response_impl { + ($doc_prelude:literal $($prefix:tt $suffix:tt)?) => +{ + getter_method!($doc_prelude, "HTTP status", + status -> crate::http::Status); + + getter_method!($doc_prelude, "Content-Type, if a valid one is set,", + content_type -> Option); + + getter_method!($doc_prelude, "HTTP headers", + headers -> &crate::http::HeaderMap<'_>); + + getter_method!($doc_prelude, "HTTP cookies as set in the `Set-Cookie` header", + cookies -> Vec>); + + getter_method!($doc_prelude, "response body, if there is one,", + body -> Option<&crate::response::ResponseBody<'_>>); + + /// Consumes `self` and reads the entirety of its body into a string. If + /// `self` doesn't have a body, reading fails, or string conversion (for + /// non-UTF-8 bodies) fails, returns `None`. + /// + /// # Example + /// + /// ```rust + #[doc = $doc_prelude] + /// + /// # Client::_test(|_, _, response| { + /// let response: LocalResponse = response; + /// let string = response.into_string(); + /// # }); + /// ``` + #[inline(always)] + pub $($prefix)? fn into_string(self) -> Option { + self._into_string() $(.$suffix)? } - impl std::fmt::Debug for LocalResponse<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self._response().fmt(f) - } + /// Consumes `self` and reads the entirety of its body into a `Vec` of `u8` + /// bytes. If `self` doesn't have a body or reading fails, returns `None`. + /// + /// # Example + /// + /// ```rust + #[doc = $doc_prelude] + /// + /// # Client::_test(|_, _, response| { + /// let response: LocalResponse = response; + /// let bytes = response.into_bytes(); + /// # }); + /// ``` + #[inline(always)] + pub $($prefix)? fn into_bytes(self) -> Option> { + self._into_bytes() $(.$suffix)? } - impl<'c> std::ops::Deref for LocalResponse<'c> { - type Target = Response<'c>; - - fn deref(&self) -> &Response<'c> { - self._response() - } + #[cfg(test)] + #[allow(dead_code)] + fn _ensure_impls_exist() { + fn is_debug() {} + is_debug::(); } }} diff --git a/core/lib/src/request/request.rs b/core/lib/src/request/request.rs index 7549d58e..e5a693ad 100644 --- a/core/lib/src/request/request.rs +++ b/core/lib/src/request/request.rs @@ -100,8 +100,8 @@ impl<'r> Request<'r> { uri: Origin<'s> ) -> Request<'r> { let mut request = Request { + uri, method: Atomic::new(method), - uri: uri, headers: HeaderMap::new(), remote: None, state: RequestState { diff --git a/core/lib/src/rocket.rs b/core/lib/src/rocket.rs index 811bdd2d..7effb6ea 100644 --- a/core/lib/src/rocket.rs +++ b/core/lib/src/rocket.rs @@ -61,6 +61,9 @@ enum PreLaunchOp { #[repr(transparent)] pub struct Cargo(Rocket); +// A token returned to force the execution of one method before another. +pub(crate) struct Token; + impl Rocket { #[inline] fn _mount(&mut self, base: Origin<'static>, routes: Vec) { @@ -133,29 +136,24 @@ impl Rocket { // requiring only a single `await` at the call site. After completion, // `self.pending` will be empty and `self.manifest` will reflect all pending // changes. - // - // Note that this returns a boxed future, because `_attach()` calls this - // function again creating a cycle. - fn actualize_manifest(&mut self) -> BoxFuture<'_, ()> { - Box::pin(async move { - // Note: attach fairings may add more ops to the `manifest`! We - // process them as a stack to maintain proper ordering. - let mut manifest = mem::replace(&mut self.manifest, vec![]); - while !manifest.is_empty() { - trace_!("[MANIEST PROGRESS]: {:?}", manifest); - match manifest.remove(0) { - PreLaunchOp::Manage(_, callback) => callback(&mut self.managed_state), - PreLaunchOp::Mount(base, routes) => self._mount(base, routes), - PreLaunchOp::Register(catchers) => self._register(catchers), - PreLaunchOp::Attach(fairing) => { - let rocket = mem::replace(self, Rocket::dummy()); - *self = rocket._attach(fairing).await; - self.manifest.append(&mut manifest); - manifest = mem::replace(&mut self.manifest, vec![]); - } + async fn actualize_manifest(&mut self) { + // Note: attach fairings may add more ops to the `manifest`! We + // process them as a stack to maintain proper ordering. + let mut manifest = mem::replace(&mut self.manifest, vec![]); + while !manifest.is_empty() { + trace_!("[MANIEST PROGRESS]: {:?}", manifest); + match manifest.remove(0) { + PreLaunchOp::Manage(_, callback) => callback(&mut self.managed_state), + PreLaunchOp::Mount(base, routes) => self._mount(base, routes), + PreLaunchOp::Register(catchers) => self._register(catchers), + PreLaunchOp::Attach(fairing) => { + let rocket = mem::replace(self, Rocket::dummy()); + *self = rocket._attach(fairing).await; + self.manifest.append(&mut manifest); + manifest = mem::replace(&mut self.manifest, vec![]); } } - }) + } } pub(crate) async fn into_cargo(mut self) -> Cargo { @@ -176,11 +174,11 @@ impl Rocket { // converts Hyper types into Rocket types, then calls the `dispatch` function, // which knows nothing about Hyper. Because responding depends on the // `HyperResponse` type, this function does the actual response processing. -fn hyper_service_fn( +async fn hyper_service_fn( rocket: Arc, h_addr: std::net::SocketAddr, hyp_req: hyper::Request, -) -> impl Future, io::Error>> { +) -> Result, io::Error> { // This future must return a hyper::Response, but that's not easy // because the response body might borrow from the request. Instead, // we do the body writing in another future that will send us @@ -211,13 +209,12 @@ fn hyper_service_fn( let data = Data::from_hyp(h_body).await; // Dispatch the request to get a response, then write that response out. - let r = rocket.dispatch(&mut req, data).await; + let token = rocket.preprocess_request(&mut req, &data).await; + let r = rocket.dispatch(token, &mut req, data).await; rocket.issue_response(r, tx).await; }); - async move { - rx.await.map_err(|e| io::Error::new(io::ErrorKind::Other, e)) - } + rx.await.map_err(|e| io::Error::new(io::ErrorKind::Other, e)) } impl Rocket { @@ -295,9 +292,14 @@ impl Rocket { /// Preprocess the request for Rocket things. Currently, this means: /// /// * Rewriting the method in the request if _method form field exists. + /// * Run the request fairings. /// /// Keep this in-sync with derive_form when preprocessing form fields. - fn preprocess_request(&self, req: &mut Request<'_>, data: &Data) { + pub(crate) async fn preprocess_request( + &self, + req: &mut Request<'_>, + data: &Data + ) -> Token { // Check if this is a form and if the form contains the special _method // field which we use to reinterpret the request's method. let data_len = data.peek().len(); @@ -312,10 +314,15 @@ impl Rocket { .next(); if let Some(Ok(method)) = method { - req.set_method(method); + req._set_method(method); } } } + + // Run request fairings. + self.fairings.handle_request(req, data).await; + + Token } /// Route the request and process the outcome to eventually get a response. @@ -384,8 +391,9 @@ impl Rocket { // Dispatch the request to the handler. let outcome = route.handler.handle(request, data).await; - // Check if the request processing completed (Some) or if the request needs - // to be forwarded. If it does, continue the loop (None) to try again. + // Check if the request processing completed (Some) or if the + // request needs to be forwarded. If it does, continue the loop + // (None) to try again. info_!("{} {}", Paint::default("Outcome:").bold(), outcome); match outcome { o@Outcome::Success(_) | o@Outcome::Failure(_) => return o, @@ -399,42 +407,35 @@ impl Rocket { } #[inline] - pub(crate) fn dispatch<'s, 'r: 's>( + pub(crate) async fn dispatch<'s, 'r: 's>( &'s self, - request: &'r mut Request<'s>, + _token: Token, + request: &'r Request<'s>, data: Data - ) -> impl Future> + 's { - async move { - info!("{}:", request); + ) -> Response<'r> { + info!("{}:", request); - // Do a bit of preprocessing before routing. - self.preprocess_request(request, &data); + // Remember if the request is `HEAD` for later body stripping. + let was_head_request = request.method() == Method::Head; - // Run the request fairings. - self.fairings.handle_request(request, &data).await; + // Route the request and run the user's handlers. + let mut response = self.route_and_process(request, data).await; - // Remember if the request is `HEAD` for later body stripping. - let was_head_request = request.method() == Method::Head; - - // Route the request and run the user's handlers. - let mut response = self.route_and_process(request, data).await; - - // Add a default 'Server' header if it isn't already there. - // TODO: If removing Hyper, write out `Date` header too. - if !response.headers().contains("Server") { - response.set_header(Header::new("Server", "Rocket")); - } - - // Run the response fairings. - self.fairings.handle_response(request, &mut response).await; - - // Strip the body if this is a `HEAD` request. - if was_head_request { - response.strip_body(); - } - - response + // Add a default 'Server' header if it isn't already there. + // TODO: If removing Hyper, write out `Date` header too. + if !response.headers().contains("Server") { + response.set_header(Header::new("Server", "Rocket")); } + + // Run the response fairings. + self.fairings.handle_response(request, &mut response).await; + + // Strip the body if this is a `HEAD` request. + if was_head_request { + response.strip_body(); + } + + response } // Finds the error catcher for the status `status` and executes it for the diff --git a/examples/session/src/tests.rs b/examples/session/src/tests.rs index 5bf5c1b7..ec9ba9b7 100644 --- a/examples/session/src/tests.rs +++ b/examples/session/src/tests.rs @@ -1,9 +1,8 @@ use super::rocket; -use rocket::Response; -use rocket::local::asynchronous::Client; +use rocket::local::asynchronous::{Client, LocalResponse}; use rocket::http::{Status, Cookie, ContentType}; -fn user_id_cookie(response: &Response<'_>) -> Option> { +fn user_id_cookie(response: &LocalResponse<'_>) -> Option> { let cookie = response.headers() .get("Set-Cookie") .filter(|v| v.starts_with("user_id"))