diff --git a/src/mimes.rs b/src/mimes.rs index 6af57e5..bc67f6d 100644 --- a/src/mimes.rs +++ b/src/mimes.rs @@ -2,5 +2,6 @@ use hyper::mime; lazy_static! { pub static ref TEXT_HTML: mime::Mime = "text/html;charset=utf-8".parse().unwrap(); + pub static ref TEXT_PLAIN: mime::Mime = "text/plain;charset=utf-8".parse().unwrap(); pub static ref APPLICATION_JSON: mime::Mime = "application/json".parse().unwrap(); } diff --git a/src/models.rs b/src/models.rs index 0d5a3af..0b088bc 100644 --- a/src/models.rs +++ b/src/models.rs @@ -17,6 +17,16 @@ pub struct ArticleRevision { pub author: Option, } +impl ArticleRevision { + pub fn link(&self) -> &str { + if self.slug.is_empty() { + "." + } else { + &self.slug + } + } +} + #[derive(Debug, Queryable)] pub struct ArticleRevisionStub { pub sequence_number: i32, diff --git a/src/resources/article_resource.rs b/src/resources/article_resource.rs index 8306975..ab3c744 100644 --- a/src/resources/article_resource.rs +++ b/src/resources/article_resource.rs @@ -1,7 +1,7 @@ use chrono::{TimeZone, DateTime, Local}; use futures::{self, Future}; use hyper; -use hyper::header::ContentType; +use hyper::header::{ContentType, Location}; use hyper::server::*; use serde_json; use serde_urlencoded; @@ -22,6 +22,13 @@ pub struct ArticleResource { edit: bool, } +#[derive(Deserialize)] +struct UpdateArticle { + base_revision: i32, + title: String, + body: String, +} + impl ArticleResource { pub fn new(state: State, article_id: i32, revision: i32, edit: bool) -> Self { Self { state, article_id, revision, edit } @@ -55,7 +62,7 @@ pub fn last_updated(article_id: i32, created: &DateTime, author: Option<& impl Resource for ArticleResource { fn allow(&self) -> Vec { use hyper::Method::*; - vec![Options, Head, Get, Put] + vec![Options, Head, Get, Put, Post] } fn head(&self) -> ResponseFuture { @@ -114,13 +121,6 @@ impl Resource for ArticleResource { use futures::Stream; - #[derive(Deserialize)] - struct UpdateArticle { - base_revision: i32, - title: String, - body: String, - } - #[derive(BartDisplay)] #[template="templates/article_contents.html"] struct Template<'a> { @@ -169,4 +169,30 @@ impl Resource for ArticleResource { }) ) } + + fn post(self: Box, body: hyper::Body, identity: Option) -> ResponseFuture { + // TODO Check incoming Content-Type + + use futures::Stream; + + Box::new(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.article_id, update.base_revision, update.title, update.body, identity) + }) + .and_then(|updated| { + futures::finished(Response::new() + .with_status(hyper::StatusCode::SeeOther) + .with_header(ContentType(TEXT_PLAIN.clone())) + .with_header(Location::new(updated.link().to_owned())) + .with_body("See other") + ) + }) + ) + } } diff --git a/src/resources/new_article_resource.rs b/src/resources/new_article_resource.rs index 67868ca..c619535 100644 --- a/src/resources/new_article_resource.rs +++ b/src/resources/new_article_resource.rs @@ -1,6 +1,6 @@ use futures::{self, Future}; use hyper; -use hyper::header::ContentType; +use hyper::header::{ContentType, Location}; use hyper::server::*; use serde_json; use serde_urlencoded; @@ -29,6 +29,13 @@ pub struct NewArticleResource { slug: Option, } +#[derive(Deserialize)] +struct CreateArticle { + base_revision: String, + title: String, + body: String, +} + impl NewArticleResource { pub fn new(state: State, slug: Option) -> Self { Self { state, slug } @@ -98,13 +105,6 @@ impl Resource for NewArticleResource { use chrono::{TimeZone, Local}; use futures::Stream; - #[derive(Deserialize)] - struct CreateArticle { - base_revision: String, - title: String, - body: String, - } - #[derive(BartDisplay)] #[template="templates/article_contents.html"] struct Template<'a> { @@ -158,4 +158,34 @@ impl Resource for NewArticleResource { }) ) } + + fn post(self: Box, body: hyper::Body, identity: Option) -> ResponseFuture { + // TODO Check incoming Content-Type + // TODO Refactor? Reduce duplication with ArticleResource::put? + + use futures::Stream; + + Box::new(body + .concat2() + .map_err(Into::into) + .and_then(|body| { + serde_urlencoded::from_bytes(&body) + .map_err(Into::into) + }) + .and_then(move |arg: CreateArticle| { + if arg.base_revision != NEW { + unimplemented!("Version update conflict"); + } + self.state.create_article(self.slug.clone(), arg.title, arg.body, identity) + }) + .and_then(|updated| { + futures::finished(Response::new() + .with_status(hyper::StatusCode::SeeOther) + .with_header(ContentType(TEXT_PLAIN.clone())) + .with_header(Location::new(updated.link().to_owned())) + .with_body("See other") + ) + }) + ) + } } diff --git a/src/site.rs b/src/site.rs index 00295bb..d0961a7 100644 --- a/src/site.rs +++ b/src/site.rs @@ -121,6 +121,7 @@ impl Service for Site { Head => resource.head(), Get => resource.get(), Put => resource.put(body, identity), + Post => resource.post(body, identity), _ => Box::new(futures::finished(resource.method_not_allowed())) } }, diff --git a/src/web/resource.rs b/src/web/resource.rs index 47b66ba..9af2436 100644 --- a/src/web/resource.rs +++ b/src/web/resource.rs @@ -1,4 +1,5 @@ use futures; +use futures::{Future, Stream}; use hyper::{self, header, mime, server}; use hyper::server::Response; use std; @@ -24,18 +25,18 @@ pub trait Resource { fn put(self: Box, body: hyper::Body, _identity: Option) -> ResponseFuture where Self: 'static { - use futures::{Future, Stream}; - - // TODO Cleanup by moving to the built in never type, !, when it stabilizes - enum Never {}; - impl std::convert::From for hyper::Error { - fn from(_: Never) -> hyper::Error { - panic!() - } - } - Box::new(body - .fold((), |_, _| -> Result<(), Never> { Ok(()) }) + .fold((), |_, _| -> Result<(), hyper::Error> { Ok(()) }) + .map_err(Into::into) + .and_then(move |_| futures::finished(self.method_not_allowed())) + ) + } + + fn post(self: Box, body: hyper::Body, _identity: Option) -> ResponseFuture + where Self: 'static + { + Box::new(body + .fold((), |_, _| -> Result<(), hyper::Error> { Ok(()) }) .map_err(Into::into) .and_then(move |_| futures::finished(self.method_not_allowed())) )