Update deps, implement RSS feed, rev 0.11.0

This commit is contained in:
Anthony J. Martinez 2021-07-11 11:38:58 +00:00
parent ddda9211fe
commit 96dee7eae1
Signed by: ajmartinez
GPG Key ID: A2206FDD769DBCFC
9 changed files with 136 additions and 46 deletions

21
Cargo.lock generated
View File

@ -22,15 +22,16 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.41"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15af2628f6890fe2609a3b91bef4c83450512802e59489f9c1cb1fa5df064a61"
checksum = "595d3cfa7a60d4555cb5067b99f07142a08ea778de5cf993f7b75c7d8fabc486"
[[package]]
name = "arse"
version = "0.11.0"
dependencies = [
"anyhow",
"chrono",
"clap",
"glob",
"hyper",
@ -455,9 +456,9 @@ checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026"
[[package]]
name = "hyper"
version = "0.14.9"
version = "0.14.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07d6baa1b441335f3ce5098ac421fb6547c46dda735ca1bc6d0153c838f9dd83"
checksum = "7728a72c4c7d72665fde02204bcbd93b247721025b222ef78606f14513e0fd03"
dependencies = [
"bytes",
"futures-channel",
@ -534,9 +535,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.97"
version = "0.2.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6"
checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790"
[[package]]
name = "lock_api"
@ -1092,9 +1093,9 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.8.0"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "570c2eb13b3ab38208130eccd41be92520388791207fde783bda7c1e8ace28d4"
checksum = "98c8b05dc14c75ea83d63dd391100353789f5f24b8b3866542a5e85c8be8e985"
dependencies = [
"autocfg",
"bytes",
@ -1112,9 +1113,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "1.2.0"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c49e3df43841dafb86046472506755d8501c5615673955f6aa17181125d13c37"
checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110"
dependencies = [
"proc-macro2",
"quote",

View File

@ -15,6 +15,7 @@ categories = ["command-line-utilities", "web-programming::http-server"]
[dependencies]
anyhow = "1.0"
chrono = "0.4"
clap = "2.33.3"
glob = "0.3.0"
hyper = "0.14"

View File

@ -7,6 +7,7 @@ and flexible base for serving sites using:
* [pulldown-cmark](https://crates.io/crates/pulldown-cmark) for CommonMark rendering
* [routerify](https://crates.io/crates/routerify) to serve the site
* [simplecss](https://simplecss.org) for default styling
* [rss](https://crates.io/crates/rss) for generating a full-site RSS feed
## Usage
@ -36,6 +37,7 @@ An example config, as generated, is shown below:
[site]
name = 'Example Site'
author = 'Arthur Writeson'
url = 'https://www.example.com'
template = 'default.tmpl'
topics = [
'one',
@ -76,7 +78,9 @@ The following elements are available within the Tera context for rendering:
* Items in `[docpaths]` are generated as full paths for completeness, however relative paths will work if desired
* From the example above the user is free to simply use `site/templates` and `site/webroot` and move the directory out of `/home/user`
* Note that `arse new` creates the site tree, and all other output files, in the current working directory.
* If `gallery` is one of the topics requested, a simple image slideshow will be generated for `/gallery/ext/*.jpg` with the same lexical reverse order as posts.
* If `gallery` is one of the topics requested
* A simple image slideshow will be generated for `/gallery/ext/*.jpg`
* Display will follow the same lexical reverse order as posts.
## Path to 1.0

View File

@ -131,6 +131,7 @@ fn csv_to_vec(csv: &str) -> Vec<String> {
pub(crate) struct Site {
pub name: String,
pub author: String,
pub url: String,
pub template: String,
pub topics: Vec<String>,
}
@ -140,10 +141,11 @@ impl Site {
pub(crate) fn new_from_input<R: BufRead>(reader: &mut R) -> Result<Site> {
let name = get_input("Please enter a name for the site: ", reader)?;
let author = get_input("Please enter the site author's name: ", reader)?;
let url = get_input("Please enter the base URL for your site: ", reader)?;
let topics = get_input("Please enter comma-separated site topics: ", reader)?;
let topics = csv_to_vec(&topics);
let template = "default.tmpl".to_owned();
let site = Site { name, author, template, topics };
let site = Site { name, author, url, template, topics };
trace!("Site: {:?}", site);
Ok(site)
@ -275,7 +277,7 @@ mod tests {
fn build_config_from_input() {
let dir = tempfile::tempdir().unwrap();
// Setup all target fields
let mut src: &[u8] = b"Site Name\nAuthor Name\nOne, Two, Three, And More\n";
let mut src: &[u8] = b"Site Name\nAuthor Name\nhttps://my.example.site\nOne, Two, Three, And More\n";
let config = AppConfig::generate(&dir, &mut src);
assert!(config.is_ok());

View File

@ -10,6 +10,7 @@ copied, modified, or distributed except according to those terms.
//! Provides the rendering engine for topics and posts using [`AppConfig`], [`Tera`], and [`pulldown_cmark`].
use std::fs::File;
use std::path::{Path, PathBuf};
use std::sync::Arc;
@ -17,9 +18,10 @@ use super::config::AppConfig;
use super::common;
use super::{Context, Result};
use chrono::{DateTime, Utc};
use log::{debug, trace};
use pulldown_cmark::{Parser, html};
use rss::{Channel, ChannelBuilder, Item, ItemBuilder};
use rss::{Channel, Item};
use tera::{Tera, Context as TemplateContext};
/// Static defaults for the rendering engine.
@ -150,7 +152,6 @@ impl Engine {
Self::read_post_to_html(post_path)
}
fn read_post_to_html<P: AsRef<Path>>(path: P) -> Result<String> {
debug!("Rendering Post Markdown to HTML");
trace!("Rendering {} to HTML", &path.as_ref().display());
@ -163,36 +164,59 @@ impl Engine {
Ok(html_output)
}
/// Generates an RSS feed of
/// Renders `/rss.xml` for all topics
pub(crate) fn rss(&self) -> Result<String> {
debug!("Rendering RSS Feed");
let site = &self.app.site;
let items = Self::rss_items(&self)?;
let channel = ChannelBuilder::default()
.title(&site.name)
.link("stuff") // TODO: add an item to config::Site to hold the domain name
.description(format!("{} RSS Feed", &site.name))
.items(items)
.build()
.unwrap();
let mut channel = Channel::default();
channel.set_title(&site.name);
channel.set_link(&site.url);
channel.set_description(format!("{} RSS Feed", &site.name));
channel.set_items(items);
Ok(channel.to_string())
}
fn rss_items(&self) -> Result<Vec<Item>> {
// TODO: Actually implement this in a meangingful way
/*
Need to loop across the topics and their contents and build Item
entities to push into a vector. Probably need to also get post
modification times.
Need to come up with some way to define the title/description.
*/
let item1 = Item::default();
debug!("Building RSS Items");
let mut items: Vec<Item> = Vec::new();
items.append(&mut Self::topic_to_item(&self, "main")?);
let item2 = Item::default();
for topic in &self.app.site.topics {
let mut topic_items = Self::topic_to_item(&self, &common::slugify(&topic))?;
items.append(&mut topic_items);
}
Ok(items)
}
fn topic_to_item(&self, topic_slug: &str) -> Result<Vec<Item>> {
trace!("Generating RSS Items for topic: {}", &topic_slug);
let mut items: Vec<Item> = Vec::new();
let topic_path = Path::new(&self.app.docpaths.webroot).join(topic_slug).join("posts");
let pat = format!("{}/*.md", topic_path.display());
let paths = common::path_matches(&pat)?;
for path in paths {
trace!("Generating RSS Item for post at: {}", path.display());
let link = format!("{}/{}/{}",
&self.app.site.url,
path.strip_prefix(&self.app.docpaths.webroot)?.parent().unwrap().to_str().unwrap(),
path.file_stem().unwrap().to_str().unwrap());
let f = File::open(&path)?;
let updated = f.metadata()?.modified()?;
let updated: DateTime<Utc> = updated.into();
let updated = updated.to_rfc2822();
let description = Self::read_post_to_html(path)?;
let mut item = Item::default();
item.set_link(link);
item.set_pub_date(updated);
item.set_description(description.to_owned());
items.push(item);
}
let items = vec![item1, item2];
Ok(items)
}
}
@ -206,7 +230,7 @@ mod tests {
#[test]
fn check_default_template() {
let dir = tempfile::tempdir().unwrap();
let mut src: &[u8] = b"Site Name\nAuthor Name\nOne, Two, Three, And More\nadmin\n";
let mut src: &[u8] = b"Site Name\nAuthor Name\nhttps://special.example.site\nOne, Gallery\nadmin\n";
let config = AppConfig::generate(&dir, &mut src).unwrap();
let config = Arc::new(config);
let tera = Engine::load_template(config);
@ -216,7 +240,7 @@ mod tests {
#[test]
fn check_render_post() {
let dir = tempfile::tempdir().unwrap();
let mut src: &[u8] = b"Site Name\nAuthor Name\nOne, Two, Three, And More\nadmin\n";
let mut src: &[u8] = b"Site Name\nAuthor Name\nhttps://special.example.site\nOne, Gallery\nadmin\n";
let config = AppConfig::generate(&dir, &mut src).unwrap();
let config = Arc::new(config);
let engine = Engine::new(config);
@ -248,7 +272,7 @@ Super Wow!
#[test]
fn check_render_topic() {
let dir = tempfile::tempdir().unwrap();
let mut src: &[u8] = b"Site Name\nAuthor Name\nOne, Two, Three, And More\nadmin\n";
let mut src: &[u8] = b"Site Name\nAuthor Name\nhttps://special.example.site\nOne, Gallery\nadmin\n";
let config = AppConfig::generate(&dir, &mut src).unwrap();
let config = Arc::new(config);
let engine = Engine::new(config);
@ -279,7 +303,7 @@ Super Wow!
#[test]
fn check_render_empty_topic() {
let dir = tempfile::tempdir().unwrap();
let mut src: &[u8] = b"Site Name\nAuthor Name\nOne, Two, Three, And More\nadmin\n";
let mut src: &[u8] = b"Site Name\nAuthor Name\nhttps://special.example.site\nOne, Gallery\nadmin\n";
let config = AppConfig::generate(&dir, &mut src).unwrap();
let config = Arc::new(config);
let engine = Engine::new(config);
@ -292,7 +316,7 @@ Super Wow!
#[test]
fn check_render_gallery_topic() {
let dir = tempfile::tempdir().unwrap();
let mut src: &[u8] = b"Site Name\nAuthor Name\nOne, Gallery\nadmin\n";
let mut src: &[u8] = b"Site Name\nAuthor Name\nhttps://special.example.site\nOne, Gallery\nadmin\n";
let config = AppConfig::generate(&dir, &mut src).unwrap();
let config = Arc::new(config);
let engine = Engine::new(config);
@ -313,7 +337,7 @@ Super Wow!
#[test]
fn check_render_empty_gallery() {
let dir = tempfile::tempdir().unwrap();
let mut src: &[u8] = b"Site Name\nAuthor Name\nOne, Gallery\nadmin\n";
let mut src: &[u8] = b"Site Name\nAuthor Name\nhttps://special.example.site\nOne, Gallery\nadmin\n";
let config = AppConfig::generate(&dir, &mut src).unwrap();
let config = Arc::new(config);
let engine = Engine::new(config);
@ -322,4 +346,44 @@ Super Wow!
assert!(page.contains("Coming Soon"));
}
#[test]
fn check_render_rss() {
let dir = tempfile::tempdir().unwrap();
let mut src: &[u8] = b"Site Name\nAuthor Name\nhttps://special.example.site\nOne, Gallery\nadmin\n";
let config = AppConfig::generate(&dir, &mut src).unwrap();
let config = Arc::new(config);
let engine = Engine::new(config);
let main_post = r#"
### The Main Page
This is just a main topic page.
"#;
let one_post1 = r#"
### A first post in One
Super Wow!
"#;
let one_post2 = r#"
### [A second post in One](/one/posts/2)
Super Wow TWICE!
"#;
let mut f = File::create(&dir.path().join("site/webroot/main/posts/index.md")).unwrap();
f.write_all(&main_post.as_bytes()).unwrap();
let mut f = File::create(&dir.path().join("site/webroot/one/posts/1.md")).unwrap();
f.write_all(&one_post1.as_bytes()).unwrap();
let mut f = File::create(&dir.path().join("site/webroot/one/posts/2.md")).unwrap();
f.write_all(&one_post2.as_bytes()).unwrap();
let rss = engine.rss().unwrap();
assert!(rss.contains("The Main Page"));
assert!(rss.contains("Super Wow!"));
assert!(rss.contains("A second post in One"));
}
}

View File

@ -27,6 +27,7 @@ pub(crate) const TEMPLATE: &str = r#"
{%- for topic in site.topics %}
<a href="/{{ topic | slugify }}">{{ topic }}</a>
{%- endfor -%}
<a href="/rss.xml">RSS</a>
</nav>
</center>
</header>

View File

@ -30,7 +30,7 @@ pub(crate) fn router(engine: Arc<Engine>) -> Router<Body, Error> {
.data(engine)
.get("/static/:fname", static_assets)
.get("/favicon.ico", favicon)
.get("/rss", rss_handler)
.get("/rss.xml", rss_handler)
.get("/:topic", topic_handler)
.get("/:topic/ext/:fname", topic_assets)
.get("/:topic/posts/:post", post_handler)
@ -60,10 +60,9 @@ async fn index_handler(req: Request<Body>) -> Result<Response<Body>> {
/// Handler for "/rss"
async fn rss_handler(req: Request<Body>) -> Result<Response<Body>> {
let engine = req.data::<Arc<Engine>>().unwrap();
info!("Handling request to '/rss'");
// TODO: call a rss function on Engine
Ok(Response::new(Body::from("TEMPORARY")))
info!("Handling request to '/rss.xml'");
let rss = engine.rss()?;
Ok(Response::new(Body::from(rss)))
}
/// Handler for "/:topic"
@ -147,7 +146,7 @@ mod tests {
#[tokio::test]
async fn check_all_handlers() {
let dir = tempfile::tempdir().unwrap();
let mut src: &[u8] = b"Site Name\nAuthor Name\nOne, Two, Three, And More\nadmin\n";
let mut src: &[u8] = b"Site Name\nAuthor Name\nhttps://some.special.site\nOne, Two, Three, And More\nadmin\n";
let app = AppConfig::generate(&dir, &mut src).unwrap();
let engine = Engine::new(Arc::new(app));
let engine = Arc::new(engine);
@ -239,6 +238,12 @@ One Important Test
.body(Body::default())
.unwrap();
let rss_request = Request::builder()
.method("GET")
.uri("http://localhost:9090/rss.xml")
.body(Body::default())
.unwrap();
let service = RouterService::new(router).unwrap();
let addr = format!("{}:{}", engine.app.server.bind, engine.app.server.port);
let addr: SocketAddr = addr.parse().unwrap();
@ -266,12 +271,14 @@ One Important Test
let topic_asset_resp = client.request(topic_asset_request).await.unwrap();
let static_asset_resp = client.request(static_asset_request).await.unwrap();
let favicon_resp = client.request(favicon_request).await.unwrap();
let rss_resp = client.request(rss_request).await.unwrap();
assert_eq!(index_resp.status(), StatusCode::OK);
assert_eq!(post_resp.status(), StatusCode::OK);
assert_eq!(topic_resp.status(), StatusCode::OK);
assert_eq!(topic_asset_resp.status(), StatusCode::OK);
assert_eq!(static_asset_resp.status(), StatusCode::OK);
assert_eq!(favicon_resp.status(), StatusCode::OK);
assert_eq!(rss_resp.status(), StatusCode::OK);
let bad_topic_resp = client.request(bad_topic_request).await.unwrap();
let bad_post_resp = client.request(bad_post_request).await.unwrap();
@ -315,6 +322,12 @@ One Important Test
.body(Body::default())
.unwrap();
let rss_request = Request::builder()
.method("GET")
.uri("http://localhost:8901/rss.xml")
.body(Body::default())
.unwrap();
let service = RouterService::new(router).unwrap();
let addr = format!("{}:{}", engine.app.server.bind, engine.app.server.port);
let addr: SocketAddr = addr.parse().unwrap();
@ -340,10 +353,12 @@ One Important Test
let post_resp = client.request(post_request).await.unwrap();
let topic_resp = client.request(topic_request).await.unwrap();
let gallery_resp = client.request(gallery_request).await.unwrap();
let rss_resp = client.request(rss_request).await.unwrap();
assert_eq!(index_resp.status(), StatusCode::OK);
assert_eq!(post_resp.status(), StatusCode::OK);
assert_eq!(topic_resp.status(), StatusCode::OK);
assert_eq!(gallery_resp.status(), StatusCode::OK);
assert_eq!(rss_resp.status(), StatusCode::OK);
let _ = tx.send(());
}

View File

@ -61,6 +61,7 @@ function change_img(dir) {
</main>
<footer>
<p>&#169; {{ site.author }}</p>
<p><a href="/rss.xml">RSS</a></p>
</footer>
</body>
</html>

View File

@ -1,6 +1,7 @@
[site]
name = "My Awesome Blog!"
author = "Neo"
url = "https://something.cool.here"
template = "special.tmpl"
topics = ["one", "two", "three", "gallery"]