HTTP handling refactoring.

Refactor to lookup/route and resource abstractions.

Bake in futures/async support.

Implement more of the HTTP standard.
This commit is contained in:
Magnus Hoff 2017-08-25 23:57:06 +02:00
parent 62812c2ddf
commit 76302353e1
7 changed files with 167 additions and 70 deletions

View file

@ -16,6 +16,7 @@ mod models;
mod schema;
mod site;
mod state;
mod web;
fn args<'a>() -> clap::ArgMatches<'a> {
use clap::{App, Arg};

View file

@ -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,

View file

@ -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<Option<Self::Resource>, 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<hyper::Method> {
use hyper::Method::*;
vec![Options, Head, Get]
}
fn head(&self) -> futures::BoxFuture<Response, Box<::std::error::Error + Send>> {
futures::finished(Response::new()
.with_status(hyper::StatusCode::Ok)
.with_header(ContentType(TEXT_HTML.clone()))
).boxed()
}
fn get(&self) -> futures::BoxFuture<Response, Box<::std::error::Error + Send>> {
// 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<Response, Self::Error>;
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()
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()
}
},
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()
}
}
} 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()
}
}

View file

@ -15,7 +15,7 @@ impl State {
State { db_connection }
}
pub fn get_article_revision_by_slug(&self, slug: &str) -> Result<Option<models::ArticleRevision>, Box<std::error::Error>> {
pub fn get_article_revision_by_slug(&self, slug: &str) -> Result<Option<models::ArticleRevision>, Box<std::error::Error + Send + Sync>> {
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<Option<models::ArticleRevision>, Box<std::error::Error>> {
pub fn get_article_revision_by_id(&self, article_id: i32) -> Result<Option<models::ArticleRevision>, Box<std::error::Error + Send + Sync>> {
use schema::article_revisions;
Ok(article_revisions::table

11
src/web/lookup.rs Normal file
View file

@ -0,0 +1,11 @@
use super::resource;
use futures;
pub trait Lookup {
type Resource: resource::Resource;
type Error;
type Future: futures::Future<Item=Option<Self::Resource>, Error=Self::Error>;
fn lookup(&self, path: &str, query: Option<&str>, fragment: Option<&str>) -> Self::Future;
}

5
src/web/mod.rs Normal file
View file

@ -0,0 +1,5 @@
mod resource;
mod lookup;
pub use self::resource::*;
pub use self::lookup::*;

30
src/web/resource.rs Normal file
View file

@ -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<std::error::Error + Send>;
pub trait Resource {
fn allow(&self) -> Vec<hyper::Method>;
fn head(&self) -> futures::BoxFuture<server::Response, Error>;
fn get(&self) -> futures::BoxFuture<server::Response, Error>;
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")
}
}