From 900b6d78589877eb062cec64103a9c38988e54e1 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Tue, 14 Jul 2020 13:27:52 -0400 Subject: [PATCH] jsonfeed support --- Cargo.lock | 101 +++++++++----------------- Cargo.toml | 3 + lib/jsonfeed/Cargo.toml | 2 +- lib/jsonfeed/src/builder.rs | 81 ++++++++++++++++++++- src/app.rs | 23 ++++++ src/handlers/feeds.rs | 21 ++++++ src/handlers/mod.rs | 1 + src/main.rs | 141 ++++++++++++++++-------------------- src/post/mod.rs | 31 ++++++++ 9 files changed, 257 insertions(+), 147 deletions(-) create mode 100644 src/handlers/feeds.rs diff --git a/Cargo.lock b/Cargo.lock index cc00ce0..6a7a6cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -203,12 +203,9 @@ checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" [[package]] name = "bytes" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "118cf036fbb97d0816e3c34b2d7a1e8cfc60f68fcf63d550ddbe9bd5f59c213b" -dependencies = [ - "loom", -] +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" [[package]] name = "cc" @@ -404,11 +401,12 @@ dependencies = [ [[package]] name = "error-chain" -version = "0.10.0" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9435d864e017c3c6afeac1654189b06cdb491cf2ff73dbf0d73b0f292f42ff8" +checksum = "d371106cc88ffdfb1eabd7111e432da544f16f3e2d7bf1dfe8bf575f1df045cd" dependencies = [ "backtrace", + "version_check 0.9.2", ] [[package]] @@ -555,19 +553,6 @@ dependencies = [ "slab", ] -[[package]] -name = "generator" -version = "0.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "add72f17bb81521258fcc8a7a3245b1e184e916bfbe34f0ea89558f440df5c68" -dependencies = [ - "cc", - "libc", - "log 0.4.8", - "rustc_version", - "winapi 0.3.9", -] - [[package]] name = "generic-array" version = "0.12.3" @@ -602,9 +587,9 @@ checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] name = "h2" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79b7246d7e4b979c03fa093da39cfb3617a96bbeee6310af63991668d7e843ff" +checksum = "993f9e0baeed60001cf565546b0d3dbe6a6ad23f2bd31644a133c641eccf6d53" dependencies = [ "bytes", "fnv", @@ -613,10 +598,10 @@ dependencies = [ "futures-util", "http", "indexmap", - "log 0.4.8", "slab", "tokio", "tokio-util", + "tracing", ] [[package]] @@ -703,9 +688,9 @@ dependencies = [ [[package]] name = "hyper" -version = "0.13.6" +version = "0.13.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6e7655b9594024ad0ee439f3b5a7299369dc2a3f459b47c696f9ff676f9aa1f" +checksum = "3e68a8dd9716185d9e64ea473ea6ef63529252e3e27623295a0378a19665d5eb" dependencies = [ "bytes", "futures-channel", @@ -716,12 +701,12 @@ dependencies = [ "http-body", "httparse", "itoa", - "log 0.4.8", "pin-project", "socket2", "time", "tokio", "tower-service", + "tracing", "want", ] @@ -887,17 +872,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "loom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ecc775857611e1df29abba5c41355cdf540e7e9d4acfdf0f355eefee82330b7" -dependencies = [ - "cfg-if", - "generator", - "scoped-tls 0.1.2", -] - [[package]] name = "maplit" version = "1.0.2" @@ -1632,15 +1606,6 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" -[[package]] -name = "rustc_version" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" -dependencies = [ - "semver", -] - [[package]] name = "ryu" version = "1.0.5" @@ -1672,12 +1637,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "scoped-tls" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "332ffa32bf586782a3efaeb58f127980944bbc8c4d6913a86107ac2a5ab24b28" - [[package]] name = "scoped-tls" version = "1.0.0" @@ -1707,21 +1666,6 @@ dependencies = [ "libc", ] -[[package]] -name = "semver" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -dependencies = [ - "semver-parser", -] - -[[package]] -name = "semver-parser" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" - [[package]] name = "serde" version = "1.0.114" @@ -2034,6 +1978,26 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" +[[package]] +name = "tracing" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e2a2de6b0d5cbb13fc21193a2296888eaab62b6044479aafb3c54c01c29fcd" +dependencies = [ + "cfg-if", + "log 0.4.8", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94ae75f0d28ae10786f3b1895c55fe72e79928fd5ccdebb5438c75e93fec178f" +dependencies = [ + "lazy_static", +] + [[package]] name = "try-lock" version = "0.2.3" @@ -2246,7 +2210,7 @@ dependencies = [ "mime_guess 2.0.3", "multipart", "pin-project", - "scoped-tls 1.0.0", + "scoped-tls", "serde", "serde_json", "serde_urlencoded", @@ -2412,6 +2376,7 @@ dependencies = [ "envy", "glob", "hyper", + "jsonfeed", "lazy_static", "log 0.4.8", "mime 0.3.16", diff --git a/Cargo.toml b/Cargo.toml index d279f14..b2065b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,9 @@ thiserror = "1" tokio = { version = "0.2", features = ["macros"] } warp = "0.2" +# workspace dependencies +jsonfeed = { path = "./lib/jsonfeed" } + [build-dependencies] ructe = { version = "0.11", features = ["warp02"] } diff --git a/lib/jsonfeed/Cargo.toml b/lib/jsonfeed/Cargo.toml index 31a446a..affc5a6 100644 --- a/lib/jsonfeed/Cargo.toml +++ b/lib/jsonfeed/Cargo.toml @@ -9,7 +9,7 @@ readme = "README.adoc" version = "0.2.0" [dependencies] -error-chain = "0.10.0" +error-chain = "0.12" serde = "1" serde_derive = "1" serde_json = "1" diff --git a/lib/jsonfeed/src/builder.rs b/lib/jsonfeed/src/builder.rs index 161b2d1..5194b11 100644 --- a/lib/jsonfeed/src/builder.rs +++ b/lib/jsonfeed/src/builder.rs @@ -20,6 +20,51 @@ impl Builder { self } + pub fn home_page_url>(mut self, url: I) -> Builder { + self.0.home_page_url = Some(url.into()); + self + } + + pub fn feed_url>(mut self, url: I) -> Builder { + self.0.feed_url = Some(url.into()); + self + } + + pub fn description>(mut self, desc: I) -> Builder { + self.0.description = Some(desc.into()); + self + } + + pub fn user_comment>(mut self, cmt: I) -> Builder { + self.0.user_comment = Some(cmt.into()); + self + } + + pub fn next_url>(mut self, url: I) -> Builder { + self.0.next_url = Some(url.into()); + self + } + + pub fn icon>(mut self, url: I) -> Builder { + self.0.icon = Some(url.into()); + self + } + + pub fn favicon>(mut self, url: I) -> Builder { + self.0.favicon = Some(url.into()); + self + } + + pub fn author(mut self, author: Author) -> Builder { + self.0.author = Some(author); + self + } + + pub fn expired(mut self) -> Builder { + self.0.expired = Some(true); + self + } + pub fn item(mut self, item: Item) -> Builder { self.0.items.push(item); self @@ -30,7 +75,6 @@ impl Builder { } } - /// Builder object for an item in a feed pub struct ItemBuilder { pub id: Option, @@ -72,6 +116,41 @@ impl ItemBuilder { self } + pub fn image>(mut self, i: I) -> ItemBuilder { + self.image = Some(i.into()); + self + } + + pub fn id>(mut self, i: I) -> ItemBuilder { + self.id = Some(i.into()); + self + } + + pub fn url>(mut self, i: I) -> ItemBuilder { + self.url = Some(i.into()); + self + } + + pub fn external_url>(mut self, i: I) -> ItemBuilder { + self.external_url = Some(i.into()); + self + } + + pub fn date_modified>(mut self, i: I) -> ItemBuilder { + self.date_modified = Some(i.into()); + self + } + + pub fn date_published>(mut self, i: I) -> ItemBuilder { + self.date_published = Some(i.into()); + self + } + + pub fn tags(mut self, tags: Vec) -> ItemBuilder { + self.tags = Some(tags); + self + } + pub fn content_html>(mut self, i: I) -> ItemBuilder { match self.content { Some(Content::Text(t)) => { diff --git a/src/app.rs b/src/app.rs index 8d8c0c6..51a7f85 100644 --- a/src/app.rs +++ b/src/app.rs @@ -29,6 +29,8 @@ pub fn markdown(inp: &str) -> String { markdown_to_html(inp, &options) } +pub const ICON: &'static str = "https://christine.website/static/img/avatar.png"; + pub struct State { pub cfg: Config, pub signalboost: Vec, @@ -37,6 +39,7 @@ pub struct State { pub gallery: Vec, pub talks: Vec, pub everything: Vec, + pub jf: jsonfeed::Feed, } pub fn init(cfg: PathBuf) -> Result { @@ -61,6 +64,25 @@ pub fn init(cfg: PathBuf) -> Result { everything.sort(); everything.reverse(); + let mut jfb = jsonfeed::Feed::builder() + .title("Christine Dodrill's Blog") + .description("My blog posts and rants about various technology things.") + .author( + jsonfeed::Author::new() + .name("Christine Dodrill") + .url("https://christine.website") + .avatar(ICON), + ) + .feed_url("https://christine.website/blog.json") + .user_comment("This is a JSON feed of my blogposts. For more information read: https://jsonfeed.org/version/1") + .icon(ICON) + .favicon(ICON); + + for post in &everything { + let post = post.clone(); + jfb = jfb.item(post.into()); + } + Ok(State { cfg: cfg, signalboost: sb, @@ -69,6 +91,7 @@ pub fn init(cfg: PathBuf) -> Result { gallery: gallery, talks: talks, everything: everything, + jf: jfb.build(), }) } diff --git a/src/handlers/feeds.rs b/src/handlers/feeds.rs new file mode 100644 index 0000000..a01f957 --- /dev/null +++ b/src/handlers/feeds.rs @@ -0,0 +1,21 @@ +use crate::{ + app::State, +}; +use lazy_static::lazy_static; +use prometheus::{IntCounterVec, register_int_counter_vec, opts}; +use std::{sync::Arc}; +use warp::{ + Reply, +}; + +lazy_static! { + static ref HIT_COUNTER: IntCounterVec = + register_int_counter_vec!(opts!("feed_hits", "Number of hits to various feeds"), &["kind"]) + .unwrap(); +} + +pub fn jsonfeed(state: Arc) -> impl Reply { + HIT_COUNTER.with_label_values(&["json"]).inc(); + let state = state.clone(); + warp::reply::json(&state.jf) +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 6c9c054..fc0233b 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -49,6 +49,7 @@ pub async fn not_found() -> Result { } pub mod blog; +pub mod feeds; pub mod gallery; pub mod talks; diff --git a/src/main.rs b/src/main.rs index 307572c..d783906 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,84 +33,69 @@ async fn main() -> Result<()> { let healthcheck = warp::get().and(warp::path(".within").and(warp::path("health")).map(|| "OK")); - let blog = { - let base = warp::path!("blog" / ..); - let index = base - .and(warp::path::end()) + let base = warp::path!("blog" / ..); + let blog_index = base + .and(warp::path::end()) + .and(with_state(state.clone())) + .and_then(handlers::blog::index); + let series = base + .and(warp::path!("series").and(with_state(state.clone()).and_then(handlers::blog::series))); + let series_view = base.and( + warp::path!("series" / String) .and(with_state(state.clone())) - .and_then(handlers::blog::index); - let series = base.and( - warp::path!("series").and(with_state(state.clone()).and_then(handlers::blog::series)), - ); - let series_view = base.and( - warp::path!("series" / String) - .and(with_state(state.clone())) - .and(warp::get()) - .and_then(handlers::blog::series_view), - ); - let post_view = base.and( - warp::path!(String) - .and(with_state(state.clone())) - .and(warp::get()) - .and_then(handlers::blog::post_view), - ); - - index.or(series.or(series_view)).or(post_view) - }; - - let gallery = { - let base = warp::path!("gallery" / ..); - let index = base - .and(warp::path::end()) + .and(warp::get()) + .and_then(handlers::blog::series_view), + ); + let post_view = base.and( + warp::path!(String) .and(with_state(state.clone())) - .and_then(handlers::gallery::index); - let post_view = base.and( - warp::path!(String) - .and(with_state(state.clone())) - .and(warp::get()) - .and_then(handlers::gallery::post_view), - ); + .and(warp::get()) + .and_then(handlers::blog::post_view), + ); - index.or(post_view) - }; - - let talks = { - let base = warp::path!("talks" / ..); - let index = base - .and(warp::path::end()) + let gallery_base = warp::path!("gallery" / ..); + let gallery_index = gallery_base + .and(warp::path::end()) + .and(with_state(state.clone())) + .and_then(handlers::gallery::index); + let gallery_post_view = gallery_base.and( + warp::path!(String) .and(with_state(state.clone())) - .and_then(handlers::talks::index); - let post_view = base.and( - warp::path!(String) - .and(with_state(state.clone())) - .and(warp::get()) - .and_then(handlers::talks::post_view), - ); + .and(warp::get()) + .and_then(handlers::gallery::post_view), + ); - index.or(post_view) - }; - - let static_pages = { - let contact = warp::path!("contact").and_then(handlers::contact); - let feeds = warp::path!("feeds").and_then(handlers::feeds); - let resume = warp::path!("resume") + let talk_base = warp::path!("talks" / ..); + let talk_index = talk_base + .and(warp::path::end()) + .and(with_state(state.clone())) + .and_then(handlers::talks::index); + let talk_post_view = talk_base.and( + warp::path!(String) .and(with_state(state.clone())) - .and_then(handlers::resume); - let signalboost = warp::path!("signalboost") - .and(with_state(state.clone())) - .and_then(handlers::signalboost); + .and(warp::get()) + .and_then(handlers::talks::post_view), + ); - contact.or(feeds).or(resume).or(signalboost) - }; + let index = warp::get().and(path::end().and_then(handlers::index)); - let files = { - let files = warp::path("static").and(warp::fs::dir("./static")); - let css = warp::path("css").and(warp::fs::dir("./css")); - let sw = warp::path("sw.js").and(warp::fs::file("./static/js/sw.js")); - let robots = warp::path("robots.txt").and(warp::fs::file("./static/robots.txt")); + let contact = warp::path!("contact").and_then(handlers::contact); + let feeds = warp::path!("feeds").and_then(handlers::feeds); + let resume = warp::path!("resume") + .and(with_state(state.clone())) + .and_then(handlers::resume); + let signalboost = warp::path!("signalboost") + .and(with_state(state.clone())) + .and_then(handlers::signalboost); - files.or(css).or(sw).or(robots) - }; + let files = warp::path("static").and(warp::fs::dir("./static")); + let css = warp::path("css").and(warp::fs::dir("./css")); + let sw = warp::path("sw.js").and(warp::fs::file("./static/js/sw.js")); + let robots = warp::path("robots.txt").and(warp::fs::file("./static/robots.txt")); + + let jsonfeed = warp::path("blog.json") + .and(with_state(state.clone())) + .map(handlers::feeds::jsonfeed); let metrics_endpoint = warp::path("metrics").and(warp::path::end()).map(move || { let encoder = TextEncoder::new(); @@ -124,14 +109,16 @@ async fn main() -> Result<()> { .unwrap() }); - let site = files - .or(warp::get().and(path::end().and_then(handlers::index))) - .or(static_pages) - .or(blog) - .or(gallery) - .or(talks) - .or(healthcheck) - .or(metrics_endpoint) + let site = index + .or(contact.or(feeds)) + .or(resume.or(signalboost)) + .or(blog_index.or(series.or(series_view).or(post_view))) + .or(gallery_index.or(gallery_post_view)) + .or(talk_index.or(talk_post_view)) + .or(jsonfeed) + .or(files.or(css)) + .or(sw.or(robots)) + .or(healthcheck.or(metrics_endpoint)) .map(|reply| { warp::reply::with_header( reply, diff --git a/src/post/mod.rs b/src/post/mod.rs index 1589aaa..c1c743f 100644 --- a/src/post/mod.rs +++ b/src/post/mod.rs @@ -14,6 +14,37 @@ pub struct Post { pub date: NaiveDate, } +impl Into for Post { + fn into(self) -> jsonfeed::Item { + let mut result = jsonfeed::Item::builder() + .title(self.front_matter.title) + .content_html(self.body_html) + .id(format!("https://christine.website/{}", self.link)) + .url(format!("https://christine.website/{}", self.link)) + .date_published(self.front_matter.date); + + let mut tags: Vec = vec![]; + + if let Some(series) = self.front_matter.series { + tags.push(series); + } + + if let Some(mut meta_tags) = self.front_matter.tags { + tags.append(&mut meta_tags); + } + + if tags.len() != 0 { + result = result.tags(tags); + } + + if let Some(image_url) = self.front_matter.image { + result = result.image(image_url); + } + + result.build().unwrap() + } +} + impl Ord for Post { fn cmp(&self, other: &Self) -> Ordering { self.partial_cmp(&other).unwrap()