Code splitting
This commit is contained in:
parent
7391b2db26
commit
573195d09c
6 changed files with 276 additions and 235 deletions
|
@ -71,8 +71,10 @@ pub fn static_resource(input: TokenStream) -> TokenStream {
|
|||
vec![Options, Head, Get]
|
||||
}
|
||||
|
||||
fn head(&self) -> futures::BoxFuture<Response, Box<::std::error::Error + Send + Sync>> {
|
||||
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<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);
|
||||
|
||||
self.head().map(move |head|
|
||||
|
@ -95,17 +99,19 @@ pub fn static_resource(input: TokenStream) -> TokenStream {
|
|||
).boxed()
|
||||
}
|
||||
|
||||
fn put(self: Box<Self>, _body: hyper::Body) -> futures::BoxFuture<Response, Box<::std::error::Error + Send + Sync>> {
|
||||
futures::finished(self.method_not_allowed()).boxed()
|
||||
fn put(self: Box<Self>, _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())
|
||||
}
|
||||
}
|
||||
|
|
158
src/article_resource.rs
Normal file
158
src/article_resource.rs
Normal 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
19
src/assets.rs
Normal 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;
|
|
@ -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<std::error::Error>> {
|
|||
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());
|
||||
|
|
233
src/site.rs
233
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<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
|
||||
}
|
||||
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<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 {
|
||||
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 {
|
||||
|
|
75
src/wiki_lookup.rs
Normal file
75
src/wiki_lookup.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue