Rework 'local' module. Add 'LocalResponse' methods.

This completes the 'local' blocking client implementation.
This commit is contained in:
Sergio Benitez 2020-07-02 21:20:28 -07:00
parent 050a2c6461
commit 6482fa2fba
15 changed files with 939 additions and 741 deletions

View File

@ -1,21 +1,26 @@
use std::sync::RwLock; use std::sync::RwLock;
use std::borrow::Cow; use std::borrow::Cow;
use crate::local::asynchronous::LocalRequest; use crate::local::asynchronous::{LocalRequest, LocalResponse};
use crate::rocket::{Rocket, Cargo}; use crate::rocket::{Rocket, Cargo};
use crate::http::{Method, private::CookieJar}; use crate::http::{Method, private::CookieJar};
use crate::error::LaunchError; 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 /// ## Multithreaded Syncronization Pitfalls
/// while tracking cookies can result in surprising, non-deterministic behavior. ///
/// This is because while cookie modifications are serialized, the exact /// Unlike its [`blocking`](crate::local::blocking) variant, this `async` `Client`
/// ordering depends on when requests are dispatched. Specifically, when cookie /// implements `Sync`. However, using it in a multithreaded environment while
/// tracking is enabled, all request dispatches are serialized, which in-turn /// tracking cookies can result in surprising, non-deterministic behavior. This
/// serializes modifications to the internally tracked cookies. /// 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 /// If possible, refrain from sharing a single instance of `Client` across
/// multiple threads. Instead, prefer to create a unique instance of `Client` /// multiple threads. Instead, prefer to create a unique instance of `Client`
@ -26,7 +31,7 @@ struct_client! { [
/// ## Example /// ## Example
/// ///
/// The following snippet creates a `Client` from a `Rocket` instance and /// 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 /// ```rust
/// use rocket::local::asynchronous::Client; /// use rocket::local::asynchronous::Client;
@ -36,14 +41,13 @@ struct_client! { [
/// let client = Client::new(rocket).await.expect("valid rocket"); /// let client = Client::new(rocket).await.expect("valid rocket");
/// let response = client.post("/") /// let response = client.post("/")
/// .body("Hello, world!") /// .body("Hello, world!")
/// .dispatch().await; /// .dispatch()
/// .await;
/// # }); /// # });
/// ``` /// ```
]
pub struct Client { pub struct Client {
cargo: Cargo, cargo: Cargo,
pub(crate) cookies: Option<RwLock<CookieJar>>, pub(in super) cookies: Option<RwLock<CookieJar>>,
}
} }
impl Client { impl Client {
@ -61,13 +65,17 @@ impl Client {
Ok(Client { cargo: rocket.into_cargo().await, cookies }) Ok(Client { cargo: rocket.into_cargo().await, cookies })
} }
// WARNING: This is unstable! Do not use this method outside of Rocket!
#[doc(hidden)] #[doc(hidden)]
/// WARNING: This is unstable! Do not use this method outside of Rocket! pub fn _test<T, F>(f: F) -> T
pub fn _test<T, F: FnOnce(Self) -> T + Send>(f: F) -> T { where F: FnOnce(&Self, LocalRequest<'_>, LocalResponse<'_>) -> T + Send
{
crate::async_test(async { crate::async_test(async {
let rocket = crate::ignite(); let rocket = crate::ignite();
let client = Client::new(rocket).await.expect("valid rocket"); 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()) 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)] #[cfg(test)]
mod test { mod test {
use super::Client;
fn assert_sync_send<T: Sync + Send>() {}
#[test] #[test]
fn test_local_client_impl_send_sync() { fn test_local_client_impl_send_sync() {
assert_sync_send::<Client>(); fn assert_sync_send<T: Sync + Send>() {}
assert_sync_send::<super::Client>();
} }
} }

View File

@ -1,8 +1,10 @@
//! Structures for asynchronous local dispatching of requests, primarily for //! Asynchronous local dispatching of requests.
//! testing.
//! //!
//! This module contains the `asynchronous` variant of the `local` API: it can //! This module contains the `asynchronous` variant of the `local` API: it can
//! be used with `#[rocket::async_test]` or another asynchronous test harness. //! 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 client;
mod request; mod request;

View File

@ -5,11 +5,14 @@ use crate::http::{Status, Method, uri::Origin, ext::IntoOwned};
use super::{Client, LocalResponse}; 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 /// ## Example
/// ///
/// The following snippet uses the available builder methods to construct a /// The following snippet uses the available builder methods to construct and
/// `POST` request to `/` with a JSON body: /// dispatch a `POST` request to `/` with a JSON body:
/// ///
/// ```rust /// ```rust
/// use rocket::local::asynchronous::{Client, LocalRequest}; /// use rocket::local::asynchronous::{Client, LocalRequest};
@ -22,16 +25,16 @@ struct_request! { [
/// .remote("127.0.0.1:8000".parse().unwrap()) /// .remote("127.0.0.1:8000".parse().unwrap())
/// .cookie(Cookie::new("name", "value")) /// .cookie(Cookie::new("name", "value"))
/// .body(r#"{ "value": 42 }"#); /// .body(r#"{ "value": 42 }"#);
///
/// let response = req.dispatch().await;
/// # }); /// # });
/// ``` /// ```
]
pub struct LocalRequest<'c> { pub struct LocalRequest<'c> {
client: &'c Client, client: &'c Client,
request: Request<'c>, request: Request<'c>,
data: Vec<u8>, data: Vec<u8>,
uri: Cow<'c, str>, uri: Cow<'c, str>,
} }
}
impl<'c> LocalRequest<'c> { impl<'c> LocalRequest<'c> {
pub(crate) fn new( pub(crate) fn new(
@ -65,33 +68,26 @@ impl<'c> LocalRequest<'c> {
&mut self.data &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. // 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> { async fn _dispatch(mut self) -> LocalResponse<'c> {
// First, validate the URI, returning an error response (generated from // First, validate the URI, returning an error response (generated from
// an error catcher) immediately if it's invalid. // an error catcher) immediately if it's invalid.
let rocket = self.client.rocket();
if let Ok(uri) = Origin::parse(&self.uri) { if let Ok(uri) = Origin::parse(&self.uri) {
self.request.set_uri(uri.into_owned()); self.request.set_uri(uri.into_owned());
} else { } else {
error!("Malformed request URI: {}", self.uri); error!("Malformed request URI: {}", self.uri);
let res = self.client.rocket() return LocalResponse::new(self.request, move |req| {
.handle_error(Status::BadRequest, self.long_lived_request()); rocket.handle_error(Status::BadRequest, req)
}).await
return LocalResponse { _request: self.request, inner: res.await };
} }
// Actually dispatch the request. // Actually dispatch the request.
let response = self.client.rocket() let data = Data::local(self.data);
.dispatch(self.long_lived_request(), Data::local(self.data)) let token = rocket.preprocess_request(&mut self.request, &data).await;
.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 // If the client is tracking cookies, updates the internal cookie jar
// with the changes reflected by `response`. // 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> { 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)
}
}

View File

@ -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}; 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 struct LocalResponse<'c> {
pub(in super) _request: Request<'c>, _request: Box<Request<'c>>,
pub(in super) inner: Response<'c>, response: Response<'c>,
}
} }
impl<'c> LocalResponse<'c> { impl<'c> LocalResponse<'c> {
fn _response(&self) -> &Response<'c> { pub(crate) fn new<F, O>(req: Request<'c>, f: F) -> impl Future<Output = LocalResponse<'c>>
&self.inner where F: FnOnce(&'c Request<'c>) -> O + Send,
} O: Future<Output = Response<'c>> + Send
{
pub(crate) async fn _into_string(mut self) -> Option<String> { // `LocalResponse` is a self-referential structure. In particular,
self.inner.body_string().await // `inner` can refer to `_request` and its contents. As such, we must
} // 1) Ensure `Request` has a stable address.
//
pub(crate) async fn _into_bytes(mut self) -> Option<Vec<u8>> { // This is done by `Box`ing the `Request`, using only the stable
self.inner.body_bytes().await // 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<String> {
self.response.body_string().await
}
pub(crate) async fn _into_bytes(mut self) -> Option<Vec<u8>> {
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<io::Result<usize>> {
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)
}
}

View File

@ -1,16 +1,20 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::cell::RefCell; use std::cell::RefCell;
use crate::error::LaunchError; use crate::error::LaunchError;
use crate::http::Method; use crate::http::Method;
use crate::local::{asynchronous, blocking::LocalRequest}; use crate::local::{asynchronous, blocking::{LocalRequest, LocalResponse}};
use crate::rocket::{Rocket, Cargo}; 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 /// ## Example
/// ///
/// The following snippet creates a `Client` from a `Rocket` instance and /// 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 /// ```rust
/// use rocket::local::blocking::Client; /// use rocket::local::blocking::Client;
@ -21,12 +25,10 @@ struct_client! { [
/// .body("Hello, world!") /// .body("Hello, world!")
/// .dispatch(); /// .dispatch();
/// ``` /// ```
]
pub struct Client { pub struct Client {
pub(crate) inner: asynchronous::Client, pub(crate) inner: asynchronous::Client,
runtime: RefCell<tokio::runtime::Runtime>, runtime: RefCell<tokio::runtime::Runtime>,
} }
}
impl Client { impl Client {
fn _new(rocket: Rocket, tracked: bool) -> Result<Client, LaunchError> { fn _new(rocket: Rocket, tracked: bool) -> Result<Client, LaunchError> {
@ -41,12 +43,16 @@ impl Client {
Ok(Self { inner, runtime: RefCell::new(runtime) }) Ok(Self { inner, runtime: RefCell::new(runtime) })
} }
// WARNING: This is unstable! Do not use this method outside of Rocket!
#[doc(hidden)] #[doc(hidden)]
/// WARNING: This is unstable! Do not use this method outside of Rocket! pub fn _test<T, F>(f: F) -> T
pub fn _test<T, F: FnOnce(Self) -> T + Send>(f: F) -> T { where F: FnOnce(&Self, LocalRequest<'_>, LocalResponse<'_>) -> T + Send
{
let rocket = crate::ignite(); let rocket = crate::ignite();
let client = Client::new(rocket).expect("valid rocket"); 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)] #[inline(always)]
@ -71,23 +77,13 @@ impl Client {
{ {
LocalRequest::new(self, method, uri.into()) 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)] #[cfg(doctest)]
mod 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<T>() {};
/// test::<Client>();
///
/// fn is_send<T: Send>() {};
/// is_send::<Client>();
/// ```
///
/// ```compile_fail /// ```compile_fail
/// use rocket::local::blocking::Client; /// use rocket::local::blocking::Client;
/// ///
@ -95,5 +91,5 @@ mod doctest {
/// not_sync::<Client>(); /// not_sync::<Client>();
/// ``` /// ```
#[allow(dead_code)] #[allow(dead_code)]
fn test_not_sync_or_send() {} fn test_not_sync() {}
} }

View File

@ -1,9 +1,11 @@
//! Structures for blocking local dispatching of requests, primarily for //! Blocking local dispatching of requests.
//! testing.
//! //!
//! This module contains the `blocking` variant of the `local` API: it can be //! This module contains the `blocking` variant of the `local` API: it can be
//! used in Rust's synchronous `#[test]` harness. This is accomplished by //! 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 client;
mod request; mod request;

View File

@ -4,11 +4,14 @@ use crate::{Request, http::Method, local::asynchronous};
use super::{Client, LocalResponse}; 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 /// ## Example
/// ///
/// The following snippet uses the available builder methods to construct a /// The following snippet uses the available builder methods to construct and
/// `POST` request to `/` with a JSON body: /// dispatch a `POST` request to `/` with a JSON body:
/// ///
/// ```rust /// ```rust
/// use rocket::local::blocking::{Client, LocalRequest}; /// use rocket::local::blocking::{Client, LocalRequest};
@ -20,14 +23,14 @@ struct_request! { [
/// .remote("127.0.0.1:8000".parse().unwrap()) /// .remote("127.0.0.1:8000".parse().unwrap())
/// .cookie(Cookie::new("name", "value")) /// .cookie(Cookie::new("name", "value"))
/// .body(r#"{ "value": 42 }"#); /// .body(r#"{ "value": 42 }"#);
///
/// let response = req.dispatch();
/// ``` /// ```
]
#[derive(Clone)] #[derive(Clone)]
pub struct LocalRequest<'c> { pub struct LocalRequest<'c> {
inner: asynchronous::LocalRequest<'c>, inner: asynchronous::LocalRequest<'c>,
client: &'c Client, client: &'c Client,
} }
}
impl<'c> LocalRequest<'c> { impl<'c> LocalRequest<'c> {
#[inline] #[inline]
@ -58,6 +61,13 @@ impl<'c> LocalRequest<'c> {
let inner = self.client.block_on(self.inner.dispatch()); let inner = self.client.block_on(self.inner.dispatch());
LocalResponse { inner, client: self.client } 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)
}
}

View File

@ -1,17 +1,63 @@
use std::io;
use tokio::io::AsyncReadExt;
use crate::{Response, local::asynchronous}; use crate::{Response, local::asynchronous};
use super::Client; 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 struct LocalResponse<'c> {
pub(in super) inner: asynchronous::LocalResponse<'c>, pub(in super) inner: asynchronous::LocalResponse<'c>,
pub(in super) client: &'c Client, pub(in super) client: &'c Client,
} }
}
impl<'c> LocalResponse<'c> { impl LocalResponse<'_> {
fn _response(&self) -> &Response<'c> { fn _response(&self) -> &Response<'_> {
&*self.inner &self.inner._response()
} }
fn _into_string(self) -> Option<String> { fn _into_string(self) -> Option<String> {
@ -21,6 +67,20 @@ impl<'c> LocalResponse<'c> {
fn _into_bytes(self) -> Option<Vec<u8>> { fn _into_bytes(self) -> Option<Vec<u8>> {
self.client.block_on(self.inner._into_bytes()) 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<usize> {
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)
}
}

View File

@ -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 { macro_rules! req_method {
($import:literal, $NAME:literal, $f:ident, $method:expr) => ( ($import:literal, $NAME:literal, $f:ident, $method:expr) => (
req_method!(@ req_method!(@
@ -62,8 +24,8 @@ macro_rules! req_method {
/// ```rust,no_run /// ```rust,no_run
#[doc = $import] #[doc = $import]
/// ///
/// # Client::_test(|client| { /// # Client::_test(|client, _, _| {
/// let client: Client = client; /// let client: &Client = client;
#[doc = $use_it] #[doc = $use_it]
/// # }); /// # });
/// ``` /// ```
@ -76,137 +38,142 @@ macro_rules! req_method {
) )
} }
macro_rules! impl_client { macro_rules! pub_client_impl {
($import:literal $(@$prefix:tt $suffix:tt)? $name:ident) => ($import:literal $(@$prefix:tt $suffix:tt)?) =>
{ {
impl $name { /// Construct a new `Client` from an instance of `Rocket` with cookie
/// Construct a new `Client` from an instance of `Rocket` with cookie /// tracking.
/// tracking. ///
/// /// # Cookie Tracking
/// # Cookie Tracking ///
/// /// By default, a `Client` propagates cookie changes made by responses
/// By default, a `Client` propagates cookie changes made by responses /// to previously dispatched requests. In other words, if a previously
/// to previously dispatched requests. In other words, if a previously /// dispatched request resulted in a response that adds a cookie, any
/// dispatched request resulted in a response that adds a cookie, any /// future requests will contain the new cookies. Similarly, cookies
/// future requests will contain the new cookies. Similarly, cookies /// removed by a response won't be propagated further.
/// removed by a response won't be propagated further. ///
/// /// This is typically the desired mode of operation for a `Client` as it
/// This is typically the desired mode of operation for a `Client` as it /// removes the burden of manually tracking cookies. Under some
/// removes the burden of manually tracking cookies. Under some /// circumstances, however, disabling this tracking may be desired. The
/// circumstances, however, disabling this tracking may be desired. The /// [`untracked()`](Client::untracked()) method creates a `Client` that
/// [`untracked()`](Client::untracked()) method creates a `Client` that /// _will not_ track cookies.
/// _will not_ track cookies. ///
/// /// # Errors
/// # Errors ///
/// /// If launching the `Rocket` instance would fail, excepting network errors,
/// If launching the `Rocket` instance would fail, excepting network errors, /// the `LaunchError` is returned.
/// the `LaunchError` is returned. ///
/// /// ```rust,no_run
/// ```rust,no_run #[doc = $import]
#[doc = $import] ///
/// /// let rocket = rocket::ignite();
/// let rocket = rocket::ignite(); /// let client = Client::new(rocket);
/// let client = Client::new(rocket); /// ```
/// ``` #[inline(always)]
#[inline(always)] pub $($prefix)? fn new(rocket: Rocket) -> Result<Self, LaunchError> {
pub $($prefix)? fn new(rocket: Rocket) -> Result<Self, LaunchError> { Self::_new(rocket, true) $(.$suffix)?
Self::_new(rocket, true) $(.$suffix)? }
}
/// Construct a new `Client` from an instance of `Rocket` _without_ /// Construct a new `Client` from an instance of `Rocket` _without_
/// cookie tracking. /// cookie tracking.
/// ///
/// # Cookie Tracking /// # Cookie Tracking
/// ///
/// Unlike the [`new()`](Client::new()) constructor, a `Client` returned /// Unlike the [`new()`](Client::new()) constructor, a `Client` returned
/// from this method _does not_ automatically propagate cookie changes. /// from this method _does not_ automatically propagate cookie changes.
/// ///
/// # Errors /// # Errors
/// ///
/// If launching the `Rocket` instance would fail, excepting network /// If launching the `Rocket` instance would fail, excepting network
/// errors, the `LaunchError` is returned. /// errors, the `LaunchError` is returned.
/// ///
/// ```rust,no_run /// ```rust,no_run
#[doc = $import] #[doc = $import]
/// ///
/// let rocket = rocket::ignite(); /// let rocket = rocket::ignite();
/// let client = Client::untracked(rocket); /// let client = Client::untracked(rocket);
/// ``` /// ```
pub $($prefix)? fn untracked(rocket: Rocket) -> Result<Self, LaunchError> { pub $($prefix)? fn untracked(rocket: Rocket) -> Result<Self, LaunchError> {
Self::_new(rocket, true) $(.$suffix)? Self::_new(rocket, true) $(.$suffix)?
} }
/// Returns a reference to the `Rocket` this client is creating requests /// Returns a reference to the `Rocket` this client is creating requests
/// for. /// for.
/// ///
/// # Example /// # Example
/// ///
/// ```rust,no_run /// ```rust,no_run
#[doc = $import] #[doc = $import]
/// ///
/// # Client::_test(|client| { /// # Client::_test(|client, _, _| {
/// let client: Client = client; /// let client: &Client = client;
/// let rocket = client.rocket(); /// let rocket = client.rocket();
/// # }); /// # });
/// ``` /// ```
#[inline(always)] #[inline(always)]
pub fn rocket(&self) -> &Rocket { pub fn rocket(&self) -> &Rocket {
&*self._cargo() &*self._cargo()
} }
/// Returns a reference to the `Cargo` of the `Rocket` this client is /// Returns a reference to the `Cargo` of the `Rocket` this client is
/// creating requests for. /// creating requests for.
/// ///
/// # Example /// # Example
/// ///
/// ```rust /// ```rust
#[doc = $import] #[doc = $import]
/// ///
/// # Client::_test(|client| { /// # Client::_test(|client, _, _| {
/// let client: Client = client; /// let client: &Client = client;
/// let cargo = client.cargo(); /// let cargo = client.cargo();
/// # }); /// # });
/// ``` /// ```
#[inline(always)] #[inline(always)]
pub fn cargo(&self) -> &Cargo { pub fn cargo(&self) -> &Cargo {
self._cargo() self._cargo()
} }
req_method!($import, "GET", get, Method::Get); req_method!($import, "GET", get, Method::Get);
req_method!($import, "PUT", put, Method::Put); req_method!($import, "PUT", put, Method::Put);
req_method!($import, "POST", post, Method::Post); req_method!($import, "POST", post, Method::Post);
req_method!($import, "DELETE", delete, Method::Delete); req_method!($import, "DELETE", delete, Method::Delete);
req_method!($import, "OPTIONS", options, Method::Options); req_method!($import, "OPTIONS", options, Method::Options);
req_method!($import, "HEAD", head, Method::Head); req_method!($import, "HEAD", head, Method::Head);
req_method!($import, "PATCH", patch, Method::Patch); req_method!($import, "PATCH", patch, Method::Patch);
/// Create a local `GET` request to the URI `uri`. /// Create a local `GET` request to the URI `uri`.
/// ///
/// When dispatched, the request will be served by the instance of /// When dispatched, the request will be served by the instance of
/// Rocket within `self`. The request is not dispatched automatically. /// Rocket within `self`. The request is not dispatched automatically.
/// To actually dispatch the request, call [`LocalRequest::dispatch()`] /// To actually dispatch the request, call [`LocalRequest::dispatch()`]
/// on the returned request. /// on the returned request.
/// ///
/// # Example /// # Example
/// ///
/// ```rust,no_run /// ```rust,no_run
#[doc = $import] #[doc = $import]
/// use rocket::http::Method; /// use rocket::http::Method;
/// ///
/// # Client::_test(|client| { /// # Client::_test(|client, _, _| {
/// let client: Client = client; /// let client: &Client = client;
/// client.req(Method::Get, "/hello"); /// client.req(Method::Get, "/hello");
/// # }); /// # });
/// ``` /// ```
#[inline(always)] #[inline(always)]
pub fn req<'c, 'u: 'c, U>( pub fn req<'c, 'u: 'c, U>(
&'c self, &'c self,
method: Method, method: Method,
uri: U uri: U
) -> LocalRequest<'c> ) -> LocalRequest<'c>
where U: Into<Cow<'u, str>> where U: Into<Cow<'u, str>>
{ {
self._req(method, uri) self._req(method, uri)
} }
#[cfg(test)]
#[allow(dead_code)]
fn _ensure_impls_exist() {
fn is_send<T: Send>() {}
is_send::<Self>();
} }
}} }}

View File

@ -5,81 +5,33 @@
//! and integration test Rocket applications by crafting requests, dispatching //! and integration test Rocket applications by crafting requests, dispatching
//! them, and verifying the response. //! them, and verifying the response.
//! //!
//! # Usage //! # Async. vs. Blocking
//! //!
//! This module contains two variants of the local API: [`asynchronous`] and //! This module contains two variants, in its two submodules, of the same local
//! [`blocking`]. The primary difference between the two is in usage: the //! API: [`asynchronous`], and [`blocking`]. As their names imply, the
//! `asynchronous` API requires an asynchronous test entry point such as //! `asynchronous` API is `async`, returning a `Future` for operations that
//! `#[rocket::async_test]`, while the `blocking` API can be used with //! would otherwise block, while `blocking` blocks for the same operations.
//! `#[test]`. Additionally, several methods in the `asynchronous` API are
//! `async` and must therefore be `await`ed.
//! //!
//! Both APIs include a [`Client`] structure that is used to create //! Unless your request handling requires concurrency to make progress, or
//! [`LocalRequest`] structures that can be dispatched against a given //! you're making use of a `Client` in an environment that necessitates or would
//! [`Rocket`](crate::Rocket) instance. Usage is straightforward: //! 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. //! Both APIs include a `Client` structure that is used to create `LocalRequest`
//! //! structures that can be dispatched against a given [`Rocket`](crate::Rocket)
//! ```rust //! instance to yield a `LocalResponse` structure. The APIs are identical except
//! let rocket = rocket::ignite(); //! in that the `asynchronous` APIs return `Future`s for otherwise blocking
//! # let _ = rocket; //! operations.
//! ```
//!
//! 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;
//! # });
//! ```
//! //!
//! # Unit/Integration Testing //! # Unit/Integration Testing
//! //!
//! This module can be used to test a Rocket application by constructing //! This module is primarily intended to be used to test a Rocket application by
//! requests via `Client` and validating the resulting response. As an example, //! constructing requests via `Client`, dispatching them, and validating the
//! consider the following complete "Hello, world!" application, with testing. //! resulting response. As a complete example, consider the following "Hello,
//! world!" application, with testing.
//! //!
//! ```rust //! ```rust
//! #![feature(proc_macro_hygiene)] //! #![feature(proc_macro_hygiene)]
@ -91,27 +43,143 @@
//! "Hello, world!" //! "Hello, world!"
//! } //! }
//! //!
//! # fn main() { } //! # /*
//! #[launch]
//! # */
//! fn rocket() -> rocket::Rocket {
//! rocket::ignite().mount("/", routes![hello])
//! }
//!
//! #[cfg(test)] //! #[cfg(test)]
//! mod test { //! mod test {
//! use super::{rocket, hello}; //! // Using the preferred `blocking` API.
//! use rocket::local::asynchronous::Client; //! #[test]
//! //! fn test_hello_world_blocking() {
//! #[rocket::async_test]
//! fn test_hello_world() {
//! // Construct a client to use for dispatching requests. //! // Construct a client to use for dispatching requests.
//! let rocket = rocket::ignite().mount("/", routes![hello]); //! use rocket::local::blocking::Client;
//! let client = Client::new(rocket).expect("valid rocket instance"); //! let client = Client::new(super::rocket())
//! .expect("valid `Rocket`");
//! //!
//! // Dispatch a request to 'GET /' and validate the response. //! // Dispatch a request to 'GET /' and validate the response.
//! let mut response = client.get("/").dispatch().await; //! let response = client.get("/").dispatch();
//! assert_eq!(response.into_string().await, Some("Hello, world!".into())); //! 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 //! [`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 client;
#[macro_use] mod request; #[macro_use] mod request;

View File

@ -1,286 +1,246 @@
macro_rules! struct_request { macro_rules! pub_request_impl {
([$(#[$attr:meta])*] $item:item) => ($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`]. /// ```rust
/// Headers can be added via the [`header`] builder method and the #[doc = $import]
/// [`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.
/// ///
$(#[$attr])* /// # Client::_test(|_, request, _| {
/// /// let request: LocalRequest = request;
/// # Dispatching /// let inner: &rocket::Request = request.inner();
/// /// # });
/// A `LocalRequest` is dispatched by calling [`dispatch`]. /// ```
/// The `LocalRequest` is consumed and a response is returned. #[inline(always)]
/// pub fn inner(&self) -> &Request<'c> {
/// Note that `LocalRequest` implements `Clone`. As such, if the self._request()
/// 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
}}
macro_rules! impl_request { /// Add a header to this request.
($import:literal $(@$prefix:tt $suffix:tt)? $name:ident) => ///
{ /// Any type that implements `Into<Header>` can be used here. Among
impl<'c> $name<'c> { /// others, this includes [`ContentType`] and [`Accept`].
/// Retrieves the inner `Request` as seen by Rocket. ///
/// /// [`ContentType`]: crate::http::ContentType
/// # Example /// [`Accept`]: crate::http::Accept
/// ///
/// ```rust /// # Examples
#[doc = $import] ///
/// use rocket::Request; /// Add the Content-Type header:
/// ///
/// # Client::_test(|client| { /// ```rust
/// let client: Client = client; #[doc = $import]
/// let req = client.get("/"); /// use rocket::http::Header;
/// let inner: &Request = req.inner(); /// use rocket::http::ContentType;
/// # }); ///
/// ``` /// # Client::_test(|_, request, _| {
#[inline(always)] /// let request: LocalRequest = request;
pub fn inner(&self) -> &Request<'c> { /// let req = request
self._request() /// .header(ContentType::JSON)
} /// .header(Header::new("X-Custom", "custom-value"));
/// # });
/// ```
#[inline]
pub fn header<H>(mut self, header: H) -> Self
where H: Into<crate::http::Header<'static>>
{
self._request_mut().add_header(header.into());
self
}
/// Add a header to this request. /// Adds a header to this request without consuming `self`.
/// ///
/// Any type that implements `Into<Header>` can be used here. Among /// # Examples
/// others, this includes [`ContentType`] and [`Accept`]. ///
/// /// Add the Content-Type header:
/// [`ContentType`]: crate::http::ContentType ///
/// [`Accept`]: crate::http::Accept /// ```rust
/// #[doc = $import]
/// # Examples /// use rocket::http::ContentType;
/// ///
/// Add the Content-Type header: /// # Client::_test(|_, mut request, _| {
/// /// let mut request: LocalRequest = request;
/// ```rust /// request.add_header(ContentType::JSON);
#[doc = $import] /// # });
/// use rocket::http::ContentType; /// ```
/// #[inline]
/// # Client::_test(|client| { pub fn add_header<H>(&mut self, header: H)
/// let client: Client = client; where H: Into<crate::http::Header<'static>>
/// let req = client.get("/").header(ContentType::JSON); {
/// # }); self._request_mut().add_header(header.into());
/// ``` }
#[inline]
pub fn header<H>(mut self, header: H) -> Self
where H: Into<crate::http::Header<'static>>
{
self._request_mut().add_header(header.into());
self
}
/// Adds a header to this request without consuming `self`. /// Set the remote address of this request.
/// ///
/// # Examples /// # Examples
/// ///
/// Add the Content-Type header: /// Set the remote address to "8.8.8.8:80":
/// ///
/// ```rust /// ```rust
#[doc = $import] #[doc = $import]
/// use rocket::http::ContentType; ///
/// /// # Client::_test(|_, request, _| {
/// # Client::_test(|client| { /// let request: LocalRequest = request;
/// let client: Client = client; /// let address = "8.8.8.8:80".parse().unwrap();
/// let mut req = client.get("/"); /// let req = request.remote(address);
/// req.add_header(ContentType::JSON); /// # });
/// # }); /// ```
/// ``` #[inline]
#[inline] pub fn remote(mut self, address: std::net::SocketAddr) -> Self {
pub fn add_header<H>(&mut self, header: H) self._request_mut().set_remote(address);
where H: Into<crate::http::Header<'static>> self
{ }
self._request_mut().add_header(header.into());
}
/// Set the remote address of this request. /// Add a cookie to this request.
/// ///
/// # Examples /// # Examples
/// ///
/// Set the remote address to "8.8.8.8:80": /// Add `user_id` cookie:
/// ///
/// ```rust /// ```rust
#[doc = $import] #[doc = $import]
/// /// use rocket::http::Cookie;
/// # Client::_test(|client| { ///
/// let address = "8.8.8.8:80".parse().unwrap(); /// # Client::_test(|_, request, _| {
/// /// let request: LocalRequest = request;
/// let client: Client = client; /// let req = request
/// let req = client.get("/").remote(address); /// .cookie(Cookie::new("username", "sb"))
/// # }); /// .cookie(Cookie::new("user_id", "12"));
/// ``` /// # });
#[inline] /// ```
pub fn remote(mut self, address: std::net::SocketAddr) -> Self { #[inline]
self._request_mut().set_remote(address); pub fn cookie(self, cookie: crate::http::Cookie<'_>) -> Self {
self self._request().cookies().add_original(cookie.into_owned());
} self
}
/// Add a cookie to this request. /// Add all of the cookies in `cookies` to this request.
/// ///
/// # Examples /// # Examples
/// ///
/// Add `user_id` cookie: /// Add `user_id` cookie:
/// ///
/// ```rust /// ```rust
#[doc = $import] #[doc = $import]
/// use rocket::http::Cookie; /// use rocket::http::Cookie;
/// ///
/// # Client::_test(|client| { /// # Client::_test(|_, request, _| {
/// let client: Client = client; /// let request: LocalRequest = request;
/// let req = client.get("/") /// let cookies = vec![Cookie::new("a", "b"), Cookie::new("c", "d")];
/// .cookie(Cookie::new("username", "sb")) /// let req = request.cookies(cookies);
/// .cookie(Cookie::new("user_id", "12")); /// # });
/// # }); /// ```
/// ``` #[inline]
#[inline] pub fn cookies(self, cookies: Vec<crate::http::Cookie<'_>>) -> Self {
pub fn cookie(self, cookie: crate::http::Cookie<'_>) -> Self { for cookie in cookies {
self._request().cookies().add_original(cookie.into_owned()); self._request().cookies().add_original(cookie.into_owned());
self
} }
/// Add all of the cookies in `cookies` to this request. self
///
/// # 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<crate::http::Cookie<'_>>) -> 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<S: AsRef<[u8]>>(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<Read>) 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<S: AsRef<[u8]>>(&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)?
}
} }
impl std::fmt::Debug for $name<'_> { /// Add a [private cookie] to this request.
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { ///
self._request().fmt(f) /// 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<S: AsRef<[u8]>>(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<Read>) 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<S: AsRef<[u8]>>(&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<T: Clone + std::fmt::Debug>() {}
is_clone_debug::<Self>();
}
}} }}

View File

@ -1,74 +1,89 @@
macro_rules! struct_response { macro_rules! getter_method {
($item:item) => ($doc_prelude:literal, $desc:literal, $f:ident -> $r:ty) => (
{ getter_method!(@$doc_prelude, $f, $desc, $r,
/// A structure representing a response from dispatching a local request. concat!("let ", stringify!($f), " = response.", stringify!($f), "();"));
/// );
/// This structure is a thin wrapper around [`Response`]. It implements no (@$doc_prelude:literal, $f:ident, $desc:expr, $r:ty, $use_it:expr) => (
/// methods of its own; all functionality is exposed via the [`Deref`] /// Returns the
/// implementation with a target of `Response`. In other words, when #[doc = $desc]
/// invoking methods, a `LocalResponse` can be treated exactly as if it were /// of `self`.
/// 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`.
/// ///
/// # Example /// # Example
/// ///
/// ```rust,ignore /// ```rust
#[doc = $import] #[doc = $doc_prelude]
/// ///
/// # Client::_test(|client| { /// # Client::_test(|_, _, response| {
/// let client: Client = client; /// let response: LocalResponse = response;
/// let response = client.get("/").body("Hello!").dispatch(); #[doc = $use_it]
/// assert_eq!(response.into_string().unwrap(), "Hello!"); /// # });
/// # })
/// ``` /// ```
#[inline(always)] #[inline(always)]
pub $($prefix)? fn into_string(self) -> Option<String> { pub fn $f(&self) -> $r {
self._into_string() $(.$suffix)? self._response().$f()
} }
)
}
/// Consumes `self` and reads its body into a `Vec` of `u8` bytes. If macro_rules! pub_response_impl {
/// `self` doesn't have a body or reading fails returns `None`. Note ($doc_prelude:literal $($prefix:tt $suffix:tt)?) =>
/// that `self`'s `body` is consumed after a call to this method. {
/// getter_method!($doc_prelude, "HTTP status",
/// # Example status -> crate::http::Status);
///
/// ```rust,ignore getter_method!($doc_prelude, "Content-Type, if a valid one is set,",
#[doc = $import] content_type -> Option<crate::http::ContentType>);
///
/// # Client::_test(|client| { getter_method!($doc_prelude, "HTTP headers",
/// let client: Client = client; headers -> &crate::http::HeaderMap<'_>);
/// let response = client.get("/").body("Hello!").dispatch();
/// assert_eq!(response.into_bytes().unwrap(), "Hello!".as_bytes()); getter_method!($doc_prelude, "HTTP cookies as set in the `Set-Cookie` header",
/// # }) cookies -> Vec<crate::http::Cookie<'_>>);
/// ```
#[inline(always)] getter_method!($doc_prelude, "response body, if there is one,",
pub $($prefix)? fn into_bytes(self) -> Option<Vec<u8>> { body -> Option<&crate::response::ResponseBody<'_>>);
self._into_bytes() $(.$suffix)?
} /// 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<String> {
self._into_string() $(.$suffix)?
} }
impl std::fmt::Debug for LocalResponse<'_> { /// Consumes `self` and reads the entirety of its body into a `Vec` of `u8`
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { /// bytes. If `self` doesn't have a body or reading fails, returns `None`.
self._response().fmt(f) ///
} /// # 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<Vec<u8>> {
self._into_bytes() $(.$suffix)?
} }
impl<'c> std::ops::Deref for LocalResponse<'c> { #[cfg(test)]
type Target = Response<'c>; #[allow(dead_code)]
fn _ensure_impls_exist() {
fn deref(&self) -> &Response<'c> { fn is_debug<T: std::fmt::Debug>() {}
self._response() is_debug::<Self>();
}
} }
}} }}

View File

@ -100,8 +100,8 @@ impl<'r> Request<'r> {
uri: Origin<'s> uri: Origin<'s>
) -> Request<'r> { ) -> Request<'r> {
let mut request = Request { let mut request = Request {
uri,
method: Atomic::new(method), method: Atomic::new(method),
uri: uri,
headers: HeaderMap::new(), headers: HeaderMap::new(),
remote: None, remote: None,
state: RequestState { state: RequestState {

View File

@ -61,6 +61,9 @@ enum PreLaunchOp {
#[repr(transparent)] #[repr(transparent)]
pub struct Cargo(Rocket); pub struct Cargo(Rocket);
// A token returned to force the execution of one method before another.
pub(crate) struct Token;
impl Rocket { impl Rocket {
#[inline] #[inline]
fn _mount(&mut self, base: Origin<'static>, routes: Vec<Route>) { fn _mount(&mut self, base: Origin<'static>, routes: Vec<Route>) {
@ -133,29 +136,24 @@ impl Rocket {
// requiring only a single `await` at the call site. After completion, // requiring only a single `await` at the call site. After completion,
// `self.pending` will be empty and `self.manifest` will reflect all pending // `self.pending` will be empty and `self.manifest` will reflect all pending
// changes. // changes.
// async fn actualize_manifest(&mut self) {
// Note that this returns a boxed future, because `_attach()` calls this // Note: attach fairings may add more ops to the `manifest`! We
// function again creating a cycle. // process them as a stack to maintain proper ordering.
fn actualize_manifest(&mut self) -> BoxFuture<'_, ()> { let mut manifest = mem::replace(&mut self.manifest, vec![]);
Box::pin(async move { while !manifest.is_empty() {
// Note: attach fairings may add more ops to the `manifest`! We trace_!("[MANIEST PROGRESS]: {:?}", manifest);
// process them as a stack to maintain proper ordering. match manifest.remove(0) {
let mut manifest = mem::replace(&mut self.manifest, vec![]); PreLaunchOp::Manage(_, callback) => callback(&mut self.managed_state),
while !manifest.is_empty() { PreLaunchOp::Mount(base, routes) => self._mount(base, routes),
trace_!("[MANIEST PROGRESS]: {:?}", manifest); PreLaunchOp::Register(catchers) => self._register(catchers),
match manifest.remove(0) { PreLaunchOp::Attach(fairing) => {
PreLaunchOp::Manage(_, callback) => callback(&mut self.managed_state), let rocket = mem::replace(self, Rocket::dummy());
PreLaunchOp::Mount(base, routes) => self._mount(base, routes), *self = rocket._attach(fairing).await;
PreLaunchOp::Register(catchers) => self._register(catchers), self.manifest.append(&mut manifest);
PreLaunchOp::Attach(fairing) => { manifest = mem::replace(&mut self.manifest, vec![]);
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 { 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, // converts Hyper types into Rocket types, then calls the `dispatch` function,
// which knows nothing about Hyper. Because responding depends on the // which knows nothing about Hyper. Because responding depends on the
// `HyperResponse` type, this function does the actual response processing. // `HyperResponse` type, this function does the actual response processing.
fn hyper_service_fn( async fn hyper_service_fn(
rocket: Arc<Rocket>, rocket: Arc<Rocket>,
h_addr: std::net::SocketAddr, h_addr: std::net::SocketAddr,
hyp_req: hyper::Request<hyper::Body>, hyp_req: hyper::Request<hyper::Body>,
) -> impl Future<Output = Result<hyper::Response<hyper::Body>, io::Error>> { ) -> Result<hyper::Response<hyper::Body>, io::Error> {
// This future must return a hyper::Response, but that's not easy // This future must return a hyper::Response, but that's not easy
// because the response body might borrow from the request. Instead, // because the response body might borrow from the request. Instead,
// we do the body writing in another future that will send us // 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; let data = Data::from_hyp(h_body).await;
// Dispatch the request to get a response, then write that response out. // 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; 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 { impl Rocket {
@ -295,9 +292,14 @@ impl Rocket {
/// Preprocess the request for Rocket things. Currently, this means: /// Preprocess the request for Rocket things. Currently, this means:
/// ///
/// * Rewriting the method in the request if _method form field exists. /// * 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. /// 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 // Check if this is a form and if the form contains the special _method
// field which we use to reinterpret the request's method. // field which we use to reinterpret the request's method.
let data_len = data.peek().len(); let data_len = data.peek().len();
@ -312,10 +314,15 @@ impl Rocket {
.next(); .next();
if let Some(Ok(method)) = method { 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. /// Route the request and process the outcome to eventually get a response.
@ -384,8 +391,9 @@ impl Rocket {
// Dispatch the request to the handler. // Dispatch the request to the handler.
let outcome = route.handler.handle(request, data).await; let outcome = route.handler.handle(request, data).await;
// Check if the request processing completed (Some) or if the request needs // Check if the request processing completed (Some) or if the
// to be forwarded. If it does, continue the loop (None) to try again. // request needs to be forwarded. If it does, continue the loop
// (None) to try again.
info_!("{} {}", Paint::default("Outcome:").bold(), outcome); info_!("{} {}", Paint::default("Outcome:").bold(), outcome);
match outcome { match outcome {
o@Outcome::Success(_) | o@Outcome::Failure(_) => return o, o@Outcome::Success(_) | o@Outcome::Failure(_) => return o,
@ -399,42 +407,35 @@ impl Rocket {
} }
#[inline] #[inline]
pub(crate) fn dispatch<'s, 'r: 's>( pub(crate) async fn dispatch<'s, 'r: 's>(
&'s self, &'s self,
request: &'r mut Request<'s>, _token: Token,
request: &'r Request<'s>,
data: Data data: Data
) -> impl Future<Output = Response<'r>> + 's { ) -> Response<'r> {
async move { info!("{}:", request);
info!("{}:", request);
// Do a bit of preprocessing before routing. // Remember if the request is `HEAD` for later body stripping.
self.preprocess_request(request, &data); let was_head_request = request.method() == Method::Head;
// Run the request fairings. // Route the request and run the user's handlers.
self.fairings.handle_request(request, &data).await; let mut response = self.route_and_process(request, data).await;
// Remember if the request is `HEAD` for later body stripping. // Add a default 'Server' header if it isn't already there.
let was_head_request = request.method() == Method::Head; // TODO: If removing Hyper, write out `Date` header too.
if !response.headers().contains("Server") {
// Route the request and run the user's handlers. response.set_header(Header::new("Server", "Rocket"));
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
} }
// 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 // Finds the error catcher for the status `status` and executes it for the

View File

@ -1,9 +1,8 @@
use super::rocket; use super::rocket;
use rocket::Response; use rocket::local::asynchronous::{Client, LocalResponse};
use rocket::local::asynchronous::Client;
use rocket::http::{Status, Cookie, ContentType}; use rocket::http::{Status, Cookie, ContentType};
fn user_id_cookie(response: &Response<'_>) -> Option<Cookie<'static>> { fn user_id_cookie(response: &LocalResponse<'_>) -> Option<Cookie<'static>> {
let cookie = response.headers() let cookie = response.headers()
.get("Set-Cookie") .get("Set-Cookie")
.filter(|v| v.starts_with("user_id")) .filter(|v| v.starts_with("user_id"))