Code splitting

This commit is contained in:
Magnus Hoff 2017-09-15 17:28:23 +02:00
parent 7391b2db26
commit 573195d09c
6 changed files with 276 additions and 235 deletions

View file

@ -71,8 +71,10 @@ pub fn static_resource(input: TokenStream) -> TokenStream {
vec![Options, Head, Get] vec![Options, Head, Get]
} }
fn head(&self) -> futures::BoxFuture<Response, Box<::std::error::Error + Send + Sync>> { fn head(&self) ->
futures::finished(Response::new() ::futures::BoxFuture<::hyper::server::Response, Box<::std::error::Error + Send + Sync>>
{
::futures::finished(::hyper::server::Response::new()
.with_status(::hyper::StatusCode::Ok) .with_status(::hyper::StatusCode::Ok)
.with_header(::hyper::header::ContentType( .with_header(::hyper::header::ContentType(
#mime.parse().expect("Statically supplied mime type must be parseable"))) #mime.parse().expect("Statically supplied mime type must be parseable")))
@ -85,7 +87,9 @@ pub fn static_resource(input: TokenStream) -> TokenStream {
).boxed() ).boxed()
} }
fn get(self: Box<Self>) -> futures::BoxFuture<Response, Box<::std::error::Error + Send + Sync>> { fn get(self: Box<Self>) ->
::futures::BoxFuture<::hyper::server::Response, Box<::std::error::Error + Send + Sync>>
{
let body = include_bytes!(#abs_filename); let body = include_bytes!(#abs_filename);
self.head().map(move |head| self.head().map(move |head|
@ -95,17 +99,19 @@ pub fn static_resource(input: TokenStream) -> TokenStream {
).boxed() ).boxed()
} }
fn put(self: Box<Self>, _body: hyper::Body) -> futures::BoxFuture<Response, Box<::std::error::Error + Send + Sync>> { fn put(self: Box<Self>, _body: ::hyper::Body) ->
futures::finished(self.method_not_allowed()).boxed() ::futures::BoxFuture<::hyper::server::Response, Box<::std::error::Error + Send + Sync>>
{
::futures::finished(self.method_not_allowed()).boxed()
} }
} }
impl #impl_generics #name #ty_generics #where_clause { impl #impl_generics #name #ty_generics #where_clause {
fn checksum() -> &'static str { pub fn checksum() -> &'static str {
#checksum #checksum
} }
fn etag() -> ::hyper::header::EntityTag { pub fn etag() -> ::hyper::header::EntityTag {
::hyper::header::EntityTag::new(false, Self::checksum().to_owned()) ::hyper::header::EntityTag::new(false, Self::checksum().to_owned())
} }
} }

158
src/article_resource.rs Normal file
View file

@ -0,0 +1,158 @@
use futures::{self, Future};
use hyper;
use hyper::header::ContentType;
use hyper::mime;
use hyper::server::*;
use serde_json;
use serde_urlencoded;
use assets::{StyleCss, ScriptJs};
use models;
use site::Layout;
use state::State;
use web::Resource;
lazy_static! {
static ref TEXT_HTML: mime::Mime = "text/html;charset=utf-8".parse().unwrap();
static ref APPLICATION_JSON: mime::Mime = "application/json".parse().unwrap();
}
fn render_markdown(src: &str) -> String {
use pulldown_cmark::Event;
struct EscapeHtml<'a, I: Iterator<Item=Event<'a>>> {
inner: I,
}
impl<'a, I: Iterator<Item=Event<'a>>> EscapeHtml<'a, I> {
fn new(inner: I) -> EscapeHtml<'a, I> {
EscapeHtml { inner }
}
}
impl<'a, I: Iterator<Item=Event<'a>>> Iterator for EscapeHtml<'a, I> {
type Item = Event<'a>;
fn next(&mut self) -> Option<Self::Item> {
use pulldown_cmark::Event::{Text, Html, InlineHtml};
match self.inner.next() {
Some(Html(x)) => Some(Text(x)),
Some(InlineHtml(x)) => Some(Text(x)),
x => x
}
}
}
use pulldown_cmark::{Parser, html};
let p = EscapeHtml::new(Parser::new(src));
let mut buf = String::new();
html::push_html(&mut buf, p);
buf
}
pub struct ArticleResource {
state: State,
data: models::ArticleRevision,
}
impl ArticleResource {
pub fn new(state: State, data: models::ArticleRevision) -> Self {
Self { state, data }
}
}
impl Resource for ArticleResource {
fn allow(&self) -> Vec<hyper::Method> {
use hyper::Method::*;
vec![Options, Head, Get, Put]
}
fn head(&self) -> futures::BoxFuture<Response, Box<::std::error::Error + Send + Sync>> {
futures::finished(Response::new()
.with_status(hyper::StatusCode::Ok)
.with_header(ContentType(TEXT_HTML.clone()))
).boxed()
}
fn get(self: Box<Self>) -> futures::BoxFuture<Response, Box<::std::error::Error + Send + Sync>> {
use chrono::{self, TimeZone, Local};
#[derive(BartDisplay)]
#[template="templates/article_revision.html"]
struct Template<'a> {
article_id: i32,
revision: i32,
created: &'a chrono::DateTime<Local>,
title: &'a str,
raw: &'a str,
rendered: String,
script_js_checksum: &'a str,
}
self.head().map(move |head|
head
.with_body(Layout {
title: &self.data.title,
body: &Template {
article_id: self.data.article_id,
revision: self.data.revision,
created: &Local.from_utc_datetime(&self.data.created),
title: &self.data.title,
raw: &self.data.body,
rendered: render_markdown(&self.data.body),
script_js_checksum: ScriptJs::checksum(),
},
style_css_checksum: StyleCss::checksum(),
}.to_string())
).boxed()
}
fn put(self: Box<Self>, body: hyper::Body) ->
futures::BoxFuture<Response, Box<::std::error::Error + Send + Sync>>
{
// TODO Check incoming Content-Type
use chrono::{TimeZone, Local};
use futures::Stream;
#[derive(Deserialize)]
struct UpdateArticle {
base_revision: i32,
body: String,
}
#[derive(Serialize)]
struct PutResponse<'a> {
revision: i32,
rendered: &'a str,
created: &'a str,
}
body
.concat2()
.map_err(Into::into)
.and_then(|body| {
serde_urlencoded::from_bytes(&body)
.map_err(Into::into)
})
.and_then(move |update: UpdateArticle| {
self.state.update_article(self.data.article_id, update.base_revision, update.body)
})
.and_then(|updated| {
futures::finished(Response::new()
.with_status(hyper::StatusCode::Ok)
.with_header(ContentType(APPLICATION_JSON.clone()))
.with_body(serde_json::to_string(&PutResponse {
revision: updated.revision,
rendered: &render_markdown(&updated.body),
created: &Local.from_utc_datetime(&updated.created).to_string(),
}).expect("Should never fail"))
)
})
.boxed()
}
}

19
src/assets.rs Normal file
View file

@ -0,0 +1,19 @@
use futures::Future;
use web::Resource;
#[derive(StaticResource)]
#[filename = "assets/style.css"]
#[mime = "text/css"]
pub struct StyleCss;
#[derive(StaticResource)]
#[filename = "assets/script.js"]
#[mime = "application/javascript"]
pub struct ScriptJs;
// SIL Open Font License 1.1: http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL
// Copyright 2015 The Amatic SC Project Authors (contact@sansoxygen.com)
#[derive(StaticResource)]
#[filename = "assets/amatic-sc-v9-latin-regular.woff"]
#[mime = "application/font-woff"]
pub struct AmaticFont;

View file

@ -18,12 +18,15 @@ extern crate serde_urlencoded;
use std::net::SocketAddr; use std::net::SocketAddr;
mod article_resource;
mod assets;
mod db; mod db;
mod models; mod models;
mod schema; mod schema;
mod site; mod site;
mod state; mod state;
mod web; mod web;
mod wiki_lookup;
fn args<'a>() -> clap::ArgMatches<'a> { fn args<'a>() -> clap::ArgMatches<'a> {
use clap::{App, Arg}; use clap::{App, Arg};
@ -58,12 +61,13 @@ fn core_main() -> Result<(), Box<std::error::Error>> {
let cpu_pool = futures_cpupool::CpuPool::new_num_cpus(); let cpu_pool = futures_cpupool::CpuPool::new_num_cpus();
let state = state::State::new(db_pool, cpu_pool); let state = state::State::new(db_pool, cpu_pool);
let lookup = wiki_lookup::WikiLookup::new(state);
let server = let server =
hyper::server::Http::new() hyper::server::Http::new()
.bind( .bind(
&SocketAddr::new(bind_host, bind_port), &SocketAddr::new(bind_host, bind_port),
move || Ok(site::Site::new(state.clone())) move || Ok(site::Site::new(lookup.clone()))
)?; )?;
println!("Listening on http://{}", server.local_addr().unwrap()); println!("Listening on http://{}", server.local_addr().unwrap());

View file

@ -1,7 +1,6 @@
// #[derive(BartDisplay)] can cause unused extern crates warning: // #[derive(BartDisplay)] can cause unused extern crates warning:
#![allow(unused_extern_crates)] #![allow(unused_extern_crates)]
use std::collections::HashMap;
use std::fmt; use std::fmt;
use futures::{self, Future}; use futures::{self, Future};
@ -9,56 +8,18 @@ use hyper;
use hyper::header::ContentType; use hyper::header::ContentType;
use hyper::mime; use hyper::mime;
use hyper::server::*; use hyper::server::*;
use serde_json;
use serde_urlencoded;
use models; use assets::StyleCss;
use state::State; use web::Lookup;
use web::{Lookup, Resource}; use wiki_lookup::WikiLookup;
use pulldown_cmark::Event;
struct EscapeHtml<'a, I: Iterator<Item=Event<'a>>> {
inner: I,
}
impl<'a, I: Iterator<Item=Event<'a>>> EscapeHtml<'a, I> {
fn new(inner: I) -> EscapeHtml<'a, I> {
EscapeHtml { inner }
}
}
impl<'a, I: Iterator<Item=Event<'a>>> Iterator for EscapeHtml<'a, I> {
type Item = Event<'a>;
fn next(&mut self) -> Option<Self::Item> {
use pulldown_cmark::Event::{Text, Html, InlineHtml};
match self.inner.next() {
Some(Html(x)) => Some(Text(x)),
Some(InlineHtml(x)) => Some(Text(x)),
x => x
}
}
}
fn render_markdown(src: &str) -> String {
use pulldown_cmark::{Parser, html};
let p = EscapeHtml::new(Parser::new(src));
let mut buf = String::new();
html::push_html(&mut buf, p);
buf
}
lazy_static! { lazy_static! {
static ref TEXT_HTML: mime::Mime = "text/html;charset=utf-8".parse().unwrap(); static ref TEXT_HTML: mime::Mime = "text/html;charset=utf-8".parse().unwrap();
static ref APPLICATION_JSON: mime::Mime = "application/json".parse().unwrap();
} }
#[derive(BartDisplay)] #[derive(BartDisplay)]
#[template = "templates/layout.html"] #[template = "templates/layout.html"]
struct Layout<'a, T: 'a + fmt::Display> { pub struct Layout<'a, T: 'a + fmt::Display> {
pub title: &'a str, pub title: &'a str,
pub body: &'a T, pub body: &'a T,
pub style_css_checksum: &'a str, pub style_css_checksum: &'a str,
@ -72,195 +33,13 @@ struct NotFound;
#[template = "templates/500.html"] #[template = "templates/500.html"]
struct InternalServerError; struct InternalServerError;
#[derive(StaticResource)]
#[filename = "assets/style.css"]
#[mime = "text/css"]
struct StyleCss;
#[derive(StaticResource)]
#[filename = "assets/script.js"]
#[mime = "application/javascript"]
struct ScriptJs;
// SIL Open Font License 1.1: http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL
// Copyright 2015 The Amatic SC Project Authors (contact@sansoxygen.com)
#[derive(StaticResource)]
#[filename = "assets/amatic-sc-v9-latin-regular.woff"]
#[mime = "application/font-woff"]
struct AmaticFont;
struct WikiLookup {
state: State,
lookup_map: HashMap<String, Box<Fn() -> Box<Resource + Sync + Send>>>,
}
impl WikiLookup {
fn new(state: State) -> WikiLookup {
let mut lookup_map = HashMap::new();
lookup_map.insert(
format!("/_assets/style-{}.css", StyleCss::checksum()),
Box::new(|| Box::new(StyleCss) as Box<Resource + Sync + Send>)
as Box<Fn() -> Box<Resource + Sync + Send>>
);
lookup_map.insert(
format!("/_assets/script-{}.js", ScriptJs::checksum()),
Box::new(|| Box::new(ScriptJs) as Box<Resource + Sync + Send>)
as Box<Fn() -> Box<Resource + Sync + Send>>
);
lookup_map.insert(
format!("/_assets/amatic-sc-v9-latin-regular.woff"),
Box::new(|| Box::new(AmaticFont) as Box<Resource + Sync + Send>)
as Box<Fn() -> Box<Resource + Sync + Send>>
);
WikiLookup { state, lookup_map }
}
}
impl Lookup for WikiLookup {
type Resource = Box<Resource + Send + Sync>;
type Error = Box<::std::error::Error + Send + Sync>;
type Future = futures::BoxFuture<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(
self.lookup_map.get(path).map(|x| x())
).boxed();
}
let slug = &path[1..];
if let Ok(article_id) = slug.parse() {
let state = self.state.clone();
self.state.get_article_revision_by_id(article_id)
.and_then(|x| Ok(x.map(move |article| Box::new(ArticleResource::new(state, article)) as Box<Resource + Sync + Send>)))
.boxed()
} else {
futures::finished(None).boxed()
}
}
}
struct ArticleResource {
state: State,
data: models::ArticleRevision,
}
impl ArticleResource {
fn new(state: State, data: models::ArticleRevision) -> Self {
Self { state, data }
}
}
impl Resource for ArticleResource {
fn allow(&self) -> Vec<hyper::Method> {
use hyper::Method::*;
vec![Options, Head, Get, Put]
}
fn head(&self) -> futures::BoxFuture<Response, Box<::std::error::Error + Send + Sync>> {
futures::finished(Response::new()
.with_status(hyper::StatusCode::Ok)
.with_header(ContentType(TEXT_HTML.clone()))
).boxed()
}
fn get(self: Box<Self>) -> futures::BoxFuture<Response, Box<::std::error::Error + Send + Sync>> {
use chrono::{self, TimeZone, Local};
#[derive(BartDisplay)]
#[template="templates/article_revision.html"]
struct Template<'a> {
article_id: i32,
revision: i32,
created: &'a chrono::DateTime<Local>,
title: &'a str,
raw: &'a str,
rendered: String,
script_js_checksum: &'a str,
}
self.head().map(move |head|
head
.with_body(Layout {
title: &self.data.title,
body: &Template {
article_id: self.data.article_id,
revision: self.data.revision,
created: &Local.from_utc_datetime(&self.data.created),
title: &self.data.title,
raw: &self.data.body,
rendered: render_markdown(&self.data.body),
script_js_checksum: ScriptJs::checksum(),
},
style_css_checksum: StyleCss::checksum(),
}.to_string())
).boxed()
}
fn put(self: Box<Self>, body: hyper::Body) -> futures::BoxFuture<Response, Box<::std::error::Error + Send + Sync>> {
// TODO Check incoming Content-Type
use chrono::{TimeZone, Local};
use futures::Stream;
#[derive(Deserialize)]
struct UpdateArticle {
base_revision: i32,
body: String,
}
#[derive(Serialize)]
struct PutResponse<'a> {
revision: i32,
rendered: &'a str,
created: &'a str,
}
body
.concat2()
.map_err(Into::into)
.and_then(|body| {
serde_urlencoded::from_bytes(&body)
.map_err(Into::into)
})
.and_then(move |update: UpdateArticle| {
self.state.update_article(self.data.article_id, update.base_revision, update.body)
})
.and_then(|updated| {
futures::finished(Response::new()
.with_status(hyper::StatusCode::Ok)
.with_header(ContentType(APPLICATION_JSON.clone()))
.with_body(serde_json::to_string(&PutResponse {
revision: updated.revision,
rendered: &render_markdown(&updated.body),
created: &Local.from_utc_datetime(&updated.created).to_string(),
}).expect("Should never fail"))
)
})
.boxed()
}
}
pub struct Site { pub struct Site {
root: WikiLookup, root: WikiLookup,
} }
impl Site { impl Site {
pub fn new(state: State) -> Site { pub fn new(root: WikiLookup) -> Site {
Site { Site { root }
root: WikiLookup::new(state)
}
} }
fn not_found() -> Response { fn not_found() -> Response {

75
src/wiki_lookup.rs Normal file
View file

@ -0,0 +1,75 @@
use std::collections::HashMap;
use futures::{self, Future};
use assets::*;
use article_resource::ArticleResource;
use state::State;
use web::{Lookup, Resource};
lazy_static! {
static ref LOOKUP_MAP: HashMap<String, Box<Fn() -> Box<Resource + Sync + Send> + Sync + Send>> = {
let mut lookup_map = HashMap::new();
lookup_map.insert(
format!("/_assets/style-{}.css", StyleCss::checksum()),
Box::new(|| Box::new(StyleCss) as Box<Resource + Sync + Send>)
as Box<Fn() -> Box<Resource + Sync + Send> + Sync + Send>
);
lookup_map.insert(
format!("/_assets/script-{}.js", ScriptJs::checksum()),
Box::new(|| Box::new(ScriptJs) as Box<Resource + Sync + Send>)
as Box<Fn() -> Box<Resource + Sync + Send> + Sync + Send>
);
lookup_map.insert(
format!("/_assets/amatic-sc-v9-latin-regular.woff"),
Box::new(|| Box::new(AmaticFont) as Box<Resource + Sync + Send>)
as Box<Fn() -> Box<Resource + Sync + Send> + Sync + Send>
);
lookup_map
};
}
#[derive(Clone)]
pub struct WikiLookup {
state: State
}
impl WikiLookup {
pub fn new(state: State) -> WikiLookup {
WikiLookup { state }
}
}
impl Lookup for WikiLookup {
type Resource = Box<Resource + Send + Sync>;
type Error = Box<::std::error::Error + Send + Sync>;
type Future = futures::BoxFuture<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(
LOOKUP_MAP.get(path).map(|x| x())
).boxed();
}
let slug = &path[1..];
if let Ok(article_id) = slug.parse() {
let state = self.state.clone();
self.state.get_article_revision_by_id(article_id)
.and_then(|x| Ok(x.map(move |article|
Box::new(ArticleResource::new(state, article)) as Box<Resource + Sync + Send>
)))
.boxed()
} else {
futures::finished(None).boxed()
}
}
}