diff --git a/libs/static_resource_derive/src/lib.rs b/libs/static_resource_derive/src/lib.rs index 7cdb939..f5e9a68 100644 --- a/libs/static_resource_derive/src/lib.rs +++ b/libs/static_resource_derive/src/lib.rs @@ -71,8 +71,10 @@ pub fn static_resource(input: TokenStream) -> TokenStream { vec![Options, Head, Get] } - fn head(&self) -> futures::BoxFuture> { - futures::finished(Response::new() + fn head(&self) -> + ::futures::BoxFuture<::hyper::server::Response, Box<::std::error::Error + Send + Sync>> + { + ::futures::finished(::hyper::server::Response::new() .with_status(::hyper::StatusCode::Ok) .with_header(::hyper::header::ContentType( #mime.parse().expect("Statically supplied mime type must be parseable"))) @@ -85,7 +87,9 @@ pub fn static_resource(input: TokenStream) -> TokenStream { ).boxed() } - fn get(self: Box) -> futures::BoxFuture> { + fn get(self: Box) -> + ::futures::BoxFuture<::hyper::server::Response, Box<::std::error::Error + Send + Sync>> + { let body = include_bytes!(#abs_filename); self.head().map(move |head| @@ -95,17 +99,19 @@ pub fn static_resource(input: TokenStream) -> TokenStream { ).boxed() } - fn put(self: Box, _body: hyper::Body) -> futures::BoxFuture> { - futures::finished(self.method_not_allowed()).boxed() + fn put(self: Box, _body: ::hyper::Body) -> + ::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 { - fn checksum() -> &'static str { + pub fn checksum() -> &'static str { #checksum } - fn etag() -> ::hyper::header::EntityTag { + pub fn etag() -> ::hyper::header::EntityTag { ::hyper::header::EntityTag::new(false, Self::checksum().to_owned()) } } diff --git a/src/article_resource.rs b/src/article_resource.rs new file mode 100644 index 0000000..7e0d676 --- /dev/null +++ b/src/article_resource.rs @@ -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>> { + inner: I, + } + + impl<'a, I: Iterator>> EscapeHtml<'a, I> { + fn new(inner: I) -> EscapeHtml<'a, I> { + EscapeHtml { inner } + } + } + + impl<'a, I: Iterator>> Iterator for EscapeHtml<'a, I> { + type Item = Event<'a>; + + fn next(&mut self) -> Option { + 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 { + use hyper::Method::*; + vec![Options, Head, Get, Put] + } + + fn head(&self) -> futures::BoxFuture> { + futures::finished(Response::new() + .with_status(hyper::StatusCode::Ok) + .with_header(ContentType(TEXT_HTML.clone())) + ).boxed() + } + + fn get(self: Box) -> futures::BoxFuture> { + use chrono::{self, TimeZone, Local}; + + #[derive(BartDisplay)] + #[template="templates/article_revision.html"] + struct Template<'a> { + article_id: i32, + revision: i32, + created: &'a chrono::DateTime, + + 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, body: hyper::Body) -> + futures::BoxFuture> + { + // 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() + } +} diff --git a/src/assets.rs b/src/assets.rs new file mode 100644 index 0000000..e610cb3 --- /dev/null +++ b/src/assets.rs @@ -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; diff --git a/src/main.rs b/src/main.rs index 7c660e2..a7310cd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,12 +18,15 @@ extern crate serde_urlencoded; use std::net::SocketAddr; +mod article_resource; +mod assets; mod db; mod models; mod schema; mod site; mod state; mod web; +mod wiki_lookup; fn args<'a>() -> clap::ArgMatches<'a> { use clap::{App, Arg}; @@ -58,12 +61,13 @@ fn core_main() -> Result<(), Box> { let cpu_pool = futures_cpupool::CpuPool::new_num_cpus(); let state = state::State::new(db_pool, cpu_pool); + let lookup = wiki_lookup::WikiLookup::new(state); let server = hyper::server::Http::new() .bind( &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()); diff --git a/src/site.rs b/src/site.rs index 73bd072..845e3ff 100644 --- a/src/site.rs +++ b/src/site.rs @@ -1,7 +1,6 @@ // #[derive(BartDisplay)] can cause unused extern crates warning: #![allow(unused_extern_crates)] -use std::collections::HashMap; use std::fmt; use futures::{self, Future}; @@ -9,56 +8,18 @@ use hyper; use hyper::header::ContentType; use hyper::mime; use hyper::server::*; -use serde_json; -use serde_urlencoded; -use models; -use state::State; -use web::{Lookup, Resource}; - -use pulldown_cmark::Event; - -struct EscapeHtml<'a, I: Iterator>> { - inner: I, -} - -impl<'a, I: Iterator>> EscapeHtml<'a, I> { - fn new(inner: I) -> EscapeHtml<'a, I> { - EscapeHtml { inner } - } -} - -impl<'a, I: Iterator>> Iterator for EscapeHtml<'a, I> { - type Item = Event<'a>; - - fn next(&mut self) -> Option { - 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 -} +use assets::StyleCss; +use web::Lookup; +use wiki_lookup::WikiLookup; 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(); } #[derive(BartDisplay)] #[template = "templates/layout.html"] -struct Layout<'a, T: 'a + fmt::Display> { +pub struct Layout<'a, T: 'a + fmt::Display> { pub title: &'a str, pub body: &'a T, pub style_css_checksum: &'a str, @@ -72,195 +33,13 @@ struct NotFound; #[template = "templates/500.html"] 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 Box>>, -} - -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) - as Box Box> - ); - - lookup_map.insert( - format!("/_assets/script-{}.js", ScriptJs::checksum()), - Box::new(|| Box::new(ScriptJs) as Box) - as Box Box> - ); - - lookup_map.insert( - format!("/_assets/amatic-sc-v9-latin-regular.woff"), - Box::new(|| Box::new(AmaticFont) as Box) - as Box Box> - ); - - WikiLookup { state, lookup_map } - } -} - -impl Lookup for WikiLookup { - type Resource = Box; - type Error = Box<::std::error::Error + Send + Sync>; - type Future = futures::BoxFuture, 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))) - .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 { - use hyper::Method::*; - vec![Options, Head, Get, Put] - } - - fn head(&self) -> futures::BoxFuture> { - futures::finished(Response::new() - .with_status(hyper::StatusCode::Ok) - .with_header(ContentType(TEXT_HTML.clone())) - ).boxed() - } - - fn get(self: Box) -> futures::BoxFuture> { - use chrono::{self, TimeZone, Local}; - - #[derive(BartDisplay)] - #[template="templates/article_revision.html"] - struct Template<'a> { - article_id: i32, - revision: i32, - created: &'a chrono::DateTime, - - 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, body: hyper::Body) -> futures::BoxFuture> { - // 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 { root: WikiLookup, } impl Site { - pub fn new(state: State) -> Site { - Site { - root: WikiLookup::new(state) - } + pub fn new(root: WikiLookup) -> Site { + Site { root } } fn not_found() -> Response { diff --git a/src/wiki_lookup.rs b/src/wiki_lookup.rs new file mode 100644 index 0000000..71366c8 --- /dev/null +++ b/src/wiki_lookup.rs @@ -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 Box + Sync + Send>> = { + let mut lookup_map = HashMap::new(); + + lookup_map.insert( + format!("/_assets/style-{}.css", StyleCss::checksum()), + Box::new(|| Box::new(StyleCss) as Box) + as Box Box + Sync + Send> + ); + + lookup_map.insert( + format!("/_assets/script-{}.js", ScriptJs::checksum()), + Box::new(|| Box::new(ScriptJs) as Box) + as Box Box + Sync + Send> + ); + + lookup_map.insert( + format!("/_assets/amatic-sc-v9-latin-regular.woff"), + Box::new(|| Box::new(AmaticFont) as Box) + as Box Box + 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; + type Error = Box<::std::error::Error + Send + Sync>; + type Future = futures::BoxFuture, 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 + ))) + .boxed() + } else { + futures::finished(None).boxed() + } + } +}