From 44f5f1998ddf067cc4a836255d376a0e2204561f Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 15 Dec 2016 00:47:31 -0800 Subject: [PATCH] New HTTP types: ContentType, Status. Responder/Handler/ErrorHandler changed. This is a complete rework of `Responder`s and of the http backend in general. This gets Rocket one step closer to HTTP library independence, enabling many future features such as transparent async I/O, automatic HEAD request parsing, pre/post hooks, and more. Summary of changes: * `Responder::response` no longer takes in `FreshHyperResponse`. Instead, it returns a new `Response` type. * The new `Response` type now encapsulates a full HTTP response. As a result, `Responder`s now return it. * The `Handler` type now returns an `Outcome` directly. * The `ErrorHandler` returns a `Result`. It can no longer forward, which made no sense previously. * `Stream` accepts a chunked size parameter. * `StatusCode` removed in favor of new `Status` type. * `ContentType` significantly modified. * New, lightweight `Header` type that plays nicely with `Response`. --- codegen/src/decorators/error.rs | 17 +- codegen/src/decorators/route.rs | 57 ++--- codegen/src/parser/error.rs | 5 + codegen/src/parser/route.rs | 4 +- contrib/src/json/mod.rs | 20 +- contrib/src/templates/mod.rs | 14 +- examples/forms/src/tests.rs | 12 +- examples/from_request/src/main.rs | 3 +- examples/hello_person/src/tests.rs | 11 +- examples/hello_ranks/src/tests.rs | 3 +- examples/hello_world/src/tests.rs | 3 +- examples/manual_routes/Rocket.toml | 3 + examples/manual_routes/src/main.rs | 53 +++-- examples/static_files/src/main.rs | 7 +- examples/testing/src/main.rs | 3 +- lib/src/catcher.rs | 26 +- lib/src/data/data.rs | 51 ++-- lib/src/data/from_data.rs | 10 +- lib/src/ext.rs | 20 ++ lib/src/handler.rs | 35 +++ lib/src/http/content_type.rs | 335 ++++++++++++-------------- lib/src/http/header.rs | 37 +++ lib/src/http/hyper.rs | 1 - lib/src/http/mod.rs | 5 +- lib/src/http/status.rs | 139 +++++++++++ lib/src/lib.rs | 19 +- lib/src/request/form/mod.rs | 6 +- lib/src/request/from_request.rs | 14 +- lib/src/request/request.rs | 6 +- lib/src/response/content.rs | 65 +++-- lib/src/response/failure.rs | 15 +- lib/src/response/flash.rs | 17 +- lib/src/response/mod.rs | 6 +- lib/src/response/named_file.rs | 14 +- lib/src/response/redirect.rs | 31 +-- lib/src/response/responder.rs | 227 ++++++++---------- lib/src/response/response.rs | 366 ++++++++++++++++++++++++++--- lib/src/response/status.rs | 90 ++++--- lib/src/response/stream.rs | 82 ++----- lib/src/rocket.rs | 190 +++++++++------ lib/src/router/collider.rs | 6 +- lib/src/router/mod.rs | 8 +- lib/src/router/route.rs | 6 +- lib/src/testing.rs | 46 +--- 44 files changed, 1258 insertions(+), 830 deletions(-) create mode 100644 examples/manual_routes/Rocket.toml create mode 100644 lib/src/ext.rs create mode 100644 lib/src/handler.rs create mode 100644 lib/src/http/header.rs create mode 100644 lib/src/http/status.rs diff --git a/codegen/src/decorators/error.rs b/codegen/src/decorators/error.rs index 57d93262..6b57d5c5 100644 --- a/codegen/src/decorators/error.rs +++ b/codegen/src/decorators/error.rs @@ -59,9 +59,12 @@ pub fn error_decorator(ecx: &mut ExtCtxt, sp: Span, meta_item: &MetaItem, emit_item(push, quote_item!(ecx, fn $catch_fn_name<'_b>($err_ident: ::rocket::Error, $req_ident: &'_b ::rocket::Request) - -> ::rocket::Response<'_b> { - let result = $user_fn_name($fn_arguments); - rocket::Response::with_raw_status($code, result) + -> ::rocket::response::Result<'_b> { + let response = $user_fn_name($fn_arguments); + let status = ::rocket::http::Status::raw($code); + ::rocket::response::Responder::respond( + ::rocket::response::status::Custom(status, response) + ) } ).expect("catch function")); @@ -69,9 +72,9 @@ pub fn error_decorator(ecx: &mut ExtCtxt, sp: Span, meta_item: &MetaItem, emit_item(push, quote_item!(ecx, #[allow(non_upper_case_globals)] pub static $struct_name: ::rocket::StaticCatchInfo = - ::rocket::StaticCatchInfo { - code: $code, - handler: $catch_fn_name - }; + ::rocket::StaticCatchInfo { + code: $code, + handler: $catch_fn_name + }; ).expect("catch info struct")); } diff --git a/codegen/src/decorators/route.rs b/codegen/src/decorators/route.rs index 0cf866e5..349e3bed 100644 --- a/codegen/src/decorators/route.rs +++ b/codegen/src/decorators/route.rs @@ -15,7 +15,6 @@ use syntax::parse::token; use syntax::ptr::P; use rocket::http::{Method, ContentType}; -use rocket::http::mime::{TopLevel, SubLevel}; fn method_to_path(ecx: &ExtCtxt, method: Method) -> Path { quote_enum!(ecx, method => ::rocket::http::Method { @@ -23,28 +22,14 @@ fn method_to_path(ecx: &ExtCtxt, method: Method) -> Path { }) } -// FIXME: This should return an Expr! (Ext is not a path.) -fn top_level_to_expr(ecx: &ExtCtxt, level: &TopLevel) -> Path { - quote_enum!(ecx, *level => ::rocket::http::mime::TopLevel { - Star, Text, Image, Audio, Video, Application, Multipart, Model, Message; - Ext(ref s) => quote_path!(ecx, ::rocket::http::mime::TopLevel::Ext($s)) - }) -} - -// FIXME: This should return an Expr! (Ext is not a path.) -fn sub_level_to_expr(ecx: &ExtCtxt, level: &SubLevel) -> Path { - quote_enum!(ecx, *level => ::rocket::http::mime::SubLevel { - Star, Plain, Html, Xml, Javascript, Css, EventStream, Json, - WwwFormUrlEncoded, Msgpack, OctetStream, FormData, Png, Gif, Bmp, Jpeg; - Ext(ref s) => quote_path!(ecx, ::rocket::http::mime::SubLevel::Ext($s)) - }) -} - fn content_type_to_expr(ecx: &ExtCtxt, ct: Option) -> Option> { ct.map(|ct| { - let top_level = top_level_to_expr(ecx, &ct.0); - let sub_level = sub_level_to_expr(ecx, &ct.1); - quote_expr!(ecx, ::rocket::http::ContentType($top_level, $sub_level, None)) + let (ttype, subtype) = (ct.ttype, ct.subtype); + quote_expr!(ecx, ::rocket::http::ContentType { + ttype: ::std::borrow::Cow::Borrowed($ttype), + subtype: ::std::borrow::Cow::Borrowed($subtype), + params: None + }) }) } @@ -84,7 +69,7 @@ impl RouteGenerateExt for RouteParams { let $name: $ty = match ::rocket::request::FromForm::from_form_string($form_string) { Ok(v) => v, - Err(_) => return ::rocket::Response::forward(_data) + Err(_) => return ::rocket::Outcome::Forward(_data) }; ).expect("form statement")) } @@ -105,11 +90,11 @@ impl RouteGenerateExt for RouteParams { Some(quote_stmt!(ecx, let $name: $ty = match ::rocket::data::FromData::from_data(&_req, _data) { - ::rocket::outcome::Outcome::Success(d) => d, - ::rocket::outcome::Outcome::Forward(d) => - return ::rocket::Response::forward(d), - ::rocket::outcome::Outcome::Failure((code, _)) => { - return ::rocket::Response::failure(code); + ::rocket::Outcome::Success(d) => d, + ::rocket::Outcome::Forward(d) => + return ::rocket::Outcome::Forward(d), + ::rocket::Outcome::Failure((code, _)) => { + return ::rocket::Outcome::Failure(code); } }; ).expect("data statement")) @@ -120,7 +105,7 @@ impl RouteGenerateExt for RouteParams { let expr = quote_expr!(ecx, match _req.uri().query() { Some(query) => query, - None => return ::rocket::Response::forward(_data) + None => return ::rocket::Outcome::Forward(_data) } ); @@ -149,11 +134,11 @@ impl RouteGenerateExt for RouteParams { let expr = match param { Param::Single(_) => quote_expr!(ecx, match _req.get_param_str($i) { Some(s) => <$ty as ::rocket::request::FromParam>::from_param(s), - None => return ::rocket::Response::forward(_data) + None => return ::rocket::Outcome::Forward(_data) }), Param::Many(_) => quote_expr!(ecx, match _req.get_raw_segments($i) { Some(s) => <$ty as ::rocket::request::FromSegments>::from_segments(s), - None => return ::rocket::Response::forward(_data) + None => return ::rocket::Outcome::forward(_data) }), }; @@ -164,7 +149,7 @@ impl RouteGenerateExt for RouteParams { Err(e) => { println!(" => Failed to parse '{}': {:?}", stringify!($original_ident), e); - return ::rocket::Response::forward(_data) + return ::rocket::Outcome::Forward(_data) } }; ).expect("declared param parsing statement")); @@ -195,9 +180,9 @@ impl RouteGenerateExt for RouteParams { ::rocket::request::FromRequest::from_request(&_req) { ::rocket::outcome::Outcome::Success(v) => v, ::rocket::outcome::Outcome::Forward(_) => - return ::rocket::Response::forward(_data), + return ::rocket::Outcome::forward(_data), ::rocket::outcome::Outcome::Failure((code, _)) => { - return ::rocket::Response::failure(code) + return ::rocket::Outcome::Failure(code) }, }; ).expect("undeclared param parsing statement")); @@ -250,12 +235,12 @@ fn generic_route_decorator(known_method: Option>, let route_fn_name = user_fn_name.prepend(ROUTE_FN_PREFIX); emit_item(push, quote_item!(ecx, fn $route_fn_name<'_b>(_req: &'_b ::rocket::Request, _data: ::rocket::Data) - -> ::rocket::Response<'_b> { + -> ::rocket::handler::Outcome<'_b> { $param_statements $query_statement $data_statement - let result = $user_fn_name($fn_arguments); - ::rocket::Response::success(result) + let responder = $user_fn_name($fn_arguments); + ::rocket::handler::Outcome::of(responder) } ).unwrap()); diff --git a/codegen/src/parser/error.rs b/codegen/src/parser/error.rs index d18ff106..b145c1d3 100644 --- a/codegen/src/parser/error.rs +++ b/codegen/src/parser/error.rs @@ -2,6 +2,8 @@ use syntax::ast::*; use syntax::ext::base::{ExtCtxt, Annotatable}; use syntax::codemap::{Span, Spanned, dummy_spanned}; +use rocket::http::Status; + use utils::{span, MetaItemExt}; use super::Function; @@ -50,6 +52,9 @@ fn parse_code(ecx: &ExtCtxt, meta_item: &NestedMetaItem) -> Spanned { if n.node < 400 || n.node > 599 { ecx.span_err(n.span, "code must be >= 400 and <= 599."); span(0, n.span) + } else if Status::from_code(n.node as u16).is_none() { + ecx.span_warn(n.span, "status code is unknown."); + span(n.node as u16, n.span) } else { span(n.node as u16, n.span) } diff --git a/codegen/src/parser/route.rs b/codegen/src/parser/route.rs index 301ce017..5ac3f3cf 100644 --- a/codegen/src/parser/route.rs +++ b/codegen/src/parser/route.rs @@ -269,7 +269,7 @@ fn parse_rank(ecx: &ExtCtxt, kv: &KVSpanned) -> isize { fn parse_format(ecx: &ExtCtxt, kv: &KVSpanned) -> ContentType { if let LitKind::Str(ref s, _) = *kv.value() { if let Ok(ct) = ContentType::from_str(&s.as_str()) { - if ct.is_ext() { + if !ct.is_known() { let msg = format!("'{}' is not a known content-type", s); ecx.span_warn(kv.value.span, &msg); } else { @@ -286,5 +286,5 @@ fn parse_format(ecx: &ExtCtxt, kv: &KVSpanned) -> ContentType { content-type accepted. e.g: format = "application/json""#) .emit(); - ContentType::any() + ContentType::Any } diff --git a/contrib/src/json/mod.rs b/contrib/src/json/mod.rs index f94564a5..d1180445 100644 --- a/contrib/src/json/mod.rs +++ b/contrib/src/json/mod.rs @@ -8,8 +8,7 @@ use rocket::outcome::{Outcome, IntoOutcome}; use rocket::request::Request; use rocket::data::{self, Data, FromData}; use rocket::response::{self, Responder, content}; -use rocket::http::StatusCode; -use rocket::http::hyper::FreshHyperResponse; +use rocket::http::Status; use self::serde::{Serialize, Deserialize}; use self::serde_json::error::Error as SerdeError; @@ -83,15 +82,14 @@ impl FromData for JSON { } } -impl Responder for JSON { - fn respond<'a>(&mut self, res: FreshHyperResponse<'a>) -> response::Outcome<'a> { - match serde_json::to_string(&self.0) { - Ok(json_string) => content::JSON(json_string).respond(res), - Err(e) => { - error_!("JSON failed to serialize: {:?}", e); - Outcome::Forward((StatusCode::BadRequest, res)) - } - } +impl Responder<'static> for JSON { + fn respond(self) -> response::Result<'static> { + serde_json::to_string(&self.0).map(|string| { + content::JSON(string).respond().unwrap() + }).map_err(|e| { + error_!("JSON failed to serialize: {:?}", e); + Status::BadRequest + }) } } diff --git a/contrib/src/templates/mod.rs b/contrib/src/templates/mod.rs index 600abcfd..3029047b 100644 --- a/contrib/src/templates/mod.rs +++ b/contrib/src/templates/mod.rs @@ -18,9 +18,7 @@ use std::collections::HashMap; use rocket::config; use rocket::response::{self, Content, Responder}; -use rocket::http::hyper::FreshHyperResponse; -use rocket::http::{ContentType, StatusCode}; -use rocket::Outcome; +use rocket::http::{ContentType, Status}; /// The Template type implements generic support for template rendering in /// Rocket. @@ -159,16 +157,16 @@ impl Template { } } -impl Responder for Template { - fn respond<'a>(&mut self, res: FreshHyperResponse<'a>) -> response::Outcome<'a> { +impl Responder<'static> for Template { + fn respond(self) -> response::Result<'static> { let content_type = match self.1 { Some(ref ext) => ContentType::from_extension(ext), - None => ContentType::html() + None => ContentType::HTML }; match self.0 { - Some(ref render) => Content(content_type, render.as_str()).respond(res), - None => Outcome::Forward((StatusCode::InternalServerError, res)), + Some(render) => Content(content_type, render).respond(), + None => Err(Status::InternalServerError) } } } diff --git a/examples/forms/src/tests.rs b/examples/forms/src/tests.rs index 1db94183..2151521a 100644 --- a/examples/forms/src/tests.rs +++ b/examples/forms/src/tests.rs @@ -4,11 +4,11 @@ use rocket::http::Method::*; fn test_login bool>(username: &str, password: &str, age: isize, test: F) { let rocket = rocket::ignite().mount("/", routes![super::user_page, super::login]); - let req = MockRequest::new(Post, "/login") + let result = MockRequest::new(Post, "/login") .headers(&[("Content-Type", "application/x-www-form-urlencoded")]) - .body(&format!("username={}&password={}&age={}", username, password, age)); - let result = req.dispatch_with(&rocket); - let result = result.unwrap(); + .body(&format!("username={}&password={}&age={}", username, password, age)) + .dispatch_with(&rocket) + .unwrap_or("".to_string()); assert!(test(result)); } @@ -29,5 +29,7 @@ fn test_bad_login() { #[test] fn test_bad_form() { - test_login("Sergio&other=blah&", "password", 30, |s| s.contains("400 Bad Request")); + // FIXME: Need to be able to examine the status. + // test_login("Sergio&other=blah&", "password", 30, |s| s.contains("400 Bad Request")); + test_login("Sergio&other=blah&", "password", 30, |s| s.is_empty()); } diff --git a/examples/from_request/src/main.rs b/examples/from_request/src/main.rs index 4487e06d..ced097a2 100644 --- a/examples/from_request/src/main.rs +++ b/examples/from_request/src/main.rs @@ -39,8 +39,9 @@ mod test { use rocket::http::Method::*; fn test_header_count<'h>(headers: &[(&'h str, &'h str)]) { + // FIXME: Should be able to count headers directly! let rocket = rocket::ignite().mount("/", routes![super::header_count]); - let req = MockRequest::new(Get, "/").headers(headers); + let mut req = MockRequest::new(Get, "/").headers(headers); let result = req.dispatch_with(&rocket); assert_eq!(result.unwrap(), format!("Your request contained {} headers!", headers.len())); diff --git a/examples/hello_person/src/tests.rs b/examples/hello_person/src/tests.rs index a4ab3033..5809b6f5 100644 --- a/examples/hello_person/src/tests.rs +++ b/examples/hello_person/src/tests.rs @@ -4,17 +4,16 @@ use rocket::http::Method::*; fn test(uri: &str, expected: String) { let rocket = rocket::ignite().mount("/", routes![super::hello, super::hi]); - let req = MockRequest::new(Get, uri); - let result = req.dispatch_with(&rocket); + let result = MockRequest::new(Get, uri).dispatch_with(&rocket); assert_eq!(result.unwrap(), expected); } fn test_404(uri: &str) { let rocket = rocket::ignite().mount("/", routes![super::hello, super::hi]); - let req = MockRequest::new(Get, uri); - let result = req.dispatch_with(&rocket); - // TODO: Be able to check that actual HTTP response status code. - assert!(result.unwrap().contains("404 Not Found")); + let result = MockRequest::new(Get, uri).dispatch_with(&rocket); + // FIXME: Be able to check that actual HTTP response status code. + // assert!(result.unwrap().contains("404")); + assert!(result.is_none()); } #[test] diff --git a/examples/hello_ranks/src/tests.rs b/examples/hello_ranks/src/tests.rs index b419dcdb..3ad27fc2 100644 --- a/examples/hello_ranks/src/tests.rs +++ b/examples/hello_ranks/src/tests.rs @@ -4,8 +4,7 @@ use rocket::http::Method::*; fn test(uri: &str, expected: String) { let rocket = rocket::ignite().mount("/", routes![super::hello, super::hi]); - let req = MockRequest::new(Get, uri); - let result = req.dispatch_with(&rocket); + let result = MockRequest::new(Get, uri).dispatch_with(&rocket); assert_eq!(result.unwrap(), expected); } diff --git a/examples/hello_world/src/tests.rs b/examples/hello_world/src/tests.rs index b142f800..36ce61fa 100644 --- a/examples/hello_world/src/tests.rs +++ b/examples/hello_world/src/tests.rs @@ -5,7 +5,6 @@ use rocket::http::Method::*; #[test] fn hello_world() { let rocket = rocket::ignite().mount("/", routes![super::hello]); - let req = MockRequest::new(Get, "/"); - let result = req.dispatch_with(&rocket); + let result = MockRequest::new(Get, "/").dispatch_with(&rocket); assert_eq!(result.unwrap().as_str(), "Hello, world!"); } diff --git a/examples/manual_routes/Rocket.toml b/examples/manual_routes/Rocket.toml new file mode 100644 index 00000000..879c30d3 --- /dev/null +++ b/examples/manual_routes/Rocket.toml @@ -0,0 +1,3 @@ +[global] +port = 8000 + diff --git a/examples/manual_routes/src/main.rs b/examples/manual_routes/src/main.rs index be4271ab..5ed7d8b5 100644 --- a/examples/manual_routes/src/main.rs +++ b/examples/manual_routes/src/main.rs @@ -3,67 +3,74 @@ extern crate rocket; use std::io; use std::fs::File; -use rocket::{Request, Response, Route, Data, Catcher, Error}; -use rocket::http::StatusCode; +use rocket::{Request, Route, Data, Catcher, Error}; +use rocket::http::Status; use rocket::request::FromParam; +use rocket::response::{self, Responder}; +use rocket::handler::Outcome; use rocket::http::Method::*; -fn forward(_req: &Request, data: Data) -> Response<'static> { - Response::forward(data) +fn forward(_req: &Request, data: Data) -> Outcome { + Outcome::forward(data) } -fn hi(_req: &Request, _: Data) -> Response<'static> { - Response::success("Hello!") +fn hi(_req: &Request, _: Data) -> Outcome { + Outcome::of("Hello!") } -fn name<'a>(req: &'a Request, _: Data) -> Response<'a> { - Response::success(req.get_param(0).unwrap_or("unnamed")) +fn name<'a>(req: &'a Request, _: Data) -> Outcome { + Outcome::of(req.get_param(0).unwrap_or("unnamed")) } -fn echo_url<'a>(req: &'a Request, _: Data) -> Response<'a> { +fn echo_url(req: &Request, _: Data) -> Outcome<'static> { let param = req.uri().as_str().split_at(6).1; - Response::success(String::from_param(param)) + Outcome::of(String::from_param(param).unwrap()) } -fn upload(req: &Request, data: Data) -> Response { - if !req.content_type().is_text() { - println!(" => Content-Type of upload must be data. Ignoring."); - return Response::failure(StatusCode::BadRequest); +fn upload(req: &Request, data: Data) -> Outcome { + if !req.content_type().is_plain() { + println!(" => Content-Type of upload must be text/plain. Ignoring."); + return Outcome::failure(Status::BadRequest); } let file = File::create("/tmp/upload.txt"); if let Ok(mut file) = file { if let Ok(n) = io::copy(&mut data.open(), &mut file) { - return Response::success(format!("OK: {} bytes uploaded.", n)); + return Outcome::of(format!("OK: {} bytes uploaded.", n)); } println!(" => Failed copying."); - Response::failure(StatusCode::InternalServerError) + Outcome::failure(Status::InternalServerError) } else { println!(" => Couldn't open file: {:?}", file.unwrap_err()); - Response::failure(StatusCode::InternalServerError) + Outcome::failure(Status::InternalServerError) } } -fn not_found_handler(_: Error, req: &Request) -> Response { - Response::success(format!("Couldn't find: {}", req.uri())) +fn get_upload(_: &Request, _: Data) -> Outcome { + Outcome::of(File::open("/tmp/upload.txt").ok()) +} + +fn not_found_handler(_: Error, req: &Request) -> response::Result { + format!("Couldn't find: {}", req.uri()).respond() } fn main() { let always_forward = Route::ranked(1, Get, "/", forward); let hello = Route::ranked(2, Get, "/", hi); - let echo = Route::new(Get, "/", echo_url); + let echo = Route::new(Get, "/echo:", echo_url); let name = Route::new(Get, "/", name); - let upload_route = Route::new(Post, "/upload", upload); + let post_upload = Route::new(Post, "/", upload); + let get_upload = Route::new(Get, "/", get_upload); let not_found_catcher = Catcher::new(404, not_found_handler); rocket::ignite() - .mount("/", vec![always_forward, hello, upload_route]) + .mount("/", vec![always_forward, hello, echo]) + .mount("/upload", vec![get_upload, post_upload]) .mount("/hello", vec![name.clone()]) .mount("/hi", vec![name]) - .mount("/echo:", vec![echo]) .catch(vec![not_found_catcher]) .launch(); } diff --git a/examples/static_files/src/main.rs b/examples/static_files/src/main.rs index a9fba7cf..0655bc61 100644 --- a/examples/static_files/src/main.rs +++ b/examples/static_files/src/main.rs @@ -6,8 +6,7 @@ extern crate rocket; use std::io; use std::path::{Path, PathBuf}; -use rocket::response::{NamedFile, Failure}; -use rocket::http::StatusCode::NotFound; +use rocket::response::NamedFile; #[get("/")] fn index() -> io::Result { @@ -15,8 +14,8 @@ fn index() -> io::Result { } #[get("/")] -fn files(file: PathBuf) -> Result { - NamedFile::open(Path::new("static/").join(file)).map_err(|_| Failure(NotFound)) +fn files(file: PathBuf) -> Option { + NamedFile::open(Path::new("static/").join(file)).ok() } fn main() { diff --git a/examples/testing/src/main.rs b/examples/testing/src/main.rs index 2ada1e79..375f7dec 100644 --- a/examples/testing/src/main.rs +++ b/examples/testing/src/main.rs @@ -21,8 +21,7 @@ mod test { #[test] fn test_hello() { let rocket = rocket::ignite().mount("/", routes![super::hello]); - let req = MockRequest::new(Get, "/"); - let result = req.dispatch_with(&rocket); + let result = MockRequest::new(Get, "/").dispatch_with(&rocket); assert_eq!(result.unwrap().as_str(), "Hello, world!"); } } diff --git a/lib/src/catcher.rs b/lib/src/catcher.rs index 0446c1b5..9a630227 100644 --- a/lib/src/catcher.rs +++ b/lib/src/catcher.rs @@ -1,5 +1,5 @@ +use response; use handler::ErrorHandler; -use response::Response; use codegen::StaticCatchInfo; use error::Error; use request::Request; @@ -76,14 +76,15 @@ impl Catcher { /// # Examples /// /// ```rust - /// use rocket::{Catcher, Request, Error, Response}; + /// use rocket::{Catcher, Request, Error}; + /// use rocket::response::{Result, Responder}; /// - /// fn handle_404(_: Error, req: &Request) -> Response { - /// Response::success(format!("Couldn't find: {}", req.uri())) + /// fn handle_404(_: Error, req: &Request) -> Result { + /// format!("Couldn't find: {}", req.uri()).respond() /// } /// - /// fn handle_500(_: Error, _: &Request) -> Response { - /// Response::success("Whoops, we messed up!") + /// fn handle_500(_: Error, _: &Request) -> Result { + /// "Whoops, we messed up!".respond() /// } /// /// let not_found_catcher = Catcher::new(404, handle_404); @@ -96,8 +97,8 @@ impl Catcher { #[doc(hidden)] #[inline(always)] - pub fn handle<'r>(&self, err: Error, request: &'r Request) -> Response<'r> { - (self.handler)(err, request) + pub fn handle<'r>(&self, err: Error, req: &'r Request) -> response::Result<'r> { + (self.handler)(err, req) } #[inline(always)] @@ -153,10 +154,10 @@ macro_rules! default_errors { let mut map = HashMap::new(); $( - fn $fn_name<'r>(_: Error, _r: &'r Request) -> Response<'r> { - Response::with_raw_status($code, + fn $fn_name<'r>(_: Error, _r: &'r Request) -> response::Result<'r> { + status::Custom(Status::from_code($code).unwrap(), content::HTML(error_page_template!($code, $name, $description)) - ) + ).respond() } map.insert($code, Catcher::new_default($code, $fn_name)); @@ -172,7 +173,8 @@ pub mod defaults { use std::collections::HashMap; use request::Request; - use response::{Response, content}; + use response::{self, content, status, Responder}; + use http::Status; use error::Error; pub fn get() -> HashMap { diff --git a/lib/src/data/data.rs b/lib/src/data/data.rs index 3785ebd8..b892ea42 100644 --- a/lib/src/data/data.rs +++ b/lib/src/data/data.rs @@ -5,6 +5,8 @@ use std::time::Duration; use std::mem::transmute; use super::data_stream::{DataStream, StreamReader, kill_stream}; + +use ext::ReadExt; use http::hyper::{HyperBodyReader, HyperHttpStream}; use http::hyper::HyperNetworkStream; use http::hyper::HyperHttpReader::*; @@ -114,7 +116,8 @@ impl Data { } /// Returns true if the `peek` buffer contains all of the data in the body - /// of the request. + /// of the request. Returns `false` if it does not or if it is not known if + /// it does. #[inline(always)] pub fn peek_complete(&self) -> bool { self.is_done @@ -150,45 +153,27 @@ impl Data { // Make sure the buffer is large enough for the bytes we want to peek. const PEEK_BYTES: usize = 4096; if buf.len() < PEEK_BYTES { - trace!("Resizing peek buffer from {} to {}.", buf.len(), PEEK_BYTES); + trace_!("Resizing peek buffer from {} to {}.", buf.len(), PEEK_BYTES); buf.resize(PEEK_BYTES, 0); } - // We want to fill the buffer with as many bytes as possible. We also - // want to record if we reach the EOF while filling the buffer. The - // buffer already has `cap` bytes. We read up to buf.len() - 1 bytes, - // and then we try reading 1 more to see if we've reached the EOF. + // Fill the buffer with as many bytes as possible. If we read less than + // that buffer's length, we know we reached the EOF. Otherwise, it's + // unclear, so we just say we didn't reach EOF. trace!("Init buffer cap: {}", cap); - let buf_len = buf.len(); - let eof = if cap < buf_len { - // We have room to read into the buffer. Let's do it. - match stream.read(&mut buf[cap..(buf_len - 1)]) { - Ok(0) => true, - Ok(n) => { - trace!("Filled peek buf with {} bytes.", n); - cap += n; - match stream.read(&mut buf[cap..(cap + 1)]) { - Ok(n) => { - cap += n; - n == 0 - } - Err(e) => { - error_!("Failed to check stream EOF status: {:?}", e); - false - } - } - } - Err(e) => { - error_!("Failed to read into peek buffer: {:?}", e); - false - } + let eof = match stream.read_max(&mut buf[cap..]) { + Ok(n) => { + trace_!("Filled peek buf with {} bytes.", n); + cap += n; + cap < buf.len() } - } else { - // There's no more room in the buffer. Assume there are still bytes. - false + Err(e) => { + error_!("Failed to read into peek buffer: {:?}.", e); + false + }, }; - trace!("Peek buffer size: {}, remaining: {}", buf_len, buf_len - cap); + trace_!("Peek buffer size: {}, remaining: {}", buf.len(), buf.len() - cap); Data { buffer: buf, stream: stream, diff --git a/lib/src/data/from_data.rs b/lib/src/data/from_data.rs index 856cf01d..9fd6af1b 100644 --- a/lib/src/data/from_data.rs +++ b/lib/src/data/from_data.rs @@ -1,17 +1,17 @@ use outcome::{self, IntoOutcome}; use outcome::Outcome::*; -use http::StatusCode; +use http::Status; use request::Request; use data::Data; /// Type alias for the `Outcome` of a `FromData` conversion. -pub type Outcome = outcome::Outcome; +pub type Outcome = outcome::Outcome; -impl<'a, S, E> IntoOutcome for Result { +impl<'a, S, E> IntoOutcome for Result { fn into_outcome(self) -> Outcome { match self { Ok(val) => Success(val), - Err(err) => Failure((StatusCode::InternalServerError, err)) + Err(err) => Failure((Status::InternalServerError, err)) } } } @@ -39,7 +39,7 @@ impl<'a, S, E> IntoOutcome for Result { /// the value for the data parameter. As long as all other parsed types /// succeed, the request will be handled by the requesting handler. /// -/// * **Failure**(StatusCode, E) +/// * **Failure**(Status, E) /// /// If the `Outcome` is `Failure`, the request will fail with the given status /// code and error. The designated error diff --git a/lib/src/ext.rs b/lib/src/ext.rs new file mode 100644 index 00000000..87bd03c2 --- /dev/null +++ b/lib/src/ext.rs @@ -0,0 +1,20 @@ +use std::io; + +pub trait ReadExt: io::Read { + fn read_max(&mut self, mut buf: &mut [u8]) -> io::Result { + let start_len = buf.len(); + while !buf.is_empty() { + match self.read(buf) { + Ok(0) => break, + Ok(n) => { let tmp = buf; buf = &mut tmp[n..]; } + Err(ref e) if e.kind() == io::ErrorKind::Interrupted => {} + Err(e) => return Err(e), + } + } + + return Ok(start_len - buf.len()) + } +} + +impl ReadExt for T { } + diff --git a/lib/src/handler.rs b/lib/src/handler.rs new file mode 100644 index 00000000..aec148bc --- /dev/null +++ b/lib/src/handler.rs @@ -0,0 +1,35 @@ +use data::Data; +use request::Request; +use response::{self, Response, Responder}; +use error::Error; +use http::Status; +use outcome; + +/// Type alias for the `Outcome` of a `Handler`. +pub type Outcome<'r> = outcome::Outcome, Status, Data>; + +impl<'r> Outcome<'r> { + #[inline] + pub fn of>(responder: T) -> Outcome<'r> { + match responder.respond() { + Ok(response) => outcome::Outcome::Success(response), + Err(status) => outcome::Outcome::Failure(status) + } + } + + #[inline(always)] + pub fn failure(code: Status) -> Outcome<'static> { + outcome::Outcome::Failure(code) + } + + #[inline(always)] + pub fn forward(data: Data) -> Outcome<'static> { + outcome::Outcome::Forward(data) + } +} + +/// The type of a request handler. +pub type Handler = for<'r> fn(&'r Request, Data) -> Outcome<'r>; + +/// The type of an error handler. +pub type ErrorHandler = for<'r> fn(Error, &'r Request) -> response::Result<'r>; diff --git a/lib/src/http/content_type.rs b/lib/src/http/content_type.rs index f3b46340..dff255bf 100644 --- a/lib/src/http/content_type.rs +++ b/lib/src/http/content_type.rs @@ -1,10 +1,9 @@ -use std::default::Default; - +use std::borrow::{Borrow, Cow}; use std::str::FromStr; -use std::borrow::Borrow; use std::fmt; -use http::mime::{Mime, Param, Attr, Value, TopLevel, SubLevel}; +use http::Header; +use http::mime::Mime; use router::Collider; /// Typed representation of HTTP Content-Types. @@ -14,69 +13,90 @@ use router::Collider; /// provides methods to parse HTTP Content-Type values /// ([from_str](#method.from_str)) and to return the ContentType associated with /// a file extension ([from_ext](#method.from_extension)). -#[derive(Debug, Clone, PartialEq)] -pub struct ContentType(pub TopLevel, pub SubLevel, pub Option>); - -macro_rules! ctrs { - ($($(#[$attr:meta])* | $name:ident: $top:ident/$sub:ident),+) => { - $ - ($(#[$attr])* - #[inline(always)] - pub fn $name() -> ContentType { - ContentType::of(TopLevel::$top, SubLevel::$sub) - })+ - }; +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct ContentType { + pub ttype: Cow<'static, str>, + pub subtype: Cow<'static, str>, + pub params: Option> } -macro_rules! checkers { - ($($(#[$attr:meta])* | $name:ident: $top:ident/$sub:ident),+) => { +macro_rules! ctr_params { + () => (None); + ($param:expr) => (Some(Cow::Borrowed($param))); +} + +macro_rules! ctrs { + ($($str:expr, $name:ident, $check_name:ident => + $top:expr, $sub:expr $(; $param:expr),*),+) => { $( - $(#[$attr])* - #[inline(always)] - pub fn $name(&self) -> bool { - self.0 == TopLevel::$top && self.1 == SubLevel::$sub - })+ + #[doc="[ContentType](struct.ContentType.html) for "] + #[doc=$str] + #[doc=": "] + #[doc=$top] + #[doc="/"] + #[doc=$sub] + $(#[doc="; "] #[doc=$param])* + #[doc=""] + #[allow(non_upper_case_globals)] + pub const $name: ContentType = ContentType { + ttype: Cow::Borrowed($top), + subtype: Cow::Borrowed($sub), + params: ctr_params!($($param)*) + }; + )+ + + /// Returns `true` if this ContentType is known to Rocket. + pub fn is_known(&self) -> bool { + match (&*self.ttype, &*self.subtype) { + $( + ($top, $sub) => true, + )+ + _ => false + } + } + + $( + #[doc="Returns `true` if `self` is a "] + #[doc=$str] + #[doc=" ContentType: "] + #[doc=$top] + #[doc="/"] + #[doc=$sub] + #[doc="."] + /// Paramaters are not taken into account when doing that check. + pub fn $check_name(&self) -> bool { + self.ttype == $top && self.subtype == $sub + } + )+ }; } impl ContentType { - #[doc(hidden)] #[inline(always)] - pub fn new(t: TopLevel, s: SubLevel, params: Option>) -> ContentType { - ContentType(t, s, params) - } - - /// Constructs a new content type of the given top level and sub level - /// types. If the top-level type is `Text`, a charset of UTF-8 is set. - /// - /// # Examples - /// - /// ```rust - /// use rocket::http::ContentType; - /// use rocket::http::mime::{TopLevel, SubLevel}; - /// - /// let ct = ContentType::of(TopLevel::Text, SubLevel::Html); - /// assert_eq!(ct.to_string(), "text/html; charset=utf-8".to_string()); - /// assert!(ct.is_html()); - /// ``` - /// - /// ```rust - /// use rocket::http::ContentType; - /// use rocket::http::mime::{TopLevel, SubLevel}; - /// - /// let ct = ContentType::of(TopLevel::Application, SubLevel::Json); - /// assert_eq!(ct.to_string(), "application/json".to_string()); - /// assert!(ct.is_json()); - /// ``` - #[inline(always)] - pub fn of(t: TopLevel, s: SubLevel) -> ContentType { - if t == TopLevel::Text { - ContentType(t, s, Some(vec![(Attr::Charset, Value::Utf8)])) - } else { - ContentType(t, s, None) + pub fn new(ttype: T, subtype: S) -> ContentType + where T: Into>, S: Into> + { + ContentType { + ttype: ttype.into(), + subtype: subtype.into(), + params: None } } + #[inline(always)] + pub fn with_params(ttype: T, subtype: S, params: Option

) -> ContentType + where T: Into>, + S: Into>, + P: Into> + { + ContentType { + ttype: ttype.into(), + subtype: subtype.into(), + params: params.map(|p| p.into()) + } + } + + /// Returns the Content-Type associated with the extension `ext`. Not all /// extensions are recognized. If an extensions is not recognized, then this /// method returns a ContentType of `any`. The currently recognized @@ -103,77 +123,37 @@ impl ContentType { /// assert!(foo.is_any()); /// ``` pub fn from_extension(ext: &str) -> ContentType { - let (top_level, sub_level) = match ext { - "txt" => (TopLevel::Text, SubLevel::Plain), - "html" | "htm" => (TopLevel::Text, SubLevel::Html), - "xml" => (TopLevel::Text, SubLevel::Xml), - "js" => (TopLevel::Application, SubLevel::Javascript), - "css" => (TopLevel::Text, SubLevel::Css), - "json" => (TopLevel::Application, SubLevel::Json), - "png" => (TopLevel::Image, SubLevel::Png), - "gif" => (TopLevel::Image, SubLevel::Gif), - "bmp" => (TopLevel::Image, SubLevel::Bmp), - "jpeg" => (TopLevel::Image, SubLevel::Jpeg), - "jpg" => (TopLevel::Image, SubLevel::Jpeg), - "pdf" => (TopLevel::Application, SubLevel::Ext("pdf".into())), - _ => (TopLevel::Star, SubLevel::Star), - }; - - ContentType::of(top_level, sub_level) - } - - ctrs! { - /// Returns a `ContentType` representing `*/*`, i.e., _any_ ContentType. - | any: Star/Star, - - /// Returns a `ContentType` representing JSON, i.e, `application/json`. - | json: Application/Json, - - /// Returns a `ContentType` representing XML, i.e, `text/xml`. - | xml: Text/Xml, - - /// Returns a `ContentType` representing HTML, i.e, `text/html`. - | html: Text/Html, - - /// Returns a `ContentType` representing plain text, i.e, `text/plain`. - | plain: Text/Plain - } - - /// Returns true if this content type is not one of the standard content - /// types, that if, if it is an "extended" content type. - pub fn is_ext(&self) -> bool { - if let TopLevel::Ext(_) = self.0 { - true - } else if let SubLevel::Ext(_) = self.1 { - true - } else { - false + match ext { + "txt" => ContentType::Plain, + "html" | "htm" => ContentType::HTML, + "xml" => ContentType::XML, + "js" => ContentType::JavaScript, + "css" => ContentType::CSS, + "json" => ContentType::JSON, + "png" => ContentType::PNG, + "gif" => ContentType::GIF, + "bmp" => ContentType::BMP, + "jpeg" | "jpg" => ContentType::JPEG, + "pdf" => ContentType::PDF, + _ => ContentType::Any } } - checkers! { - /// Returns true if the content type is plain text, i.e.: `text/plain`. - | is_text: Text/Plain, - - /// Returns true if the content type is JSON, i.e: `application/json`. - | is_json: Application/Json, - - /// Returns true if the content type is XML, i.e: `text/xml`. - | is_xml: Text/Xml, - - /// Returns true if the content type is any, i.e.: `*/*`. - | is_any: Star/Star, - - /// Returns true if the content type is HTML, i.e.: `text/html`. - | is_html: Text/Html, - - /// Returns true if the content type is that for non-data HTTP forms, - /// i.e.: `application/x-www-form-urlencoded`. - | is_form: Application/WwwFormUrlEncoded, - - /// Returns true if the content type is that for data HTTP forms, i.e.: - /// `multipart/form-data`. - | is_form_data: Multipart/FormData + ctrs! { + "any", Any, is_any => "*", "*", + "form", Form, is_form => "application", "x-www-form-urlencoded", + "data form", DataForm, is_data_form => "multipart", "form-data", + "JSON", JSON, is_json => "application", "json", + "XML", XML, is_xml => "text", "xml"; "charset=utf-8", + "HTML", HTML, is_html => "text", "html"; "charset=utf-8", + "Plain", Plain, is_plain => "text", "plain"; "charset=utf-8", + "JavaScript", JavaScript, is_javascript => "application", "javascript", + "CSS", CSS, is_css => "text", "css"; "charset=utf-8", + "PNG", PNG, is_png => "image", "png", + "GIF", GIF, is_gif => "image", "gif", + "BMP", BMP, is_bmp => "image", "bmp", + "JPEG", JPEG, is_jpeg => "image", "jpeg", + "PDF", PDF, is_pdf => "application", "pdf" } } @@ -181,14 +161,7 @@ impl Default for ContentType { /// Returns a ContentType of `any`, or `*/*`. #[inline(always)] fn default() -> ContentType { - ContentType::any() - } -} - -#[doc(hidden)] -impl Into for ContentType { - fn into(self) -> Mime { - Mime(self.0, self.1, self.2.unwrap_or_default()) + ContentType::Any } } @@ -205,10 +178,15 @@ impl From for ContentType { fn from(mime: Mime) -> ContentType { let params = match mime.2.len() { 0 => None, - _ => Some(mime.2), + _ => { + Some(mime.2.into_iter() + .map(|(attr, value)| format!("{}={}", attr, value)) + .collect::>() + .join("; ")) + } }; - ContentType(mime.0, mime.1, params) + ContentType::with_params(mime.0.to_string(), mime.1.to_string(), params) } } @@ -239,8 +217,8 @@ impl FromStr for ContentType { /// use std::str::FromStr; /// use rocket::http::ContentType; /// - /// let json = ContentType::from_str("application/json"); - /// assert_eq!(json, Ok(ContentType::json())); + /// let json = ContentType::from_str("application/json").unwrap(); + /// assert_eq!(json, ContentType::JSON); /// ``` /// /// Parsing a content-type extension: @@ -251,9 +229,9 @@ impl FromStr for ContentType { /// use rocket::http::mime::{TopLevel, SubLevel}; /// /// let custom = ContentType::from_str("application/x-custom").unwrap(); - /// assert!(custom.is_ext()); - /// assert_eq!(custom.0, TopLevel::Application); - /// assert_eq!(custom.1, SubLevel::Ext("x-custom".into())); + /// assert!(!custom.is_known()); + /// assert_eq!(custom.ttype, "application"); + /// assert_eq!(custom.subtype, "x-custom"); /// ``` /// /// Parsing an invalid Content-Type value: @@ -291,12 +269,10 @@ impl FromStr for ContentType { return Err("Invalid character in string."); } - let (top_s, sub_s) = (&*top_s.to_lowercase(), &*sub_s.to_lowercase()); - let top_level = TopLevel::from_str(top_s).map_err(|_| "Bad TopLevel")?; - let sub_level = SubLevel::from_str(sub_s).map_err(|_| "Bad SubLevel")?; + let (top_s, sub_s) = (top_s.to_lowercase(), sub_s.to_lowercase()); // FIXME: Use `rest` to find params. - Ok(ContentType::new(top_level, sub_level, None)) + Ok(ContentType::new(top_s, sub_s)) } } @@ -308,53 +284,44 @@ impl fmt::Display for ContentType { /// ```rust /// use rocket::http::ContentType; /// - /// let ct = format!("{}", ContentType::json()); - /// assert_eq!(ct, "application/json".to_string()); + /// let ct = format!("{}", ContentType::JSON); + /// assert_eq!(ct, "application/json"); /// ``` fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}/{}", self.0.as_str(), self.1.as_str())?; + write!(f, "{}/{}", self.ttype, self.subtype)?; - self.2.as_ref().map_or(Ok(()), |params| { - for param in params.iter() { - let (ref attr, ref value) = *param; - write!(f, "; {}={}", attr, value)?; - } + if let Some(ref params) = self.params { + write!(f, "; {}", params)?; + } - Ok(()) - }) + Ok(()) + } +} + +impl Into> for ContentType { + #[inline] + fn into(self) -> Header<'static> { + Header::new("Content-Type", self.to_string()) } } impl Collider for ContentType { fn collides_with(&self, other: &ContentType) -> bool { - self.0.collides_with(&other.0) && self.1.collides_with(&other.1) - } -} - -impl Collider for TopLevel { - fn collides_with(&self, other: &TopLevel) -> bool { - *self == TopLevel::Star || *other == TopLevel::Star || *self == *other - } -} - -impl Collider for SubLevel { - fn collides_with(&self, other: &SubLevel) -> bool { - *self == SubLevel::Star || *other == SubLevel::Star || *self == *other + (self.ttype == "*" || other.ttype == "*" || self.ttype == other.ttype) && + (self.subtype == "*" || other.subtype == "*" || self.subtype == other.subtype) } } #[cfg(test)] mod test { use super::ContentType; - use hyper::mime::{TopLevel, SubLevel}; use std::str::FromStr; - macro_rules! assert_no_parse { ($string:expr) => ({ let result = ContentType::from_str($string); if !result.is_err() { - println!("{} parsed!", $string); + println!("{} parsed unexpectedly!", $string); } assert!(result.is_err()); @@ -367,29 +334,34 @@ mod test { assert!(result.is_ok()); result.unwrap() }); - ($string:expr, $top:tt/$sub:tt) => ({ + + ($string:expr, $ct:expr) => ({ let c = assert_parse!($string); - assert_eq!(c.0, TopLevel::$top); - assert_eq!(c.1, SubLevel::$sub); + assert_eq!(c.ttype, $ct.ttype); + assert_eq!(c.subtype, $ct.subtype); c }) } #[test] fn test_simple() { - assert_parse!("application/json", Application/Json); - assert_parse!("*/json", Star/Json); - assert_parse!("text/html", Text/Html); - assert_parse!("TEXT/html", Text/Html); - assert_parse!("*/*", Star/Star); - assert_parse!("application/*", Application/Star); + assert_parse!("application/json", ContentType::JSON); + assert_parse!("*/json", ContentType::new("*", "json")); + assert_parse!("text/html", ContentType::HTML); + assert_parse!("TEXT/html", ContentType::HTML); + assert_parse!("*/*", ContentType::Any); + assert_parse!("application/*", ContentType::new("application", "*")); } #[test] fn test_params() { - assert_parse!("application/json; charset=utf8", Application/Json); - assert_parse!("application/*;charset=utf8;else=1", Application/Star); - assert_parse!("*/*;charset=utf8;else=1", Star/Star); + // TODO: Test these. + assert_parse!("application/json; charset=utf-8", ContentType::JSON); + assert_parse!("*/*;", ContentType::Any); + assert_parse!("application/*;else=1", + ContentType::with_params("application", "*", Some("else=1"))); + assert_parse!("*/*;charset=utf8;else=1", + ContentType::with_params("*", "*", Some("charset=utf-8;else=1"))); } #[test] @@ -403,5 +375,6 @@ mod test { assert_no_parse!("*/"); assert_no_parse!("/*"); assert_no_parse!("///"); + assert_no_parse!(""); } } diff --git a/lib/src/http/header.rs b/lib/src/http/header.rs new file mode 100644 index 00000000..5b380268 --- /dev/null +++ b/lib/src/http/header.rs @@ -0,0 +1,37 @@ +use std::borrow::Cow; +use http::hyper::header::Header as HyperHeader; +use http::hyper::header::HeaderFormat as HyperHeaderFormat; +use http::hyper::header::HeaderFormatter as HyperHeaderFormatter; +use std::fmt; + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct Header<'h> { + pub name: Cow<'h, str>, + pub value: Cow<'h, str>, +} + +impl<'h> Header<'h> { + #[inline(always)] + pub fn new<'a: 'h, 'b: 'h, N, V>(name: N, value: V) -> Header<'h> + where N: Into>, V: Into> + { + Header { + name: name.into(), + value: value.into() + } + } +} + +impl<'h> fmt::Display for Header<'h> { + #[inline(always)] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}: {}", self.name, self.value) + } +} + +impl From for Header<'static> where T: HyperHeader + HyperHeaderFormat { + fn from(hyper_header: T) -> Header<'static> { + let formatter = HyperHeaderFormatter(&hyper_header); + Header::new(T::header_name(), format!("{}", formatter)) + } +} diff --git a/lib/src/http/hyper.rs b/lib/src/http/hyper.rs index 111f28df..74a9aa91 100644 --- a/lib/src/http/hyper.rs +++ b/lib/src/http/hyper.rs @@ -26,7 +26,6 @@ pub use hyper::net::NetworkStream as HyperNetworkStream; pub use hyper::http::h1::HttpReader as HyperHttpReader; pub use hyper::header; -// This is okay for now. pub use hyper::status::StatusCode; // TODO: Remove from Rocket in favor of a more flexible HTTP library. diff --git a/lib/src/http/mod.rs b/lib/src/http/mod.rs index a3751d43..962af220 100644 --- a/lib/src/http/mod.rs +++ b/lib/src/http/mod.rs @@ -11,12 +11,15 @@ pub mod uri; mod cookies; mod method; mod content_type; +mod status; +mod header; // TODO: Removed from Rocket in favor of a more flexible HTTP library. pub use hyper::mime; pub use self::method::Method; -pub use self::hyper::StatusCode; pub use self::content_type::ContentType; +pub use self::status::Status; +pub use self::header::Header; pub use self::cookies::{Cookie, Cookies}; diff --git a/lib/src/http/status.rs b/lib/src/http/status.rs new file mode 100644 index 00000000..fcb97941 --- /dev/null +++ b/lib/src/http/status.rs @@ -0,0 +1,139 @@ +use std::fmt; + +pub enum Class { + Informational, + Success, + Redirection, + ClientError, + ServerError, + Unknown +} + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub struct Status { + /// The HTTP status code associated with this status. + pub code: u16, + /// The HTTP reason phrase associated with this status. + pub reason: &'static str +} + +macro_rules! ctrs { + ($($code:expr, $code_str:expr, $name:ident => $reason:expr),+) => { + pub fn from_code(code: u16) -> Option { + match code { + $($code => Some(Status::$name),)+ + _ => None + } + } + + $( + #[doc="[Status](struct.Status.html) with code "] + #[doc=$code_str] + #[doc=" and reason "] + #[doc=$reason] + #[doc="."] + #[allow(non_upper_case_globals)] + pub const $name: Status = Status::new($code, $reason); + )+ + }; +} + +impl Status { + #[inline(always)] + pub const fn new(code: u16, reason: &'static str) -> Status { + Status { + code: code, + reason: reason + } + } + + pub fn class(&self) -> Class { + match self.code / 100 { + 1 => Class::Informational, + 2 => Class::Success, + 3 => Class::Redirection, + 4 => Class::ClientError, + 5 => Class::ServerError, + _ => Class::Unknown + } + } + + ctrs! { + 100, "100", Continue => "Continue", + 101, "101", SwitchingProtocols => "Switching Protocols", + 102, "102", Processing => "Processing", + 200, "200", Ok => "OK", + 201, "201", Created => "Created", + 202, "202", Accepted => "Accepted", + 203, "203", NonAuthoritativeInformation => "Non-Authoritative Information", + 204, "204", NoContent => "No Content", + 205, "205", ResetContent => "Reset Content", + 206, "206", PartialContent => "Partial Content", + 207, "207", MultiStatus => "Multi-Status", + 208, "208", AlreadyReported => "Already Reported", + 226, "226", ImUsed => "IM Used", + 300, "300", MultipleChoices => "Multiple Choices", + 301, "301", MovedPermanently => "Moved Permanently", + 302, "302", Found => "Found", + 303, "303", SeeOther => "See Other", + 304, "304", NotModified => "Not Modified", + 305, "305", UseProxy => "Use Proxy", + 307, "307", TemporaryRedirect => "Temporary Redirect", + 308, "308", PermanentRedirect => "Permanent Redirect", + 400, "400", BadRequest => "Bad Request", + 401, "401", Unauthorized => "Unauthorized", + 402, "402", PaymentRequired => "Payment Required", + 403, "403", Forbidden => "Forbidden", + 404, "404", NotFound => "Not Found", + 405, "405", MethodNotAllowed => "Method Not Allowed", + 406, "406", NotAcceptable => "Not Acceptable", + 407, "407", ProxyAuthenticationRequired => "Proxy Authentication Required", + 408, "408", RequestTimeout => "Request Timeout", + 409, "409", Conflict => "Conflict", + 410, "410", Gone => "Gone", + 411, "411", LengthRequired => "Length Required", + 412, "412", PreconditionFailed => "Precondition Failed", + 413, "413", PayloadTooLarge => "Payload Too Large", + 414, "414", UriTooLong => "URI Too Long", + 415, "415", UnsupportedMediaType => "Unsupported Media Type", + 416, "416", RangeNotSatisfiable => "Range Not Satisfiable", + 417, "417", ExpectationFailed => "Expectation Failed", + 418, "418", ImATeapot => "I'm a teapot", + 421, "421", MisdirectedRequest => "Misdirected Request", + 422, "422", UnprocessableEntity => "Unprocessable Entity", + 423, "423", Locked => "Locked", + 424, "424", FailedDependency => "Failed Dependency", + 426, "426", UpgradeRequired => "Upgrade Required", + 428, "428", PreconditionRequired => "Precondition Required", + 429, "429", TooManyRequests => "Too Many Requests", + 431, "431", RequestHeaderFieldsTooLarge => "Request Header Fields Too Large", + 451, "451", UnavailableForLegalReasons => "Unavailable For Legal Reasons", + 500, "500", InternalServerError => "Internal Server Error", + 501, "501", NotImplemented => "Not Implemented", + 502, "502", BadGateway => "Bad Gateway", + 503, "503", ServiceUnavailable => "Service Unavailable", + 504, "504", GatewayTimeout => "Gateway Timeout", + 505, "505", HttpVersionNotSupported => "HTTP Version Not Supported", + 506, "506", VariantAlsoNegotiates => "Variant Also Negotiates", + 507, "507", InsufficientStorage => "Insufficient Storage", + 508, "508", LoopDetected => "Loop Detected", + 510, "510", NotExtended => "Not Extended", + 511, "511", NetworkAuthenticationRequired => "Network Authentication Required" + } + + #[doc(hidden)] + #[inline] + pub fn raw(code: u16) -> Status { + match Status::from_code(code) { + Some(status) => status, + None => Status::new(code, "") + } + } +} + +impl fmt::Display for Status { + #[inline(always)] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} {}", self.code, self.reason) + } +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 57525ede..0e42b1fc 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,6 +1,8 @@ #![feature(specialization)] #![feature(conservative_impl_trait)] #![feature(drop_types_in_const)] +#![feature(associated_consts)] +#![feature(const_fn)] //! # Rocket - Core API Documentation //! @@ -103,27 +105,14 @@ pub mod response; pub mod outcome; pub mod config; pub mod data; +pub mod handler; mod error; mod router; mod rocket; mod codegen; mod catcher; - -/// Defines the types for request and error handlers. -#[doc(hidden)] -pub mod handler { - use data::Data; - use request::Request; - use response::Response; - use error::Error; - - /// The type of a request handler. - pub type Handler = for<'r> fn(&'r Request, Data) -> Response<'r>; - - /// The type of an error handler. - pub type ErrorHandler = for<'r> fn(Error, &'r Request) -> Response<'r>; -} +mod ext; #[doc(inline)] pub use response::Response; #[doc(inline)] pub use handler::{Handler, ErrorHandler}; diff --git a/lib/src/request/form/mod.rs b/lib/src/request/form/mod.rs index 8d34f26a..11cfa6d2 100644 --- a/lib/src/request/form/mod.rs +++ b/lib/src/request/form/mod.rs @@ -27,7 +27,7 @@ use std::marker::PhantomData; use std::fmt::{self, Debug}; use std::io::Read; -use http::StatusCode; +use http::Status; use request::Request; use data::{self, Data, FromData}; use outcome::Outcome::*; @@ -242,13 +242,13 @@ impl<'f, T: FromForm<'f>> FromData for Form<'f, T> where T::Error: Debug { let mut stream = data.open().take(32768); if let Err(e) = stream.read_to_string(&mut form_string) { error_!("IO Error: {:?}", e); - Failure((StatusCode::InternalServerError, None)) + Failure((Status::InternalServerError, None)) } else { match Form::new(form_string) { Ok(form) => Success(form), Err((form_string, e)) => { error_!("Failed to parse value from form: {:?}", e); - Failure((StatusCode::BadRequest, Some(form_string))) + Failure((Status::BadRequest, Some(form_string))) } } } diff --git a/lib/src/request/from_request.rs b/lib/src/request/from_request.rs index bf058cd3..06ca190b 100644 --- a/lib/src/request/from_request.rs +++ b/lib/src/request/from_request.rs @@ -3,16 +3,16 @@ use std::fmt::Debug; use outcome::{self, IntoOutcome}; use request::Request; use outcome::Outcome::*; -use http::{StatusCode, ContentType, Method, Cookies}; +use http::{Status, ContentType, Method, Cookies}; /// Type alias for the `Outcome` of a `FromRequest` conversion. -pub type Outcome = outcome::Outcome; +pub type Outcome = outcome::Outcome; -impl IntoOutcome for Result { +impl IntoOutcome for Result { fn into_outcome(self) -> Outcome { match self { Ok(val) => Success(val), - Err(val) => Failure((StatusCode::BadRequest, val)) + Err(val) => Failure((Status::BadRequest, val)) } } } @@ -49,7 +49,7 @@ impl IntoOutcome for Result { /// the value for the corresponding parameter. As long as all other parsed /// types succeed, the request will be handled. /// -/// * **Failure**(StatusCode, E) +/// * **Failure**(Status, E) /// /// If the `Outcome` is `Failure`, the request will fail with the given status /// code and error. The designated error @@ -78,7 +78,7 @@ impl IntoOutcome for Result { /// # extern crate rocket; /// # /// use rocket::Outcome; -/// use rocket::http::StatusCode; +/// use rocket::http::Status; /// use rocket::request::{self, Request, FromRequest}; /// /// struct APIKey(String); @@ -93,7 +93,7 @@ impl IntoOutcome for Result { /// fn from_request(request: &'r Request) -> request::Outcome { /// if let Some(keys) = request.headers().get_raw("x-api-key") { /// if keys.len() != 1 { -/// return Outcome::Failure((StatusCode::BadRequest, ())); +/// return Outcome::Failure((Status::BadRequest, ())); /// } /// /// if let Ok(key) = String::from_utf8(keys[0].clone()) { diff --git a/lib/src/request/request.rs b/lib/src/request/request.rs index b2a3d7ca..1b4e2dde 100644 --- a/lib/src/request/request.rs +++ b/lib/src/request/request.rs @@ -140,7 +140,7 @@ impl Request { #[inline(always)] pub fn content_type(&self) -> ContentType { let hyp_ct = self.headers().get::(); - hyp_ct.map_or(ContentType::any(), |ct| ContentType::from(&ct.0)) + hyp_ct.map_or(ContentType::Any, |ct| ContentType::from(&ct.0)) } ///

@@ -154,10 +154,10 @@ impl Request { /// Returns the first content-type accepted by this request. pub fn accepts(&self) -> ContentType { let accept = self.headers().get::(); - accept.map_or(ContentType::any(), |accept| { + accept.map_or(ContentType::Any, |accept| { let items = &accept.0; if items.len() < 1 { - return ContentType::any(); + return ContentType::Any; } else { return ContentType::from(items[0].item.clone()); } diff --git a/lib/src/response/content.rs b/lib/src/response/content.rs index d89a752b..83a191c1 100644 --- a/lib/src/response/content.rs +++ b/lib/src/response/content.rs @@ -21,10 +21,8 @@ //! let response = content::HTML("

Hello, world!

"); //! ``` -use response::{Responder, Outcome}; -use http::hyper::{header, FreshHyperResponse}; -use http::mime::{Mime, TopLevel, SubLevel}; -use http::ContentType; +use response::{Response, Responder}; +use http::{Status, ContentType}; /// Set the Content-Type to any arbitrary value. /// @@ -41,57 +39,50 @@ use http::ContentType; /// let response = Content(ContentType::from_extension("pdf"), "Hi."); /// ``` #[derive(Debug)] -pub struct Content(pub ContentType, pub T); +pub struct Content(pub ContentType, pub R); /// Sets the Content-Type of the response to the wrapped `ContentType` then /// delegates the remainder of the response to the wrapped responder. -impl Responder for Content { - fn respond<'b>(&mut self, mut res: FreshHyperResponse<'b>) -> Outcome<'b> { - res.headers_mut().set(header::ContentType(self.0.clone().into())); - self.1.respond(res) +impl<'r, R: Responder<'r>> Responder<'r> for Content { + #[inline(always)] + fn respond(self) -> Result, Status> { + Response::build() + .merge(self.1.respond()?) + .header(self.0) + .ok() } } macro_rules! ctrs { - ($($(#[$attr:meta])* | $name:ident: $top:ident/$sub:ident),+) => { + ($($name:ident: $name_str:expr, $ct_str:expr),+) => { $( - $(#[$attr])* + #[doc="Set the `Content-Type` of the response to "] + #[doc=$name_str] + #[doc=", or "] + #[doc=$ct_str] + #[doc="."] /// /// Delagates the remainder of the response to the wrapped responder. #[derive(Debug)] - pub struct $name(pub T); + pub struct $name(pub R); /// Sets the Content-Type of the response then delegates the /// remainder of the response to the wrapped responder. - impl Responder for $name { - #[inline(always)] - fn respond<'b>(&mut self, mut res: FreshHyperResponse<'b>) -> Outcome<'b> { - let mime = Mime(TopLevel::$top, SubLevel::$sub, vec![]); - res.headers_mut().set(header::ContentType(mime)); - self.0.respond(res) + impl<'r, R: Responder<'r>> Responder<'r> for $name { + fn respond(self) -> Result, Status> { + Content(ContentType::$name, self.0).respond() } - })+ + } + )+ } } ctrs! { - /// Sets the Content-Type of the response to JSON (`application/json`). - | JSON: Application/Json, - - /// Sets the Content-Type of the response to XML (`text/xml`). - | XML: Text/Xml, - - /// Sets the Content-Type of the response to HTML (`text/html`). - | HTML: Text/Html, - - /// Sets the Content-Type of the response to plain text (`text/plain`). - | Plain: Text/Plain, - - /// Sets the Content-Type of the response to CSS (`text/css`). - | CSS: Text/Css, - - /// Sets the Content-Type of the response to JavaScript - /// (`application/javascript`). - | JavaScript: Application/Javascript + JSON: "JSON", "application/json", + XML: "XML", "text/xml", + HTML: "HTML", "text/html", + Plain: "plain text", "text/plain", + CSS: "CSS", "text/css", + JavaScript: "JavaScript", "application/javascript" } diff --git a/lib/src/response/failure.rs b/lib/src/response/failure.rs index 28386484..4c2aa307 100644 --- a/lib/src/response/failure.rs +++ b/lib/src/response/failure.rs @@ -1,14 +1,13 @@ -use outcome::Outcome; -use response::{self, Responder}; -use http::hyper::{FreshHyperResponse, StatusCode}; +use response::{Response, Responder}; +use http::Status; /// A failing response; simply forwards to the catcher for the given -/// `StatusCode`. +/// `Status`. #[derive(Debug)] -pub struct Failure(pub StatusCode); +pub struct Failure(pub Status); -impl Responder for Failure { - fn respond<'a>(&mut self, res: FreshHyperResponse<'a>) -> response::Outcome<'a> { - Outcome::Forward((self.0, res)) +impl<'r> Responder<'r> for Failure { + fn respond(self) -> Result, Status> { + Err(self.0) } } diff --git a/lib/src/response/flash.rs b/lib/src/response/flash.rs index 175859aa..4d33c15d 100644 --- a/lib/src/response/flash.rs +++ b/lib/src/response/flash.rs @@ -1,9 +1,10 @@ use std::convert::AsRef; use outcome::IntoOutcome; -use response::{self, Responder}; +use response::{Response, Responder}; use request::{self, Request, FromRequest}; -use http::hyper::{HyperSetCookie, HyperCookiePair, FreshHyperResponse}; +use http::hyper::{HyperSetCookie, HyperCookiePair}; +use http::Status; // The name of the actual flash cookie. const FLASH_COOKIE_NAME: &'static str = "_flash"; @@ -87,7 +88,7 @@ pub struct Flash { responder: R, } -impl Flash { +impl<'r, R: Responder<'r>> Flash { /// Constructs a new `Flash` message with the given `name`, `msg`, and /// underlying `responder`. /// @@ -173,11 +174,13 @@ impl Flash { /// response. In other words, simply sets a cookie and delagates the rest of the /// response handling to the wrapped responder. As a result, the `Outcome` of /// the response is the `Outcome` of the wrapped `Responder`. -impl Responder for Flash { - fn respond<'b>(&mut self, mut res: FreshHyperResponse<'b>) -> response::Outcome<'b> { +impl<'r, R: Responder<'r>> Responder<'r> for Flash { + fn respond(self) -> Result, Status> { trace_!("Flash: setting message: {}:{}", self.name, self.message); - res.headers_mut().set(HyperSetCookie(vec![self.cookie_pair()])); - self.responder.respond(res) + let cookie = vec![self.cookie_pair()]; + Response::build_from(self.responder.respond()?) + .header_adjoin(HyperSetCookie(cookie)) + .ok() } } diff --git a/lib/src/response/mod.rs b/lib/src/response/mod.rs index b1ac4d9d..b68b98d5 100644 --- a/lib/src/response/mod.rs +++ b/lib/src/response/mod.rs @@ -24,11 +24,13 @@ mod failure; pub mod content; pub mod status; -pub use self::response::Response; -pub use self::responder::{Outcome, Responder}; +pub use self::response::{Response, Body, DEFAULT_CHUNK_SIZE}; +pub use self::responder::Responder; pub use self::redirect::Redirect; pub use self::flash::Flash; pub use self::named_file::NamedFile; pub use self::stream::Stream; pub use self::content::Content; pub use self::failure::Failure; + +pub type Result<'r> = ::std::result::Result, ::http::Status>; diff --git a/lib/src/response/named_file.rs b/lib/src/response/named_file.rs index 6da24ca7..e3d6b8c6 100644 --- a/lib/src/response/named_file.rs +++ b/lib/src/response/named_file.rs @@ -3,9 +3,8 @@ use std::path::{Path, PathBuf}; use std::io; use std::ops::{Deref, DerefMut}; -use response::{Responder, Outcome}; -use http::hyper::{header, FreshHyperResponse}; -use http::ContentType; +use response::{Response, Responder}; +use http::{Status, ContentType}; /// A file with an associated name; responds with the Content-Type based on the /// file extension. @@ -76,18 +75,19 @@ impl NamedFile { /// /// If reading the file fails permanently at any point during the response, an /// `Outcome` of `Failure` is returned, and the response is terminated abrubtly. -impl Responder for NamedFile { - fn respond<'a>(&mut self, mut res: FreshHyperResponse<'a>) -> Outcome<'a> { +impl<'r> Responder<'r> for NamedFile { + fn respond(self) -> Result, Status> { + let mut response = Response::new(); if let Some(ext) = self.path().extension() { // TODO: Use Cow for lowercase. let ext_string = ext.to_string_lossy().to_lowercase(); let content_type = ContentType::from_extension(&ext_string); if !content_type.is_any() { - res.headers_mut().set(header::ContentType(content_type.into())); + response.set_header(content_type) } } - self.file_mut().respond(res) + Ok(response) } } diff --git a/lib/src/response/redirect.rs b/lib/src/response/redirect.rs index 8bf82868..8b9b39f1 100644 --- a/lib/src/response/redirect.rs +++ b/lib/src/response/redirect.rs @@ -1,12 +1,12 @@ -use response::{Outcome, Responder}; -use http::hyper::{header, FreshHyperResponse, StatusCode}; -use outcome::IntoOutcome; +use response::{Response, Responder}; +use http::hyper::header; +use http::Status; /// An empty redirect response to a given URL. /// /// This type simplifies returning a redirect response to the client. #[derive(Debug)] -pub struct Redirect(StatusCode, String); +pub struct Redirect(Status, String); impl Redirect { /// Construct a temporary "see other" (303) redirect response. This is the @@ -22,7 +22,7 @@ impl Redirect { /// let redirect = Redirect::to("/other_url"); /// ``` pub fn to(uri: &str) -> Redirect { - Redirect(StatusCode::SeeOther, String::from(uri)) + Redirect(Status::SeeOther, String::from(uri)) } /// Construct a "temporary" (307) redirect response. This response instructs @@ -39,7 +39,7 @@ impl Redirect { /// let redirect = Redirect::temporary("/other_url"); /// ``` pub fn temporary(uri: &str) -> Redirect { - Redirect(StatusCode::TemporaryRedirect, String::from(uri)) + Redirect(Status::TemporaryRedirect, String::from(uri)) } /// Construct a "permanent" (308) redirect response. This redirect must only @@ -57,7 +57,7 @@ impl Redirect { /// let redirect = Redirect::permanent("/other_url"); /// ``` pub fn permanent(uri: &str) -> Redirect { - Redirect(StatusCode::PermanentRedirect, String::from(uri)) + Redirect(Status::PermanentRedirect, String::from(uri)) } /// Construct a temporary "found" (302) redirect response. This response @@ -75,7 +75,7 @@ impl Redirect { /// let redirect = Redirect::found("/other_url"); /// ``` pub fn found(uri: &str) -> Redirect { - Redirect(StatusCode::Found, String::from(uri)) + Redirect(Status::Found, String::from(uri)) } /// Construct a permanent "moved" (301) redirect response. This response @@ -91,18 +91,19 @@ impl Redirect { /// let redirect = Redirect::moved("/other_url"); /// ``` pub fn moved(uri: &str) -> Redirect { - Redirect(StatusCode::MovedPermanently, String::from(uri)) + Redirect(Status::MovedPermanently, String::from(uri)) } } /// Constructs a response with the appropriate status code and the given URL in /// the `Location` header field. The body of the response is empty. This /// responder does not fail. -impl<'a> Responder for Redirect { - fn respond<'b>(&mut self, mut res: FreshHyperResponse<'b>) -> Outcome<'b> { - res.headers_mut().set(header::ContentLength(0)); - res.headers_mut().set(header::Location(self.1.clone())); - *(res.status_mut()) = self.0; - res.send(b"").into_outcome() +impl Responder<'static> for Redirect { + fn respond(self) -> Result, Status> { + Response::build() + .status(self.0) + .header(header::ContentLength(0)) + .header(header::Location(self.1.clone())) + .ok() } } diff --git a/lib/src/response/responder.rs b/lib/src/response/responder.rs index 1c5b2337..ef867beb 100644 --- a/lib/src/response/responder.rs +++ b/lib/src/response/responder.rs @@ -1,24 +1,9 @@ use std::fs::File; +use std::io::Cursor; use std::fmt; -use http::mime::{Mime, TopLevel, SubLevel}; -use http::hyper::{header, FreshHyperResponse, StatusCode}; -use outcome::{self, IntoOutcome}; -use outcome::Outcome::*; -use response::Stream; - - -/// Type alias for the `Outcome` of a `Responder`. -pub type Outcome<'a> = outcome::Outcome<(), (), (StatusCode, FreshHyperResponse<'a>)>; - -impl<'a, T, E> IntoOutcome<(), (), (StatusCode, FreshHyperResponse<'a>)> for Result { - fn into_outcome(self) -> Outcome<'a> { - match self { - Ok(_) => Success(()), - Err(_) => Failure(()) - } - } -} +use http::{Status, ContentType}; +use response::{Response, Stream}; /// Trait implemented by types that send a response to clients. /// @@ -32,29 +17,16 @@ impl<'a, T, E> IntoOutcome<(), (), (StatusCode, FreshHyperResponse<'a>)> for Res /// /// In this example, `T` can be any type that implements `Responder`. /// -/// # Outcomes +/// # Return Value /// -/// The returned [Outcome](/rocket/outcome/index.html) of a `respond` call -/// determines how the response will be processed, if at all. +/// A `Responder` returns an `Ok(Response)` or an `Err(Status)`. /// -/// * **Success** +/// An `Ok` variant means that the `Responder` was successful in generating a +/// new `Response`. The `Response` will be written out to the client. /// -/// An `Outcome` of `Success` indicates that the responder was successful in -/// sending the response to the client. No further processing will occur as a -/// result. -/// -/// * **Failure** -/// -/// An `Outcome` of `Failure` indicates that the responder failed after -/// beginning a response. The response is incomplete, and there is no way to -/// salvage the response. No further processing will occur. -/// -/// * **Forward**(StatusCode, FreshHyperResponse<'a>) -/// -/// If the `Outcome` is `Forward`, the response will be forwarded to the -/// designated error [Catcher](/rocket/struct.Catcher.html) for the given -/// `StatusCode`. This requires that a response wasn't started and thus is -/// still fresh. +/// An `Err` variant means that the `Responder` could not or did not generate a +/// `Response`. The contained `Status` will be used to find the relevant error +/// catcher to use to generate a proper response. /// /// # Provided Implementations /// @@ -63,42 +35,44 @@ impl<'a, T, E> IntoOutcome<(), (), (StatusCode, FreshHyperResponse<'a>)> for Res /// overloaded, allowing for two `Responder`s to be used at once, depending on /// the variant. /// -/// * **impl<'a> Responder for &'a str** +/// * **&str** /// -/// Sets the `Content-Type`t to `text/plain` if it is not already set. Sends -/// the string as the body of the response. +/// Sets the `Content-Type`t to `text/plain`. The string is used as the body +/// of the response, which is fixed size and not streamed. To stream a raw +/// string, use `Stream::from(Cursor::new(string))`. /// -/// * **impl Responder for String** +/// * **String** /// -/// Sets the `Content-Type`t to `text/html` if it is not already set. Sends -/// the string as the body of the response. +/// Sets the `Content-Type`t to `text/html`. The string is used as the body +/// of the response, which is fixed size and not streamed. To stream a +/// string, use `Stream::from(Cursor::new(string))`. /// -/// * **impl Responder for File** +/// * **File** /// /// Streams the `File` to the client. This is essentially an alias to -/// [Stream](struct.Stream.html)<File>. +/// `Stream::from(file)`. /// /// * **impl Responder for ()** /// -/// Responds with an empty body. +/// Responds with an empty body. No Content-Type is set. /// -/// * **impl<T: Responder> Responder for Option<T>** +/// * **Option<T>** /// /// If the `Option` is `Some`, the wrapped responder is used to respond to -/// respond to the client. Otherwise, the response is forwarded to the 404 -/// error catcher and a warning is printed to the console. +/// respond to the client. Otherwise, an `Err` with status **404 Not Found** +/// is returned and a warning is printed to the console. /// -/// * **impl<T: Responder, E: Debug> Responder for Result<T, E>** +/// * **Result<T, E>** _where_ **E: Debug** /// /// If the `Result` is `Ok`, the wrapped responder is used to respond to the -/// client. Otherwise, the response is forwarded to the 500 error catcher -/// and the error is printed to the console using the `Debug` +/// client. Otherwise, an `Err` with status **500 Internal Server Error** is +/// returned and the error is printed to the console using the `Debug` /// implementation. /// -/// * **impl<T: Responder, E: Responder + Debug> Responder for Result<T, E>** +/// * **Result<T, E>** _where_ **E: Debug + Responder** /// /// If the `Result` is `Ok`, the wrapped `Ok` responder is used to respond -/// to the client. If the `Result` is `Err`, the wrapped error responder is +/// to the client. If the `Result` is `Err`, the wrapped `Err` responder is /// used to respond to the client. /// /// # Implementation Tips @@ -113,13 +87,13 @@ impl<'a, T, E> IntoOutcome<(), (), (StatusCode, FreshHyperResponse<'a>)> for Res /// requires its `Err` type to implement `Debug`. Therefore, a type implementing /// `Debug` can more easily be composed. /// -/// ## Check Before Changing +/// ## Joining and Merging /// -/// Unless a given type is explicitly designed to change some information in the -/// response, it should first _check_ that some information hasn't been set -/// before _changing_ that information. For example, before setting the -/// `Content-Type` header of a response, first check that the header hasn't been -/// set. +/// When chaining/wrapping other `Responder`s, use the +/// [merge](/rocket/struct.Response.html#method.merge) or +/// [join](/rocket/struct.Response.html#method.join) methods on the `Response` +/// struct. Ensure that you document the merging or joining behavior +/// appropriately. /// /// # Example /// @@ -151,34 +125,23 @@ impl<'a, T, E> IntoOutcome<(), (), (StatusCode, FreshHyperResponse<'a>)> for Res /// # #[derive(Debug)] /// # struct Person { name: String, age: u16 } /// # -/// use std::str::FromStr; -/// use std::fmt::Write; +/// use std::io::Cursor; /// -/// use rocket::response::{Responder, Outcome}; -/// use rocket::outcome::IntoOutcome; -/// use rocket::http::hyper::{FreshHyperResponse, header}; +/// use rocket::response::{self, Response, Responder}; /// use rocket::http::ContentType; /// -/// impl Responder for Person { -/// fn respond<'b>(&mut self, mut res: FreshHyperResponse<'b>) -> Outcome<'b> { -/// // Set the custom headers. -/// let name_bytes = self.name.clone().into_bytes(); -/// let age_bytes = self.age.to_string().into_bytes(); -/// res.headers_mut().set_raw("X-Person-Name", vec![name_bytes]); -/// res.headers_mut().set_raw("X-Person-Age", vec![age_bytes]); -/// -/// // Set the custom Content-Type header. -/// let ct = ContentType::from_str("application/x-person").unwrap(); -/// res.headers_mut().set(header::ContentType(ct.into())); -/// -/// // Write out the "custom" body, here just the debug representation. -/// let mut repr = String::with_capacity(50); -/// write!(&mut repr, "{:?}", *self); -/// res.send(repr.as_bytes()).into_outcome() +/// impl<'r> Responder<'r> for Person { +/// fn respond(self) -> response::Result<'r> { +/// Response::build() +/// .sized_body(Cursor::new(format!("{:?}", self))) +/// .raw_header("X-Person-Name", self.name) +/// .raw_header("X-Person-Age", self.age.to_string()) +/// .header(ContentType::new("application", "x-person")) +/// .ok() /// } /// } /// ``` -pub trait Responder { +pub trait Responder<'r> { /// Attempts to write a response to `res`. /// /// If writing the response successfully completes, an outcome of `Success` @@ -186,76 +149,82 @@ pub trait Responder { /// `Failure` is returned. If writing a response fails before writing /// anything out, an outcome of `Forward` can be returned, which causes the /// response to be written by the appropriate error catcher instead. - fn respond<'a>(&mut self, res: FreshHyperResponse<'a>) -> Outcome<'a>; + fn respond(self) -> Result, Status>; } /// Sets the `Content-Type`t to `text/plain` if it is not already set. Sends the -/// string as the body of the response. -impl<'a> Responder for &'a str { - fn respond<'b>(&mut self, mut res: FreshHyperResponse<'b>) -> Outcome<'b> { - if res.headers().get::().is_none() { - let mime = Mime(TopLevel::Text, SubLevel::Plain, vec![]); - res.headers_mut().set(header::ContentType(mime)); - } - - res.send(self.as_bytes()).into_outcome() +/// string as the body of the response. Never fails. +/// +/// # Example +/// +/// ```rust +/// use rocket::response::Responder; +/// use rocket::http::ContentType; +/// +/// let mut response = "Hello".respond().unwrap(); +/// +/// let body_string = response.body().unwrap().to_string().unwrap(); +/// assert_eq!(body_string, "Hello".to_string()); +/// +/// let content_type: Vec<_> = response.get_header_values("Content-Type").collect(); +/// assert_eq!(content_type.len(), 1); +/// assert_eq!(content_type[0], ContentType::Plain.to_string()); +/// ``` +impl<'r> Responder<'r> for &'r str { + fn respond(self) -> Result, Status> { + Response::build() + .header(ContentType::Plain) + .sized_body(Cursor::new(self)) + .ok() } } -impl Responder for String { - fn respond<'a>(&mut self, mut res: FreshHyperResponse<'a>) -> Outcome<'a> { - if res.headers().get::().is_none() { - let mime = Mime(TopLevel::Text, SubLevel::Html, vec![]); - res.headers_mut().set(header::ContentType(mime)); - } - - res.send(self.as_bytes()).into_outcome() +impl Responder<'static> for String { + fn respond(self) -> Result, Status> { + Response::build() + .header(ContentType::HTML) + .sized_body(Cursor::new(self)) + .ok() } } /// Essentially aliases Stream. -impl Responder for File { - fn respond<'a>(&mut self, res: FreshHyperResponse<'a>) -> Outcome<'a> { - Stream::from(self).respond(res) +impl Responder<'static> for File { + fn respond(self) -> Result, Status> { + Stream::from(self).respond() } } /// Empty response. -impl Responder for () { - fn respond<'a>(&mut self, res: FreshHyperResponse<'a>) -> Outcome<'a> { - res.send(&[]).into_outcome() +impl Responder<'static> for () { + fn respond(self) -> Result, Status> { + Ok(Response::new()) } } -impl Responder for Option { - fn respond<'a>(&mut self, res: FreshHyperResponse<'a>) -> Outcome<'a> { - if let Some(ref mut val) = *self { - val.respond(res) - } else { +impl<'r, R: Responder<'r>> Responder<'r> for Option { + fn respond(self) -> Result, Status> { + self.map_or_else(|| { warn_!("Response was `None`."); - Forward((StatusCode::NotFound, res)) - } + Err(Status::NotFound) + }, |r| r.respond()) } } -impl Responder for Result { - // prepend with `default` when using impl specialization - default fn respond<'a>(&mut self, res: FreshHyperResponse<'a>) -> Outcome<'a> { - match *self { - Ok(ref mut val) => val.respond(res), - Err(ref e) => { - error_!("{:?}", e); - Forward((StatusCode::InternalServerError, res)) - } - } +impl<'r, R: Responder<'r>, E: fmt::Debug> Responder<'r> for Result { + default fn respond(self) -> Result, Status> { + self.map(|r| r.respond()).unwrap_or_else(|e| { + warn_!("Response was `Err`: {:?}.", e); + Err(Status::InternalServerError) + }) } } -impl Responder for Result { - fn respond<'a>(&mut self, res: FreshHyperResponse<'a>) -> Outcome<'a> { - match *self { - Ok(ref mut responder) => responder.respond(res), - Err(ref mut responder) => responder.respond(res), +impl<'r, R: Responder<'r>, E: Responder<'r> + fmt::Debug> Responder<'r> for Result { + fn respond(self) -> Result, Status> { + match self { + Ok(responder) => responder.respond(), + Err(responder) => responder.respond(), } } } diff --git a/lib/src/response/response.rs b/lib/src/response/response.rs index 3c9d05b3..673befb6 100644 --- a/lib/src/response/response.rs +++ b/lib/src/response/response.rs @@ -1,36 +1,342 @@ -use data::Data; -use outcome::{self, Outcome}; -use http::hyper::StatusCode; -use response::{Responder, status}; +use std::{io, fmt, str}; +use std::borrow::{Borrow, Cow}; +use std::collections::HashMap; -/// Type alias for the `Outcome` of a `Handler`. -pub type Response<'a> = outcome::Outcome, StatusCode, Data>; +use http::Header; +use http::Status; -impl<'a> Response<'a> { - #[inline(always)] - pub fn success(responder: T) -> Response<'a> { - Outcome::Success(Box::new(responder)) +pub const DEFAULT_CHUNK_SIZE: u64 = 4096; + +pub enum Body { + Sized(T, u64), + Chunked(T, u64) +} + +impl Body { + pub fn as_mut(&mut self) -> Body<&mut T> { + match *self { + Body::Sized(ref mut b, n) => Body::Sized(b, n), + Body::Chunked(ref mut b, n) => Body::Chunked(b, n) + } } - #[inline(always)] - pub fn failure(code: StatusCode) -> Response<'static> { - Outcome::Failure(code) - } - - #[inline(always)] - pub fn forward(data: Data) -> Response<'static> { - Outcome::Forward(data) - } - - #[inline(always)] - pub fn with_raw_status(status: u16, body: T) -> Response<'a> { - let status_code = StatusCode::from_u16(status); - Response::success(status::Custom(status_code, body)) - } - - #[doc(hidden)] - #[inline(always)] - pub fn responder(self) -> Option> { - self.succeeded() + pub fn map U>(self, f: F) -> Body { + match self { + Body::Sized(b, n) => Body::Sized(f(b), n), + Body::Chunked(b, n) => Body::Chunked(f(b), n) + } + } +} + +impl Body { + pub fn to_string(self) -> Option { + let (mut body, mut string) = match self { + Body::Sized(b, size) => (b, String::with_capacity(size as usize)), + Body::Chunked(b, _) => (b, String::new()) + }; + + if let Err(e) = body.read_to_string(&mut string) { + error_!("Error reading body: {:?}", e); + return None; + } + + Some(string) + } +} + +impl fmt::Debug for Body { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Body::Sized(_, n) => writeln!(f, "Sized Body [{} bytes]", n), + Body::Chunked(_, n) => writeln!(f, "Chunked Body [{} bytes]", n), + } + } +} + +pub struct ResponseBuilder<'r> { + response: Response<'r> +} + +impl<'r> ResponseBuilder<'r> { + #[inline(always)] + pub fn new(base: Response<'r>) -> ResponseBuilder<'r> { + ResponseBuilder { + response: base + } + } + + #[inline(always)] + pub fn status(&mut self, status: Status) -> &mut ResponseBuilder<'r> { + self.response.set_status(status); + self + } + + #[inline(always)] + pub fn raw_status(&mut self, code: u16, reason: &'static str) + -> &mut ResponseBuilder<'r> { + self.response.set_raw_status(code, reason); + self + } + + #[inline(always)] + pub fn header<'h: 'r, H>(&mut self, header: H) -> &mut ResponseBuilder<'r> + where H: Into> + { + self.response.set_header(header); + self + } + + #[inline(always)] + pub fn header_adjoin<'h: 'r, H>(&mut self, header: H) -> &mut ResponseBuilder<'r> + where H: Into> + { + self.response.adjoin_header(header); + self + } + + #[inline(always)] + pub fn raw_header<'a: 'r, 'b: 'r, N, V>(&mut self, name: N, value: V) + -> &mut ResponseBuilder<'r> + where N: Into>, V: Into> + { + self.response.set_raw_header(name, value); + self + } + + #[inline(always)] + pub fn raw_header_adjoin<'a: 'r, 'b: 'r, N, V>(&mut self, name: N, value: V) + -> &mut ResponseBuilder<'r> + where N: Into>, V: Into> + { + self.response.adjoin_raw_header(name, value); + self + } + + #[inline(always)] + pub fn sized_body(&mut self, body: B) -> &mut ResponseBuilder<'r> + where B: io::Read + io::Seek + 'r + { + self.response.set_sized_body(body); + self + } + + #[inline(always)] + pub fn streamed_body(&mut self, body: B) -> &mut ResponseBuilder<'r> + where B: io::Read + 'r + { + self.response.set_streamed_body(body); + self + } + + #[inline(always)] + pub fn chunked_body(&mut self, body: B, chunk_size: u64) + -> &mut ResponseBuilder<'r> + { + self.response.set_chunked_body(body, chunk_size); + self + } + + #[inline(always)] + pub fn merge(&mut self, other: Response<'r>) -> &mut ResponseBuilder<'r> { + self.response.merge(other); + self + } + + #[inline(always)] + pub fn join(&mut self, other: Response<'r>) -> &mut ResponseBuilder<'r> { + self.response.join(other); + self + } + + #[inline(always)] + pub fn finalize(&mut self) -> Response<'r> { + ::std::mem::replace(&mut self.response, Response::new()) + } + + #[inline(always)] + pub fn ok(&mut self) -> Result, T> { + Ok(self.finalize()) + } +} + +// `join`? Maybe one does one thing, the other does another? IE: `merge` +// replaces, `join` adds. One more thing that could be done: we could make it +// some that _some_ headers default to replacing, and other to joining. +/// Return type of a thing. +pub struct Response<'r> { + status: Option, + headers: HashMap, Vec>>, + body: Option>>, +} + +impl<'r> Response<'r> { + #[inline(always)] + pub fn new() -> Response<'r> { + Response { + status: None, + headers: HashMap::new(), + body: None, + } + } + + #[inline(always)] + pub fn build() -> ResponseBuilder<'r> { + Response::build_from(Response::new()) + } + + #[inline(always)] + pub fn build_from(other: Response<'r>) -> ResponseBuilder<'r> { + ResponseBuilder::new(other) + } + + #[inline(always)] + pub fn status(&self) -> Status { + self.status.unwrap_or(Status::Ok) + } + + #[inline(always)] + pub fn set_status(&mut self, status: Status) { + self.status = Some(status); + } + + #[inline(always)] + pub fn set_raw_status(&mut self, code: u16, reason: &'static str) { + self.status = Some(Status::new(code, reason)); + } + + #[inline(always)] + pub fn headers<'a>(&'a self) -> impl Iterator> { + self.headers.iter().flat_map(|(key, values)| { + values.iter().map(move |val| { + Header::new(key.borrow(), val.borrow()) + }) + }) + } + + #[inline(always)] + pub fn get_header_values<'h>(&'h self, name: &str) + -> impl Iterator { + self.headers.get(name).into_iter().flat_map(|values| { + values.iter().map(|val| val.borrow()) + }) + } + + #[inline(always)] + pub fn set_header<'h: 'r, H: Into>>(&mut self, header: H) { + let header = header.into(); + self.headers.insert(header.name, vec![header.value]); + } + + #[inline(always)] + pub fn set_raw_header<'a: 'r, 'b: 'r, N, V>(&mut self, name: N, value: V) + where N: Into>, V: Into> + { + self.set_header(Header::new(name, value)); + } + + #[inline(always)] + pub fn adjoin_header<'h: 'r, H: Into>>(&mut self, header: H) { + let header = header.into(); + self.headers.entry(header.name).or_insert(vec![]).push(header.value); + } + + #[inline(always)] + pub fn adjoin_raw_header<'a: 'r, 'b: 'r, N, V>(&mut self, name: N, value: V) + where N: Into>, V: Into> + { + self.adjoin_header(Header::new(name, value)); + } + + #[inline(always)] + pub fn remove_header<'h>(&mut self, name: &'h str) { + self.headers.remove(name); + } + + #[inline(always)] + pub fn body(&mut self) -> Option> { + // Looks crazy, right? Needed so Rust infers lifetime correctly. Weird. + match self.body.as_mut() { + Some(body) => Some(match body.as_mut() { + Body::Sized(b, u64) => Body::Sized(b, u64), + Body::Chunked(b, u64) => Body::Chunked(b, u64), + }), + None => None + } + } + + #[inline(always)] + pub fn take_body(&mut self) -> Option>> { + self.body.take() + } + + #[inline(always)] + pub fn set_sized_body(&mut self, mut body: B) + where B: io::Read + io::Seek + 'r + { + let size = body.seek(io::SeekFrom::End(0)) + .expect("Attempted to retrieve size by seeking, but failed."); + body.seek(io::SeekFrom::Start(0)) + .expect("Attempted to reset body by seeking after getting size."); + self.body = Some(Body::Sized(Box::new(body), size)); + } + + #[inline(always)] + pub fn set_streamed_body(&mut self, body: B) where B: io::Read + 'r { + self.set_chunked_body(body, DEFAULT_CHUNK_SIZE); + } + + #[inline(always)] + pub fn set_chunked_body(&mut self, body: B, chunk_size: u64) + where B: io::Read + 'r { + self.body = Some(Body::Chunked(Box::new(body), chunk_size)); + } + + // Replaces this response's status and body with that of `other`, if they + // exist. Any headers that exist in `other` replace the ones in `self`. Any + // in `self` that aren't in `other` remain. + pub fn merge(&mut self, other: Response<'r>) { + if let Some(status) = other.status { + self.status = Some(status); + } + + if let Some(body) = other.body { + self.body = Some(body); + } + + for (name, values) in other.headers.into_iter() { + self.headers.insert(name, values); + } + } + + // Sets `self`'s status and body to that of `other` if they are not already + // set in `self`. Any headers present in both `other` and `self` are + // adjoined. + pub fn join(&mut self, other: Response<'r>) { + if self.status.is_none() { + self.status = other.status; + } + + if self.body.is_none() { + self.body = other.body; + } + + for (name, mut values) in other.headers.into_iter() { + self.headers.entry(name).or_insert(vec![]).append(&mut values) + } + } +} + +impl<'r> fmt::Debug for Response<'r> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "{}", self.status())?; + + for header in self.headers() { + writeln!(f, "{}", header)?; + } + + match self.body { + Some(ref body) => writeln!(f, "{:?}", body), + None => writeln!(f, "Empty Body") + } } } diff --git a/lib/src/response/status.rs b/lib/src/response/status.rs index 0f77e829..81a4119a 100644 --- a/lib/src/response/status.rs +++ b/lib/src/response/status.rs @@ -10,9 +10,9 @@ use std::hash::{Hash, Hasher}; use std::collections::hash_map::DefaultHasher; -use response::{Responder, Outcome}; -use outcome::IntoOutcome; -use http::hyper::{StatusCode, FreshHyperResponse, header}; +use response::{Responder, Response}; +use http::hyper::header; +use http::Status; /// Sets the status of the response to 201 (Created). /// @@ -28,7 +28,7 @@ use http::hyper::{StatusCode, FreshHyperResponse, header}; /// let content = "{ 'resource': 'Hello, world!' }"; /// let response = status::Created(url, Some(content)); /// ``` -pub struct Created(pub String, pub Option); +pub struct Created(pub String, pub Option); /// Sets the status code of the response to 201 Created. Sets the `Location` /// header to the `String` parameter in the constructor. @@ -37,14 +37,14 @@ pub struct Created(pub String, pub Option); /// responder should write the body of the response so that it contains /// information about the created resource. If no responder is provided, the /// response body will be empty. -impl Responder for Created { - default fn respond<'b>(&mut self, mut res: FreshHyperResponse<'b>) -> Outcome<'b> { - *res.status_mut() = StatusCode::Created; - res.headers_mut().set(header::Location(self.0.clone())); - match self.1 { - Some(ref mut r) => r.respond(res), - None => res.send(&[]).into_outcome() +impl<'r, R: Responder<'r>> Responder<'r> for Created { + default fn respond(self) -> Result, Status> { + let mut build = Response::build(); + if let Some(responder) = self.1 { + build.merge(responder.respond()?); } + + build.status(Status::Created).header(header::Location(self.0)).ok() } } @@ -52,21 +52,19 @@ impl Responder for Created { /// the response with the `Responder`, the `ETag` header is set conditionally if /// a `Responder` is provided that implements `Hash`. The `ETag` header is set /// to a hash value of the responder. -impl Responder for Created { - fn respond<'b>(&mut self, mut res: FreshHyperResponse<'b>) -> Outcome<'b> { - *res.status_mut() = StatusCode::Created; - res.headers_mut().set(header::Location(self.0.clone())); - +impl<'r, R: Responder<'r> + Hash> Responder<'r> for Created { + fn respond(self) -> Result, Status> { let mut hasher = DefaultHasher::default(); - match self.1 { - Some(ref mut responder) => { - responder.hash(&mut hasher); - let tag = header::EntityTag::strong(hasher.finish().to_string()); - res.headers_mut().set(header::ETag(tag)); - responder.respond(res) - } - None => res.send(&[]).into_outcome() + let mut build = Response::build(); + if let Some(responder) = self.1 { + responder.hash(&mut hasher); + let hash = hasher.finish().to_string(); + + build.merge(responder.respond()?); + build.header(header::ETag(header::EntityTag::strong(hash))); } + + build.status(Status::Created).header(header::Location(self.0)).ok() } } @@ -92,17 +90,18 @@ impl Responder for Created { /// /// let response = status::Accepted(Some("processing")); /// ``` -pub struct Accepted(pub Option); +pub struct Accepted(pub Option); /// Sets the status code of the response to 202 Accepted. If the responder is /// `Some`, it is used to finalize the response. -impl Responder for Accepted { - fn respond<'b>(&mut self, mut res: FreshHyperResponse<'b>) -> Outcome<'b> { - *res.status_mut() = StatusCode::Accepted; - match self.0 { - Some(ref mut r) => r.respond(res), - None => res.send(&[]).into_outcome() +impl<'r, R: Responder<'r>> Responder<'r> for Accepted { + fn respond(self) -> Result, Status> { + let mut build = Response::build(); + if let Some(responder) = self.0 { + build.merge(responder.respond()?); } + + build.status(Status::Accepted).ok() } } @@ -120,10 +119,9 @@ pub struct NoContent; /// Sets the status code of the response to 204 No Content. The body of the /// response will be empty. -impl Responder for NoContent { - fn respond<'b>(&mut self, mut res: FreshHyperResponse<'b>) -> Outcome<'b> { - *res.status_mut() = StatusCode::NoContent; - res.send(&[]).into_outcome() +impl<'r> Responder<'r> for NoContent { + fn respond(self) -> Result, Status> { + Response::build().status(Status::NoContent).ok() } } @@ -141,10 +139,9 @@ pub struct Reset; /// Sets the status code of the response to 205 Reset Content. The body of the /// response will be empty. -impl Responder for Reset { - fn respond<'b>(&mut self, mut res: FreshHyperResponse<'b>) -> Outcome<'b> { - *res.status_mut() = StatusCode::ResetContent; - res.send(&[]).into_outcome() +impl<'r> Responder<'r> for Reset { + fn respond(self) -> Result, Status> { + Response::build().status(Status::ResetContent).ok() } } @@ -154,18 +151,19 @@ impl Responder for Reset { /// /// ```rust /// use rocket::response::status; -/// use rocket::http::StatusCode; +/// use rocket::http::Status; /// -/// let response = status::Custom(StatusCode::ImATeapot, "Hi!"); +/// let response = status::Custom(Status::ImATeapot, "Hi!"); /// ``` -pub struct Custom(pub StatusCode, pub R); +pub struct Custom(pub Status, pub R); /// Sets the status code of the response and then delegates the remainder of the /// response to the wrapped responder. -impl Responder for Custom { - fn respond<'b>(&mut self, mut res: FreshHyperResponse<'b>) -> Outcome<'b> { - *(res.status_mut()) = self.0; - self.1.respond(res) +impl<'r, R: Responder<'r>> Responder<'r> for Custom { + fn respond(self) -> Result, Status> { + Response::build_from(self.1.respond()?) + .status(self.0) + .ok() } } diff --git a/lib/src/response/stream.rs b/lib/src/response/stream.rs index bb3b4af4..502c5f45 100644 --- a/lib/src/response/stream.rs +++ b/lib/src/response/stream.rs @@ -1,13 +1,8 @@ -use std::io::{Read, Write, ErrorKind}; +use std::io::Read; use std::fmt::{self, Debug}; -use response::{Responder, Outcome}; -use http::hyper::FreshHyperResponse; -use outcome::Outcome::*; - -// TODO: Support custom chunk sizes. -/// The default size of each chunk in the streamed response. -pub const CHUNK_SIZE: usize = 4096; +use response::{Response, Responder, DEFAULT_CHUNK_SIZE}; +use http::Status; /// Streams a response to a client from an arbitrary `Read`er type. /// @@ -15,7 +10,7 @@ pub const CHUNK_SIZE: usize = 4096; /// 4KiB. This means that at most 4KiB are stored in memory while the response /// is being sent. This type should be used when sending responses that are /// arbitrarily large in size, such as when streaming from a local socket. -pub struct Stream(T); +pub struct Stream(T, u64); impl Stream { /// Create a new stream from the given `reader`. @@ -32,18 +27,26 @@ impl Stream { /// let response = Stream::from(io::stdin()); /// ``` pub fn from(reader: T) -> Stream { - Stream(reader) + Stream(reader, DEFAULT_CHUNK_SIZE) } - // pub fn chunked(mut self, size: usize) -> Self { - // self.1 = size; - // self - // } - - // #[inline(always)] - // pub fn chunk_size(&self) -> usize { - // self.1 - // } + /// Create a new stream from the given `reader` and sets the chunk size for + /// each streamed chunk to `chunk_size` bytes. + /// + /// # Example + /// + /// Stream a response from whatever is in `stdin` with a chunk size of 10 + /// bytes. Note: you probably shouldn't do this. + /// + /// ```rust + /// use std::io; + /// use rocket::response::Stream; + /// + /// let response = Stream::chunked(io::stdin(), 10); + /// ``` + pub fn chunked(reader: T, chunk_size: u64) -> Stream { + Stream(reader, chunk_size) + } } impl Debug for Stream { @@ -60,43 +63,8 @@ impl Debug for Stream { /// If reading from the input stream fails at any point during the response, the /// response is abandoned, and the response ends abruptly. An error is printed /// to the console with an indication of what went wrong. -impl Responder for Stream { - fn respond<'a>(&mut self, res: FreshHyperResponse<'a>) -> Outcome<'a> { - let mut stream = match res.start() { - Ok(s) => s, - Err(ref err) => { - error_!("Failed opening response stream: {:?}", err); - return Failure(()); - } - }; - - let mut buffer = [0; CHUNK_SIZE]; - let mut complete = false; - while !complete { - let mut read = 0; - while read < buffer.len() && !complete { - match self.0.read(&mut buffer[read..]) { - Ok(n) if n == 0 => complete = true, - Ok(n) => read += n, - Err(ref e) if e.kind() == ErrorKind::Interrupted => continue, - Err(ref e) => { - error_!("Error streaming response: {:?}", e); - return Failure(()); - } - } - } - - if let Err(e) = stream.write_all(&buffer[..read]) { - error_!("Stream write_all() failed: {:?}", e); - return Failure(()); - } - } - - if let Err(e) = stream.end() { - error_!("Stream end() failed: {:?}", e); - return Failure(()); - } - - Success(()) +impl<'r, T: Read + 'r> Responder<'r> for Stream { + fn respond(self) -> Result, Status> { + Response::build().chunked_body(self.0, self.1).ok() } } diff --git a/lib/src/rocket.rs b/lib/src/rocket.rs index 0bef904e..a68326b7 100644 --- a/lib/src/rocket.rs +++ b/lib/src/rocket.rs @@ -2,22 +2,24 @@ use std::collections::HashMap; use std::str::from_utf8_unchecked; use std::cmp::min; use std::process; +use std::io::{self, Write}; use term_painter::Color::*; use term_painter::ToStyle; use logger; +use ext::ReadExt; use config::{self, Config}; use request::{Request, FormItems}; use data::Data; -use response::Responder; +use response::{Body, Response}; use router::{Router, Route}; use catcher::{self, Catcher}; use outcome::Outcome; use error::Error; -use http::{Method, StatusCode}; -use http::hyper::{HyperRequest, FreshHyperResponse}; +use http::{Method, Status}; +use http::hyper::{self, HyperRequest, FreshHyperResponse}; use http::hyper::{HyperServer, HyperHandler, HyperSetCookie, header}; /// The main `Rocket` type: used to mount routes and catchers and launch the @@ -39,7 +41,7 @@ impl HyperHandler for Rocket { // response processing. fn handle<'h, 'k>(&self, hyp_req: HyperRequest<'h, 'k>, - mut res: FreshHyperResponse<'h>) { + res: FreshHyperResponse<'h>) { // Get all of the information from Hyper. let (_, h_method, h_headers, h_uri, _, h_body) = hyp_req.deconstruct(); @@ -52,8 +54,8 @@ impl HyperHandler for Rocket { Err(ref reason) => { let mock = Request::mock(Method::Get, uri.as_str()); error!("{}: bad request ({}).", mock, reason); - self.handle_error(StatusCode::InternalServerError, &mock, res); - return; + let r = self.handle_error(Status::InternalServerError, &mock); + return self.issue_response(r, res); } }; @@ -62,49 +64,99 @@ impl HyperHandler for Rocket { Ok(data) => data, Err(reason) => { error_!("Bad data in request: {}", reason); - self.handle_error(StatusCode::InternalServerError, &request, res); - return; + let r = self.handle_error(Status::InternalServerError, &request); + return self.issue_response(r, res); } }; - // Set the common response headers and preprocess the request. - res.headers_mut().set(header::Server("rocket".to_string())); - self.preprocess_request(&mut request, &data); - // Now that we've Rocket-ized everything, actually dispatch the request. - let mut responder = match self.dispatch(&request, data) { - Ok(responder) => responder, - Err(StatusCode::NotFound) if request.method == Method::Head => { - // TODO: Handle unimplemented HEAD requests automatically. - info_!("Redirecting to {}.", Green.paint(Method::Get)); - self.handle_error(StatusCode::NotFound, &request, res); - return; - } - Err(code) => { - self.handle_error(code, &request, res); - return; + self.preprocess_request(&mut request, &data); + let mut response = match self.dispatch(&request, data) { + Ok(response) => response, + Err(status) => { + if status == Status::NotFound && request.method == Method::Head { + // FIXME: Handle unimplemented HEAD requests automatically. + info_!("Redirecting to {}.", Green.paint(Method::Get)); + } + + let response = self.handle_error(status, &request); + return self.issue_response(response, res); } }; - // We have a responder. Update the cookies in the header. + // We have a response from the user. Update the cookies in the header. let cookie_delta = request.cookies().delta(); if cookie_delta.len() > 0 { - res.headers_mut().set(HyperSetCookie(cookie_delta)); + response.adjoin_header(HyperSetCookie(cookie_delta)); } - // Actually call the responder. - let outcome = responder.respond(res); - info_!("{} {}", White.paint("Outcome:"), outcome); - - // Check if the responder wants to forward to a catcher. If it doesn't, - // it's a success or failure, so we can't do any more processing. - if let Some((code, f_res)) = outcome.forwarded() { - self.handle_error(code, &request, f_res); - } + // Actually write out the response. + return self.issue_response(response, res); } } impl Rocket { + #[inline] + fn issue_response(&self, mut response: Response, hyp_res: FreshHyperResponse) { + // Add the 'rocket' server header, and write out the response. + // TODO: If removing Hyper, write out `Data` header too. + response.set_header(header::Server("rocket".to_string())); + + match self.write_response(response, hyp_res) { + Ok(_) => info_!("{}", Green.paint("Response succeeded.")), + Err(e) => error_!("Failed to write response: {:?}.", e) + } + } + + fn write_response(&self, mut response: Response, + mut hyp_res: FreshHyperResponse) -> io::Result<()> + { + *hyp_res.status_mut() = hyper::StatusCode::from_u16(response.status().code); + + for header in response.headers() { + let name = header.name.into_owned(); + let value = vec![header.value.into_owned().into()]; + hyp_res.headers_mut().set_raw(name, value); + } + + if response.body().is_none() { + hyp_res.headers_mut().set(header::ContentLength(0)); + return hyp_res.start()?.end(); + } + + match response.body() { + None => { + hyp_res.headers_mut().set(header::ContentLength(0)); + hyp_res.start()?.end() + } + Some(Body::Sized(mut body, size)) => { + hyp_res.headers_mut().set(header::ContentLength(size)); + let mut stream = hyp_res.start()?; + io::copy(body, &mut stream)?; + stream.end() + } + Some(Body::Chunked(mut body, chunk_size)) => { + // This _might_ happen on a 32-bit machine! + if chunk_size > (usize::max_value() as u64) { + let msg = "chunk size exceeds limits of usize type"; + return Err(io::Error::new(io::ErrorKind::Other, msg)); + } + + // The buffer stores the current chunk being written out. + let mut buffer = vec![0; chunk_size as usize]; + let mut stream = hyp_res.start()?; + loop { + match body.read_max(&mut buffer)? { + 0 => break, + n => stream.write_all(&buffer[..n])?, + } + } + + stream.end() + } + } + } + /// Preprocess the request for Rocket-specific things. At this time, we're /// only checking for _method in forms. fn preprocess_request(&self, req: &mut Request, data: &Data) { @@ -133,7 +185,7 @@ impl Rocket { /// the request, this function returns an `Err` with the status code. #[doc(hidden)] pub fn dispatch<'r>(&self, request: &'r Request, mut data: Data) - -> Result, StatusCode> { + -> Result, Status> { // Go through the list of matching routes until we fail or succeed. info!("{}:", request); let matches = self.router.route(&request); @@ -143,56 +195,41 @@ impl Rocket { request.set_params(route); // Dispatch the request to the handler. - let response = (route.handler)(&request, data); + let outcome = (route.handler)(&request, data); // Check if the request processing completed or if the request needs // to be forwarded. If it does, continue the loop to try again. - info_!("{} {}", White.paint("Response:"), response); - match response { - Outcome::Success(responder) => return Ok(responder), - Outcome::Failure(status_code) => return Err(status_code), + info_!("{} {}", White.paint("Outcome:"), outcome); + match outcome { + Outcome::Success(response) => return Ok(response), + Outcome::Failure(status) => return Err(status), Outcome::Forward(unused_data) => data = unused_data, }; } - error_!("No matching routes."); - Err(StatusCode::NotFound) + error_!("No matching routes for {}.", request); + Err(Status::NotFound) } - // Attempts to send a response to the client by using the catcher for the - // given status code. If no catcher is found (including the defaults), the - // 500 internal server error catcher is used. If the catcher fails to - // respond, this function returns `false`. It returns `true` if a response - // was sucessfully sent to the client. + // TODO: DOC. #[doc(hidden)] - pub fn handle_error<'r>(&self, - code: StatusCode, - req: &'r Request, - response: FreshHyperResponse) -> bool { - // Find the catcher or use the one for internal server errors. - let catcher = self.catchers.get(&code.to_u16()).unwrap_or_else(|| { - error_!("No catcher found for {}.", code); - warn_!("Using internal server error catcher."); - self.catchers.get(&500).expect("500 catcher should exist!") + pub fn handle_error<'r>(&self, status: Status, req: &'r Request) -> Response<'r> { + warn_!("Responding with {} catcher.", Red.paint(&status)); + + // Try to get the active catcher but fallback to user's 500 catcher. + let catcher = self.catchers.get(&status.code).unwrap_or_else(|| { + error_!("No catcher found for {}. Using 500 catcher.", status); + self.catchers.get(&500).expect("500 catcher.") }); - if let Some(mut responder) = catcher.handle(Error::NoRoute, req).responder() { - if !responder.respond(response).is_success() { - error_!("Catcher outcome was unsuccessul; aborting response."); - return false; - } else { - info_!("Responded with {} catcher.", White.paint(code)); - } - } else { - error_!("Catcher returned an incomplete response."); - warn_!("Using default error response."); - let catcher = self.default_catchers.get(&code.to_u16()) - .unwrap_or(self.default_catchers.get(&500).expect("500 default")); - let responder = catcher.handle(Error::Internal, req).responder(); - responder.unwrap().respond(response).expect("default catcher failed") - } - - true + // Dispatch to the user's catcher. If it fails, use the default 500. + let error = Error::NoRoute; + catcher.handle(error, req).unwrap_or_else(|err_status| { + error_!("Catcher failed with status: {}!", err_status); + warn_!("Using default 500 error catcher."); + let default = self.default_catchers.get(&500).expect("Default 500"); + default.handle(error, req).expect("Default 500 response.") + }) } /// Create a new `Rocket` application using the configuration information in @@ -300,11 +337,12 @@ impl Rocket { /// `hi` route. /// /// ```rust - /// use rocket::{Request, Response, Route, Data}; + /// use rocket::{Request, Route, Data}; + /// use rocket::handler::Outcome; /// use rocket::http::Method::*; /// - /// fn hi(_: &Request, _: Data) -> Response<'static> { - /// Response::success("Hello!") + /// fn hi(_: &Request, _: Data) -> Outcome { + /// Outcome::of("Hello!") /// } /// /// # if false { // We don't actually want to launch the server in an example. diff --git a/lib/src/router/collider.rs b/lib/src/router/collider.rs index 223bad9c..91fef1ce 100644 --- a/lib/src/router/collider.rs +++ b/lib/src/router/collider.rs @@ -51,7 +51,7 @@ mod tests { use router::Collider; use request::Request; use data::Data; - use response::Response; + use handler::Outcome; use router::route::Route; use http::{Method, ContentType}; use http::uri::URI; @@ -60,8 +60,8 @@ mod tests { type SimpleRoute = (Method, &'static str); - fn dummy_handler(_req: &Request, _: Data) -> Response<'static> { - Response::success("hi") + fn dummy_handler(_req: &Request, _: Data) -> Outcome<'static> { + Outcome::of("hi") } fn m_collide(a: SimpleRoute, b: SimpleRoute) -> bool { diff --git a/lib/src/router/mod.rs b/lib/src/router/mod.rs index 6352b20b..2c3f0561 100644 --- a/lib/src/router/mod.rs +++ b/lib/src/router/mod.rs @@ -72,10 +72,12 @@ mod test { use http::Method; use http::Method::*; use http::uri::URI; - use {Response, Request, Data}; + use request::Request; + use data::Data; + use handler::Outcome; - fn dummy_handler(_req: &Request, _: Data) -> Response<'static> { - Response::success("hi") + fn dummy_handler(_req: &Request, _: Data) -> Outcome<'static> { + Outcome::of("hi") } fn router_with_routes(routes: &[&'static str]) -> Router { diff --git a/lib/src/router/route.rs b/lib/src/router/route.rs index 440f8f22..80e10f6f 100644 --- a/lib/src/router/route.rs +++ b/lib/src/router/route.rs @@ -45,7 +45,7 @@ impl Route { handler: handler, rank: default_rank(path.as_ref()), path: URIBuf::from(path.as_ref()), - content_type: ContentType::any(), + content_type: ContentType::Any, } } @@ -58,7 +58,7 @@ impl Route { path: URIBuf::from(path.as_ref()), handler: handler, rank: rank, - content_type: ContentType::any(), + content_type: ContentType::Any, } } @@ -133,7 +133,7 @@ impl fmt::Debug for Route { impl<'a> From<&'a StaticRouteInfo> for Route { fn from(info: &'a StaticRouteInfo) -> Route { let mut route = Route::new(info.method, info.path, info.handler); - route.content_type = info.format.clone().unwrap_or(ContentType::any()); + route.content_type = info.format.clone().unwrap_or(ContentType::Any); if let Some(rank) = info.rank { route.rank = rank; } diff --git a/lib/src/testing.rs b/lib/src/testing.rs index 7d19c961..83a0567f 100644 --- a/lib/src/testing.rs +++ b/lib/src/testing.rs @@ -99,8 +99,6 @@ //! } //! ``` -use std::io::Cursor; -use outcome::Outcome::*; use http::{hyper, Method}; use {Rocket, Request, Data}; @@ -201,46 +199,20 @@ impl MockRequest { /// /// # fn main() { /// let rocket = rocket::ignite().mount("/", routes![hello]); - /// let req = MockRequest::new(Get, "/"); - /// let result = req.dispatch_with(&rocket); - /// assert_eq!(result.unwrap().as_str(), "Hello, world!"); + /// let result = MockRequest::new(Get, "/").dispatch_with(&rocket); + /// assert_eq!(&result.unwrap(), "Hello, world!"); /// # } /// ``` - pub fn dispatch_with(mut self, rocket: &Rocket) -> Option { - let request = self.request; + /// FIXME: Can now return Response to get all info! + pub fn dispatch_with(&mut self, rocket: &Rocket) -> Option { let data = ::std::mem::replace(&mut self.data, Data::new(vec![])); - let mut response = Cursor::new(vec![]); - - // Create a new scope so we can get the inner from response later. - let ok = { - let mut h_h = hyper::HyperHeaders::new(); - let res = hyper::FreshHyperResponse::new(&mut response, &mut h_h); - match rocket.dispatch(&request, data) { - Ok(mut responder) => { - match responder.respond(res) { - Success(_) => true, - Failure(_) => false, - Forward((code, r)) => rocket.handle_error(code, &request, r) - } - } - Err(code) => rocket.handle_error(code, &request, res) - } + let mut response = match rocket.dispatch(&self.request, data) { + Ok(response) => response, + // FIXME: Send to catcher? Not sure what user would want. + Err(_status) => return None }; - if !ok { - return None; - } - - match String::from_utf8(response.into_inner()) { - Ok(string) => { - // TODO: Expose the full response (with headers) somewhow. - string.find("\r\n\r\n").map(|i| string[(i + 4)..].to_string()) - } - Err(e) => { - error_!("Could not create string from response: {:?}", e); - None - } - } + response.body().and_then(|body| body.to_string()) } }