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:
parent
62812c2ddf
commit
76302353e1
7 changed files with 167 additions and 70 deletions
|
@ -16,6 +16,7 @@ mod models;
|
|||
mod schema;
|
||||
mod site;
|
||||
mod state;
|
||||
mod web;
|
||||
|
||||
fn args<'a>() -> clap::ArgMatches<'a> {
|
||||
use clap::{App, Arg};
|
||||
|
|
|
@ -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,
|
||||
|
|
184
src/site.rs
184
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<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()
|
||||
},
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
11
src/web/lookup.rs
Normal 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
5
src/web/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
mod resource;
|
||||
mod lookup;
|
||||
|
||||
pub use self::resource::*;
|
||||
pub use self::lookup::*;
|
30
src/web/resource.rs
Normal file
30
src/web/resource.rs
Normal 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")
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue