From 76302353e1ff130dccc1078a04cc52d5ea5985f6 Mon Sep 17 00:00:00 2001 From: Magnus Hoff Date: Fri, 25 Aug 2017 23:57:06 +0200 Subject: [PATCH] HTTP handling refactoring. Refactor to lookup/route and resource abstractions. Bake in futures/async support. Implement more of the HTTP standard. --- src/main.rs | 1 + src/models.rs | 2 +- src/site.rs | 184 ++++++++++++++++++++++++++++---------------- src/state.rs | 4 +- src/web/lookup.rs | 11 +++ src/web/mod.rs | 5 ++ src/web/resource.rs | 30 ++++++++ 7 files changed, 167 insertions(+), 70 deletions(-) create mode 100644 src/web/lookup.rs create mode 100644 src/web/mod.rs create mode 100644 src/web/resource.rs diff --git a/src/main.rs b/src/main.rs index 3481b02..7309afa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,6 +16,7 @@ mod models; mod schema; mod site; mod state; +mod web; fn args<'a>() -> clap::ArgMatches<'a> { use clap::{App, Arg}; diff --git a/src/models.rs b/src/models.rs index 64893d2..ad0cd4c 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,6 +1,6 @@ use chrono; -#[derive(BartDisplay, Debug, Queryable)] +#[derive(BartDisplay, Clone, Debug, Queryable)] #[template="templates/article_revision.html"] pub struct ArticleRevision { pub article_id: i32, diff --git a/src/site.rs b/src/site.rs index 68b3f90..1cf6ea0 100644 --- a/src/site.rs +++ b/src/site.rs @@ -5,7 +5,10 @@ use hyper; use hyper::header::ContentType; use hyper::mime; use hyper::server::*; + +use models; use state::State; +use web::{Lookup, Resource}; lazy_static! { static ref TEXT_HTML: mime::Mime = "text/html;charset=utf-8".parse().unwrap(); @@ -26,13 +29,109 @@ struct NotFound; #[template = "templates/500.html"] struct InternalServerError; +struct WikiLookup { + state: State, +} + +impl Lookup for WikiLookup { + type Resource = ArticleResource; + type Error = Box<::std::error::Error + Send>; + type Future = futures::future::FutureResult, Self::Error>; + + fn lookup(&self, path: &str, _query: Option<&str>, _fragment: Option<&str>) -> Self::Future { + assert!(path.starts_with("/")); + + if path.starts_with("/_") { + // Reserved namespace + return futures::finished(None); + } + + let slug = &path[1..]; + if let Ok(article_id) = slug.parse() { + match self.state.get_article_revision_by_id(article_id) { + Ok(Some(article)) => { + futures::finished(Some(ArticleResource::new(article))) + }, + Ok(None) => futures::finished(None), + Err(err) => futures::failed(err), + } + } else { + futures::finished(None) + } + } +} + +struct ArticleResource { + data: models::ArticleRevision, +} + +impl ArticleResource { + fn new(data: models::ArticleRevision) -> Self { + Self { + data + } + } +} + +impl Resource for ArticleResource { + fn allow(&self) -> Vec { + use hyper::Method::*; + vec![Options, Head, Get] + } + + fn head(&self) -> futures::BoxFuture> { + futures::finished(Response::new() + .with_status(hyper::StatusCode::Ok) + .with_header(ContentType(TEXT_HTML.clone())) + ).boxed() + } + + fn get(&self) -> futures::BoxFuture> { + // Accidental clone here: + let data = self.data.clone(); + + self.head().map(move |head| + head + .with_body(Layout { + title: &data.title, + body: &data + }.to_string()) + ).boxed() + } +} + + pub struct Site { - state: State + root: WikiLookup, } impl Site { pub fn new(state: State) -> Site { - Site { state } + Site { + root: WikiLookup { state } + } + } + + fn not_found() -> Response { + Response::new() + .with_header(ContentType(TEXT_HTML.clone())) + .with_body(Layout { + title: "Not found", + body: &NotFound, + }.to_string()) + .with_status(hyper::StatusCode::NotFound) + } + + fn internal_server_error(err: Box<::std::error::Error + Send>) -> Response { + eprintln!("Internal Server Error:\n{:#?}", err); + + Response::new() + .with_header(ContentType(TEXT_HTML.clone())) + .with_body(Layout { + title: "Internal server error", + body: &InternalServerError, + }.to_string()) + .with_status(hyper::StatusCode::InternalServerError) } } @@ -43,72 +142,23 @@ impl Service for Site { type Future = futures::BoxFuture; fn call(&self, req: Request) -> Self::Future { - println!("{} {}", req.method(), req.path()); + let (method, uri, _http_version, _headers, _body) = req.deconstruct(); + println!("{} {}", method, uri); - let path = req.path(); - - if path.starts_with("/_") { - futures::finished( - Response::new() - .with_header(ContentType(TEXT_HTML.clone())) - .with_body(Layout { - title: "Not found", - body: &NotFound, - }.to_string()) - .with_status(hyper::StatusCode::NotFound) - ).boxed() - } else { - assert!(path.starts_with("/")); - let slug = &path[1..]; - if let Ok(article_id) = slug.parse() { - match self.state.get_article_revision_by_id(article_id) { - Ok(Some(article)) => { - futures::finished( - Response::new() - .with_header(ContentType(TEXT_HTML.clone())) - .with_body(Layout { - title: &article.title, - body: &article - }.to_string()) - .with_status(hyper::StatusCode::Ok) - ).boxed() - }, - Ok(None) => { - futures::finished( - Response::new() - .with_header(ContentType(TEXT_HTML.clone())) - .with_body(Layout { - title: "Not found", - body: &NotFound, - }.to_string()) - .with_status(hyper::StatusCode::NotFound) - ).boxed() - }, - Err(err) => { - eprintln!("Error while servicing request {} {}:\n{:#?}", req.method(), req.path(), err); - futures::finished( - Response::new() - .with_header(ContentType(TEXT_HTML.clone())) - .with_body(Layout { - title: "Internal server error", - body: &InternalServerError, - }.to_string()) - .with_status(hyper::StatusCode::InternalServerError) - ).boxed() + self.root.lookup(uri.path(), uri.query(), None /*uri.fragment()*/) + .and_then(move |resource| match resource { + Some(resource) => { + use hyper::Method::*; + match method { + Options => futures::finished(resource.options()).boxed(), + Head => resource.head(), + Get => resource.get(), + _ => futures::finished(resource.method_not_allowed()).boxed() } - } - } else { - // slugs must be article IDs... for now - futures::finished( - Response::new() - .with_header(ContentType(TEXT_HTML.clone())) - .with_body(Layout { - title: "Not found", - body: &NotFound, - }.to_string()) - .with_status(hyper::StatusCode::NotFound) - ).boxed() - } - } + }, + None => futures::finished(Self::not_found()).boxed() + }) + .or_else(|err| Ok(Self::internal_server_error(err))) + .boxed() } } diff --git a/src/state.rs b/src/state.rs index d1c7375..814ff33 100644 --- a/src/state.rs +++ b/src/state.rs @@ -15,7 +15,7 @@ impl State { State { db_connection } } - pub fn get_article_revision_by_slug(&self, slug: &str) -> Result, Box> { + pub fn get_article_revision_by_slug(&self, slug: &str) -> Result, Box> { Ok(Some(models::ArticleRevision { article_id: 0, revision: 0, @@ -25,7 +25,7 @@ impl State { })) } - pub fn get_article_revision_by_id(&self, article_id: i32) -> Result, Box> { + pub fn get_article_revision_by_id(&self, article_id: i32) -> Result, Box> { use schema::article_revisions; Ok(article_revisions::table diff --git a/src/web/lookup.rs b/src/web/lookup.rs new file mode 100644 index 0000000..6132f9c --- /dev/null +++ b/src/web/lookup.rs @@ -0,0 +1,11 @@ +use super::resource; + +use futures; + +pub trait Lookup { + type Resource: resource::Resource; + type Error; + type Future: futures::Future, Error=Self::Error>; + + fn lookup(&self, path: &str, query: Option<&str>, fragment: Option<&str>) -> Self::Future; +} diff --git a/src/web/mod.rs b/src/web/mod.rs new file mode 100644 index 0000000..32cb811 --- /dev/null +++ b/src/web/mod.rs @@ -0,0 +1,5 @@ +mod resource; +mod lookup; + +pub use self::resource::*; +pub use self::lookup::*; diff --git a/src/web/resource.rs b/src/web/resource.rs new file mode 100644 index 0000000..ab42f2f --- /dev/null +++ b/src/web/resource.rs @@ -0,0 +1,30 @@ +use futures; +use hyper::{self, header, mime, server}; +use hyper::server::Response; +use std; + +lazy_static! { + static ref TEXT_PLAIN: mime::Mime = "text/plain;charset=utf-8".parse().unwrap(); +} + +type Error = Box; + +pub trait Resource { + fn allow(&self) -> Vec; + fn head(&self) -> futures::BoxFuture; + fn get(&self) -> futures::BoxFuture; + + fn options(&self) -> Response { + Response::new() + .with_status(hyper::StatusCode::Ok) + .with_header(header::Allow(self.allow())) + } + + fn method_not_allowed(&self) -> Response { + Response::new() + .with_status(hyper::StatusCode::MethodNotAllowed) + .with_header(header::Allow(self.allow())) + .with_header(header::ContentType(TEXT_PLAIN.clone())) + .with_body("Method not allowed\n") + } +}