mirror of https://github.com/rwf2/Rocket.git
Rework 'local' module. Add 'LocalResponse' methods.
This completes the 'local' blocking client implementation.
This commit is contained in:
parent
050a2c6461
commit
6482fa2fba
|
@ -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<RwLock<CookieJar>>,
|
||||
}
|
||||
pub(in super) cookies: Option<RwLock<CookieJar>>,
|
||||
}
|
||||
|
||||
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, F: FnOnce(Self) -> T + Send>(f: F) -> T {
|
||||
pub fn _test<T, F>(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<T: Sync + Send>() {}
|
||||
|
||||
#[test]
|
||||
fn test_local_client_impl_send_sync() {
|
||||
assert_sync_send::<Client>();
|
||||
fn assert_sync_send<T: Sync + Send>() {}
|
||||
assert_sync_send::<super::Client>();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<u8>,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Request<'c>>,
|
||||
response: Response<'c>,
|
||||
}
|
||||
|
||||
impl<'c> LocalResponse<'c> {
|
||||
fn _response(&self) -> &Response<'c> {
|
||||
&self.inner
|
||||
pub(crate) fn new<F, O>(req: Request<'c>, f: F) -> impl Future<Output = LocalResponse<'c>>
|
||||
where F: FnOnce(&'c Request<'c>) -> O + Send,
|
||||
O: Future<Output = Response<'c>> + 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
|
||||
}
|
||||
|
||||
pub(crate) async fn _into_string(mut self) -> Option<String> {
|
||||
self.inner.body_string().await
|
||||
}
|
||||
|
||||
pub(crate) async fn _into_bytes(mut self) -> Option<Vec<u8>> {
|
||||
self.inner.body_bytes().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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<tokio::runtime::Runtime>,
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
fn _new(rocket: Rocket, tracked: bool) -> Result<Client, LaunchError> {
|
||||
|
@ -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, F: FnOnce(Self) -> T + Send>(f: F) -> T {
|
||||
pub fn _test<T, F>(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<T>() {};
|
||||
/// test::<Client>();
|
||||
///
|
||||
/// fn is_send<T: Send>() {};
|
||||
/// is_send::<Client>();
|
||||
/// ```
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// use rocket::local::blocking::Client;
|
||||
///
|
||||
|
@ -95,5 +91,5 @@ mod doctest {
|
|||
/// not_sync::<Client>();
|
||||
/// ```
|
||||
#[allow(dead_code)]
|
||||
fn test_not_sync_or_send() {}
|
||||
fn test_not_sync() {}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String> {
|
||||
|
@ -21,6 +67,20 @@ impl<'c> LocalResponse<'c> {
|
|||
fn _into_bytes(self) -> Option<Vec<u8>> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,10 +38,9 @@ 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.
|
||||
///
|
||||
|
@ -144,8 +105,8 @@ macro_rules! impl_client {
|
|||
/// ```rust,no_run
|
||||
#[doc = $import]
|
||||
///
|
||||
/// # Client::_test(|client| {
|
||||
/// let client: Client = client;
|
||||
/// # Client::_test(|client, _, _| {
|
||||
/// let client: &Client = client;
|
||||
/// let rocket = client.rocket();
|
||||
/// # });
|
||||
/// ```
|
||||
|
@ -162,8 +123,8 @@ macro_rules! impl_client {
|
|||
/// ```rust
|
||||
#[doc = $import]
|
||||
///
|
||||
/// # Client::_test(|client| {
|
||||
/// let client: Client = client;
|
||||
/// # Client::_test(|client, _, _| {
|
||||
/// let client: &Client = client;
|
||||
/// let cargo = client.cargo();
|
||||
/// # });
|
||||
/// ```
|
||||
|
@ -193,8 +154,8 @@ macro_rules! impl_client {
|
|||
#[doc = $import]
|
||||
/// use rocket::http::Method;
|
||||
///
|
||||
/// # Client::_test(|client| {
|
||||
/// let client: Client = client;
|
||||
/// # Client::_test(|client, _, _| {
|
||||
/// let client: &Client = client;
|
||||
/// client.req(Method::Get, "/hello");
|
||||
/// # });
|
||||
/// ```
|
||||
|
@ -208,5 +169,11 @@ macro_rules! impl_client {
|
|||
{
|
||||
self._req(method, uri)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(dead_code)]
|
||||
fn _ensure_impls_exist() {
|
||||
fn is_send<T: Send>() {}
|
||||
is_send::<Self>();
|
||||
}
|
||||
}}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,55 +1,16 @@
|
|||
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`].
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
$(#[$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
|
||||
}}
|
||||
|
||||
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();
|
||||
/// # Client::_test(|_, request, _| {
|
||||
/// let request: LocalRequest = request;
|
||||
/// let inner: &rocket::Request = request.inner();
|
||||
/// # });
|
||||
/// ```
|
||||
#[inline(always)]
|
||||
|
@ -71,11 +32,14 @@ macro_rules! impl_request {
|
|||
///
|
||||
/// ```rust
|
||||
#[doc = $import]
|
||||
/// use rocket::http::Header;
|
||||
/// use rocket::http::ContentType;
|
||||
///
|
||||
/// # Client::_test(|client| {
|
||||
/// let client: Client = client;
|
||||
/// let req = client.get("/").header(ContentType::JSON);
|
||||
/// # Client::_test(|_, request, _| {
|
||||
/// let request: LocalRequest = request;
|
||||
/// let req = request
|
||||
/// .header(ContentType::JSON)
|
||||
/// .header(Header::new("X-Custom", "custom-value"));
|
||||
/// # });
|
||||
/// ```
|
||||
#[inline]
|
||||
|
@ -96,10 +60,9 @@ macro_rules! impl_request {
|
|||
#[doc = $import]
|
||||
/// use rocket::http::ContentType;
|
||||
///
|
||||
/// # Client::_test(|client| {
|
||||
/// let client: Client = client;
|
||||
/// let mut req = client.get("/");
|
||||
/// req.add_header(ContentType::JSON);
|
||||
/// # Client::_test(|_, mut request, _| {
|
||||
/// let mut request: LocalRequest = request;
|
||||
/// request.add_header(ContentType::JSON);
|
||||
/// # });
|
||||
/// ```
|
||||
#[inline]
|
||||
|
@ -118,11 +81,10 @@ macro_rules! impl_request {
|
|||
/// ```rust
|
||||
#[doc = $import]
|
||||
///
|
||||
/// # Client::_test(|client| {
|
||||
/// # Client::_test(|_, request, _| {
|
||||
/// let request: LocalRequest = request;
|
||||
/// let address = "8.8.8.8:80".parse().unwrap();
|
||||
///
|
||||
/// let client: Client = client;
|
||||
/// let req = client.get("/").remote(address);
|
||||
/// let req = request.remote(address);
|
||||
/// # });
|
||||
/// ```
|
||||
#[inline]
|
||||
|
@ -141,9 +103,9 @@ macro_rules! impl_request {
|
|||
#[doc = $import]
|
||||
/// use rocket::http::Cookie;
|
||||
///
|
||||
/// # Client::_test(|client| {
|
||||
/// let client: Client = client;
|
||||
/// let req = client.get("/")
|
||||
/// # Client::_test(|_, request, _| {
|
||||
/// let request: LocalRequest = request;
|
||||
/// let req = request
|
||||
/// .cookie(Cookie::new("username", "sb"))
|
||||
/// .cookie(Cookie::new("user_id", "12"));
|
||||
/// # });
|
||||
|
@ -164,10 +126,10 @@ macro_rules! impl_request {
|
|||
#[doc = $import]
|
||||
/// use rocket::http::Cookie;
|
||||
///
|
||||
/// # Client::_test(|client| {
|
||||
/// let client: Client = client;
|
||||
/// # Client::_test(|_, request, _| {
|
||||
/// let request: LocalRequest = request;
|
||||
/// let cookies = vec![Cookie::new("a", "b"), Cookie::new("c", "d")];
|
||||
/// let req = client.get("/").cookies(cookies);
|
||||
/// let req = request.cookies(cookies);
|
||||
/// # });
|
||||
/// ```
|
||||
#[inline]
|
||||
|
@ -194,9 +156,9 @@ macro_rules! impl_request {
|
|||
#[doc = $import]
|
||||
/// use rocket::http::Cookie;
|
||||
///
|
||||
/// # Client::_test(|client| {
|
||||
/// let client: Client = client;
|
||||
/// let req = client.get("/").private_cookie(Cookie::new("user_id", "sb"));
|
||||
/// # Client::_test(|_, request, _| {
|
||||
/// let request: LocalRequest = request;
|
||||
/// let req = request.private_cookie(Cookie::new("user_id", "sb"));
|
||||
/// # });
|
||||
/// ```
|
||||
#[inline]
|
||||
|
@ -216,9 +178,9 @@ macro_rules! impl_request {
|
|||
#[doc = $import]
|
||||
/// use rocket::http::ContentType;
|
||||
///
|
||||
/// # Client::_test(|client| {
|
||||
/// let client: Client = client;
|
||||
/// let req = client.post("/")
|
||||
/// # Client::_test(|_, request, _| {
|
||||
/// let request: LocalRequest = request;
|
||||
/// let req = request
|
||||
/// .header(ContentType::JSON)
|
||||
/// .body(r#"{ "key": "value", "array": [1, 2, 3], }"#);
|
||||
/// # });
|
||||
|
@ -244,10 +206,10 @@ macro_rules! impl_request {
|
|||
#[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], }"#);
|
||||
/// # 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]
|
||||
|
@ -263,24 +225,22 @@ macro_rules! impl_request {
|
|||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::local::asynchronous::Client;
|
||||
#[doc = $import]
|
||||
///
|
||||
/// # rocket::async_test(async {
|
||||
/// let client = Client::new(rocket::ignite()).await.unwrap();
|
||||
/// let response = client.get("/").dispatch();
|
||||
/// # Client::_test(|_, request, _| {
|
||||
/// let request: LocalRequest = request;
|
||||
/// let response = request.dispatch();
|
||||
/// # });
|
||||
/// ```
|
||||
#[inline(always)]
|
||||
pub $($prefix)? fn dispatch(self) -> LocalResponse<'c> {
|
||||
self._dispatch()$(.$suffix)?
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for $name<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self._request().fmt(f)
|
||||
#[cfg(test)]
|
||||
#[allow(dead_code)]
|
||||
fn _ensure_impls_exist() {
|
||||
fn is_clone_debug<T: Clone + std::fmt::Debug>() {}
|
||||
is_clone_debug::<Self>();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add test to check that `LocalRequest` is `Clone`.
|
||||
}}
|
||||
|
|
|
@ -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 fn $f(&self) -> $r {
|
||||
self._response().$f()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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<crate::http::ContentType>);
|
||||
|
||||
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<crate::http::Cookie<'_>>);
|
||||
|
||||
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<String> {
|
||||
self._into_string() $(.$suffix)?
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// 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,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_bytes().unwrap(), "Hello!".as_bytes());
|
||||
/// # })
|
||||
/// # 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 std::fmt::Debug for LocalResponse<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self._response().fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
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<T: std::fmt::Debug>() {}
|
||||
is_debug::<Self>();
|
||||
}
|
||||
}}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<Route>) {
|
||||
|
@ -133,11 +136,7 @@ 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 {
|
||||
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![]);
|
||||
|
@ -155,7 +154,6 @@ impl Rocket {
|
|||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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<Rocket>,
|
||||
h_addr: std::net::SocketAddr,
|
||||
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
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
|
||||
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,20 +407,14 @@ 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<Output = Response<'r>> + 's {
|
||||
async move {
|
||||
) -> Response<'r> {
|
||||
info!("{}:", request);
|
||||
|
||||
// Do a bit of preprocessing before routing.
|
||||
self.preprocess_request(request, &data);
|
||||
|
||||
// Run the request fairings.
|
||||
self.fairings.handle_request(request, &data).await;
|
||||
|
||||
// Remember if the request is `HEAD` for later body stripping.
|
||||
let was_head_request = request.method() == Method::Head;
|
||||
|
||||
|
@ -435,7 +437,6 @@ impl Rocket {
|
|||
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
// Finds the error catcher for the status `status` and executes it for the
|
||||
// given request `req`. If a user has registered a catcher for `status`, the
|
||||
|
|
|
@ -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<Cookie<'static>> {
|
||||
fn user_id_cookie(response: &LocalResponse<'_>) -> Option<Cookie<'static>> {
|
||||
let cookie = response.headers()
|
||||
.get("Set-Cookie")
|
||||
.filter(|v| v.starts_with("user_id"))
|
||||
|
|
Loading…
Reference in New Issue