Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Response builder and related changes #580

Merged
merged 9 commits into from
Jun 27, 2020
8 changes: 2 additions & 6 deletions examples/chunked.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
use async_std::task;
use tide::{Body, Response, StatusCode};

fn main() -> Result<(), std::io::Error> {
task::block_on(async {
let mut app = tide::new();
app.at("/").get(|_| async {
let mut res = Response::new(StatusCode::Ok);
res.set_body(Body::from_file(file!()).await.unwrap());
Ok(res)
});
app.at("/")
.get(|_| async { Ok(tide::Body::from_file(file!()).await?) });
yoshuawuyts marked this conversation as resolved.
Show resolved Hide resolved
app.listen("127.0.0.1:8080").await?;
Ok(())
})
Expand Down
29 changes: 12 additions & 17 deletions examples/graphql.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use async_std::task;
use juniper::RootNode;
use juniper::{http::graphiql, http::GraphQLRequest, RootNode};
use std::sync::RwLock;
use tide::{Body, Redirect, Request, Response, Server, StatusCode};
use tide::{http::mime, Body, Redirect, Request, Response, Server, StatusCode};

#[derive(Clone)]
struct User {
Expand Down Expand Up @@ -72,30 +72,25 @@ fn create_schema() -> Schema {
Schema::new(QueryRoot {}, MutationRoot {})
}

async fn handle_graphql(mut req: Request<State>) -> tide::Result {
let query: juniper::http::GraphQLRequest = req
.body_json()
.await
.expect("be able to deserialize the graphql request");

async fn handle_graphql(mut request: Request<State>) -> tide::Result {
let query: GraphQLRequest = request.body_json().await?;
let schema = create_schema(); // probably worth making the schema a singleton using lazy_static library
let response = query.execute(&schema, req.state());
let response = query.execute(&schema, request.state());
let status = if response.is_ok() {
StatusCode::Ok
} else {
StatusCode::BadRequest
};

let mut res = Response::new(status);
res.set_body(Body::from_json(&response)?);
Ok(res)
Ok(Response::builder(status)
.body(Body::from_json(&response)?)
.build())
}

async fn handle_graphiql(_: Request<State>) -> tide::Result {
let mut res = Response::new(StatusCode::Ok);
res.set_body(juniper::http::graphiql::graphiql_source("/graphql"));
res.set_content_type(tide::http::mime::HTML);
Ok(res)
async fn handle_graphiql(_: Request<State>) -> tide::Result<impl Into<Response>> {
Ok(Response::builder(200)
.body(graphiql::graphiql_source("/graphql"))
.content_type(mime::HTML))
}

fn main() -> std::io::Result<()> {
Expand Down
6 changes: 2 additions & 4 deletions examples/json.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use async_std::task;
use serde::{Deserialize, Serialize};
use tide::prelude::*;
use tide::{Body, Request, Response};
use tide::{Body, Request};

#[derive(Deserialize, Serialize)]
struct Cat {
Expand All @@ -20,9 +20,7 @@ fn main() -> tide::Result<()> {
name: "chashu".into(),
};

let mut res = Response::new(200);
res.set_body(Body::from_json(&cat)?);
Ok(res)
Ok(Body::from_json(&cat)?)
});

app.at("/animals").get(|_| async {
Expand Down
31 changes: 16 additions & 15 deletions examples/middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,21 +102,22 @@ async fn main() -> Result<()> {

app.middleware(After(|result: Result| async move {
let response = result.unwrap_or_else(|e| Response::new(e.status()));
match response.status() {
StatusCode::NotFound => {
let mut res = Response::new(404);
res.set_content_type(mime::HTML);
res.set_body(NOT_FOUND_HTML_PAGE);
Ok(res)
}
StatusCode::InternalServerError => {
let mut res = Response::new(500);
res.set_content_type(mime::HTML);
res.set_body(INTERNAL_SERVER_ERROR_HTML_PAGE);
Ok(res)
}
_ => Ok(response),
}

let response = match response.status() {
StatusCode::NotFound => Response::builder(404)
.content_type(mime::HTML)
.body(NOT_FOUND_HTML_PAGE)
.build(),

StatusCode::InternalServerError => Response::builder(500)
.content_type(mime::HTML)
.body(INTERNAL_SERVER_ERROR_HTML_PAGE)
.build(),

_ => response,
};

Ok(response)
}));

app.middleware(user_loader);
Expand Down
4 changes: 3 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@
//! [`Request::ext`].
//!
//! If the endpoint needs to share values with middleware, response scoped state can be set via
//! [`Response::set_ext`] and is available through [`Response::ext`].
//! [`Response::insert_ext`] and is available through [`Response::ext`].
//!
//! Application scoped state is used when a complete application needs access to a particular
//! value. Examples of this include: database connections, websocket connections, or
Expand Down Expand Up @@ -198,6 +198,7 @@ mod middleware;
mod redirect;
mod request;
mod response;
mod response_builder;
mod route;

#[cfg(not(feature = "__internal__bench"))]
Expand All @@ -218,6 +219,7 @@ pub use middleware::{Middleware, Next};
pub use redirect::Redirect;
pub use request::Request;
pub use response::Response;
pub use response_builder::ResponseBuilder;
pub use route::Route;
pub use server::Server;

Expand Down
67 changes: 61 additions & 6 deletions src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::http::cookies::Cookie;
use crate::http::headers::{self, HeaderName, HeaderValues, ToHeaderValues};
use crate::http::Mime;
use crate::http::{self, Body, StatusCode};
use crate::ResponseBuilder;

#[derive(Debug)]
pub(crate) enum CookieEvent {
Expand Down Expand Up @@ -36,14 +37,68 @@ impl Response {
}
}

/// Returns the statuscode.
/// Begin a chained response builder. For more details, see [ResponseBuilder](crate::ResponseBuilder)
///
/// # Example:
/// ```rust
/// # use tide::{StatusCode, Response, http::mime};
/// # async_std::task::block_on(async move {
/// let mut response = Response::builder(203)
/// .body("<html>hi</html>")
/// .header("custom-header", "value")
/// .content_type(mime::HTML)
/// .build();
///
/// assert_eq!(response.take_body().into_string().await.unwrap(), "<html>hi</html>");
/// assert_eq!(response.status(), StatusCode::NonAuthoritativeInformation);
/// assert_eq!(response["custom-header"], "value");
/// assert_eq!(response["content-type"], "text/html;charset=utf-8");
/// # });
/// ```
#[must_use]
pub fn builder<S>(status: S) -> ResponseBuilder
where
S: TryInto<StatusCode>,
S::Error: Debug,
{
ResponseBuilder::new(status)
}

/// Returns the http status code.
#[must_use]
pub fn status(&self) -> crate::StatusCode {
self.res.status()
}

/// Set the statuscode.
pub fn set_status(&mut self, status: crate::StatusCode) {
/// Set the http status code.
///
/// # Example:
/// ```rust
/// # use tide::{StatusCode, Response};
/// let mut response = Response::new(StatusCode::Ok);
///
/// response.set_status(418); // the status can be a valid u16 http status code
/// assert_eq!(response.status(), StatusCode::ImATeapot);
///
/// response.set_status(StatusCode::NonAuthoritativeInformation); // or a tide::StatusCode
/// assert_eq!(response.status(), StatusCode::NonAuthoritativeInformation);
/// ```
/// # Panics
/// `set_status` will panic if the status argument cannot be successfully converted into a StatusCode.
///
/// ```should_panic
/// # use tide::Response;
/// Response::new(200).set_status(210); // this is not an established status code and will panic
/// ```
pub fn set_status<S>(&mut self, status: S)
where
S: TryInto<StatusCode>,
S::Error: Debug,
{
let status = status
.try_into()
.expect("Could not convert into a valid `StatusCode`");

self.res.set_status(status);
}

Expand Down Expand Up @@ -183,7 +238,7 @@ impl Response {
///
/// ## Warning
/// Take care when calling this function with a cookie that was returned by
/// [`Request::cookie`](Request::cookie). As per [section 5.3 step 11 of RFC 6265], a new
/// [`Request::cookie`](crate::Request::cookie). As per [section 5.3 step 11 of RFC 6265], a new
/// cookie is only treated as the same as an old one if it has a matching name, domain and
/// path.
///
Expand All @@ -195,7 +250,7 @@ impl Response {
///
/// To avoid this you can manually set the [domain](Cookie::set_domain) and
/// [path](Cookie::set_path) as necessary after retrieving the cookie using
/// [`Request::cookie`](Request::cookie).
/// [`Request::cookie`](crate::Request::cookie).
///
/// [section 5.3 step 11 of RFC 6265]: https://tools.ietf.org/html/rfc6265#section-5.3
pub fn remove_cookie(&mut self, cookie: Cookie<'static>) {
Expand All @@ -209,7 +264,7 @@ impl Response {
}

/// Set a response scoped extension value.
pub fn insert_ext<T: Send + Sync + 'static>(mut self, val: T) {
pub fn insert_ext<T: Send + Sync + 'static>(&mut self, val: T) {
self.res.ext_mut().insert(val);
}

Expand Down
84 changes: 84 additions & 0 deletions src/response_builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use crate::http::headers::{HeaderName, ToHeaderValues};
use crate::http::{Body, Mime, StatusCode};
use crate::Response;
use std::convert::TryInto;

#[derive(Debug)]

/// Response Builder
///
/// Provides an ergonomic way to chain the creation of a response. This is generally accessed through [`Response::builder`](crate::Response::builder)
///
/// # Example
/// ```rust
/// # use tide::{StatusCode, Response, http::mime};
/// # async_std::task::block_on(async move {
/// let mut response = Response::builder(203)
/// .body("body")
/// .content_type(mime::HTML)
/// .header("custom-header", "value")
/// .build();
///
/// assert_eq!(response.take_body().into_string().await.unwrap(), "body");
/// assert_eq!(response.status(), StatusCode::NonAuthoritativeInformation);
/// assert_eq!(response["custom-header"], "value");
/// assert_eq!(response.content_type(), Some(mime::HTML));
/// # });

pub struct ResponseBuilder(Response);

impl ResponseBuilder {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this include methods for cookie and ext?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I initially added those, but I don't think they make a lot of sense on a builder. The purpose of the builder is to make expressing a one-off response easy. I think it's pretty reasonable to say if they need to make cookie or ext changes, that it's not a quick one-liner type response (before rustfmt). It's easy enough to do:

let mut res = Response::build().status(203).body("hello").unwrap();
res.add_cookie();
res.insert_ext();

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not exactly seeing the advantage of that to writing this if it is only partial?
The same example with less characters:

let mut res = Response::new(203);
res.set_body("hello");
res.add_cookie();
res.insert_ext();

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, in that circumstance there isn't any advantage to using the builder

pub(crate) fn new<S>(status: S) -> Self
where
S: TryInto<StatusCode>,
S::Error: std::fmt::Debug,
{
Self(Response::new(status))
}

/// Returns the inner Response
pub fn build(self) -> Response {
self.0
}

/// Sets a header on the response.
/// ```
/// # use tide::Response;
/// let response = Response::builder(200).header("header-name", "header-value").build();
/// assert_eq!(response["header-name"], "header-value");
/// ```
pub fn header(mut self, key: impl Into<HeaderName>, value: impl ToHeaderValues) -> Self {
self.0.insert_header(key, value);
self
}

/// Sets the Content-Type header on the response.
/// ```
/// # use tide::{http::mime, Response};
/// let response = Response::builder(200).content_type(mime::HTML).build();
/// assert_eq!(response["content-type"], "text/html;charset=utf-8");
/// ```
pub fn content_type(mut self, content_type: impl Into<Mime>) -> Self {
self.0.set_content_type(content_type);
self
}

/// Sets the body of the response.
/// ```
/// # async_std::task::block_on(async move {
/// # use tide::{Response, convert::json};
/// let mut response = Response::builder(200).body(json!({ "any": "Into<Body>"})).build();
/// assert_eq!(response.take_body().into_string().await.unwrap(), "{\"any\":\"Into<Body>\"}");
/// # });
/// ```
pub fn body(mut self, body: impl Into<Body>) -> Self {
self.0.set_body(body);
self
}
}

impl Into<Response> for ResponseBuilder {
fn into(self) -> Response {
self.build()
}
}
13 changes: 3 additions & 10 deletions tests/chunked-encode-large.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ mod test_utils;
use async_std::io::Cursor;
use async_std::prelude::*;
use async_std::task;
use http_types::mime;
use http_types::StatusCode;
use std::time::Duration;

use tide::{Body, Response};
use tide::Body;

const TEXT: &'static str = concat![
"Et provident reprehenderit accusamus dolores et voluptates sed quia. Repellendus odit porro ut et hic molestiae. Sit autem reiciendis animi fugiat deleniti vel iste. Laborum id odio ullam ut impedit dolores. Vel aperiam dolorem voluptatibus dignissimos maxime.",
Expand Down Expand Up @@ -72,13 +70,8 @@ async fn chunked_large() -> Result<(), http_types::Error> {
let port = test_utils::find_port().await;
let server = task::spawn(async move {
let mut app = tide::new();
app.at("/").get(|mut _req: tide::Request<()>| async {
let mut res = Response::new(StatusCode::Ok);
let body = Cursor::new(TEXT.to_owned());
res.set_body(Body::from_reader(body, None));
res.set_content_type(mime::PLAIN);
Ok(res)
});
app.at("/")
.get(|_| async { Ok(Body::from_reader(Cursor::new(TEXT), None)) });
jbr marked this conversation as resolved.
Show resolved Hide resolved
app.listen(("localhost", port)).await?;
Result::<(), http_types::Error>::Ok(())
});
Expand Down
Loading