Skip to content

Commit

Permalink
factoring out extra news service
Browse files Browse the repository at this point in the history
  • Loading branch information
tanneberger committed Aug 20, 2024
1 parent bcaf883 commit 36845f7
Show file tree
Hide file tree
Showing 5 changed files with 376 additions and 4 deletions.
4 changes: 4 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use crate::bird::Bird;
use crate::blog::Blog;
use crate::documents::Documents;
use crate::event::EventHandler;
use crate::news::News;
use crate::peers::NetworkService;
use crate::routes::{route, ContentPaths};
use crate::state::FoundationState;
Expand All @@ -25,6 +26,7 @@ mod cache;
mod documents;
mod event;
mod lang;
mod news;
mod peers;
mod routes;
mod state;
Expand Down Expand Up @@ -53,6 +55,7 @@ async fn main() -> anyhow::Result<()> {

let state = FoundationState {
blog: Blog::load(&args.content_directory.join("blog")).await?,
news: News::load(&args.content_directory.join("news")).await?,
text_blocks: TextBlocks::load(&args.content_directory.join("text_blocks"), &args.base_url)
.await?,
documents: Documents::load(&args.content_directory.join("documents")).await?,
Expand All @@ -75,6 +78,7 @@ async fn main() -> anyhow::Result<()> {
let router = route(&ContentPaths {
blog: args.content_directory.join("blog/assets"),
event: args.content_directory.join("event/assets"),
news: args.content_directory.join("news/assets"),
text_blocks: args.content_directory.join("text_blocks/assets"),
document: args.content_directory.join("documents/download"),
team: args.content_directory.join("team/assets"),
Expand Down
296 changes: 296 additions & 0 deletions src/news.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
use std::collections::HashSet;
use std::path::Path;
use std::sync::Arc;

use anyhow::anyhow;
use rst_parser::parse;
use rst_renderer::render_html;
use serde::de::Error;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use time::Date;

use crate::lang::Language;

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
struct MyDate(Date);

#[derive(Debug, Clone)]
pub(crate) struct News {
posts: Arc<Vec<Arc<Post>>>,
small_posts: Arc<Vec<Arc<SmallPost>>>,
}

#[derive(Deserialize)]
pub(crate) struct WrittenNewsMeta {
title: String,
published: MyDate,
modified: Option<MyDate>,
description: String,
keywords: Vec<String>,
authors: Vec<String>,
image: Option<String>,
}

#[derive(Serialize, Debug, Clone)]
pub(crate) struct Post {
slug: String,
lang: Language,
idx: u32,
title: String,
published: MyDate,
modified: Option<MyDate>,
description: String,
keywords: Vec<String>,
authors: Vec<String>,
image: Option<String>,
body: String,
}

#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
pub(crate) struct SmallPost {
slug: String,
lang: Language,
idx: u32,
title: String,
published: MyDate,
modified: Option<MyDate>,
description: String,
keywords: Vec<String>,
authors: Vec<String>,
image: Option<String>,
}

impl News {
pub(crate) async fn load(directory: &Path) -> anyhow::Result<Self> {
let mut posts = Vec::new();

let mut dir = tokio::fs::read_dir(directory).await?;
while let Some(entry) = dir.next_entry().await? {
if entry.file_type().await?.is_dir() {
continue;
}

let path = entry.path();
let content = tokio::fs::read_to_string(path.as_path()).await?;
let content = content.trim_start();
let content = content.strip_prefix("---").unwrap();
let (meta, text) = content.split_once("---").unwrap();

let meta: WrittenNewsMeta = serde_yaml::from_str(meta)?;
let file_name = path.file_name().unwrap().to_str().unwrap();

if file_name.starts_with('_') {
continue;
}

let is_rst_file = file_name.ends_with(".rst");

let (idx, lang, slug) = parse_file_name(file_name)?;

let body = if is_rst_file {
let mut buffer: Vec<u8> = Vec::new();
let parsed_rst = parse(text)
.map_err(|e| {
eprintln!("cannot parse rst file {} with error {}", &file_name, e);
})
.unwrap_or_default();
render_html(&parsed_rst, &mut buffer, true)
.map_err(|e| {
eprintln!(
"cannot render rst file to html {} with error {}",
&file_name, e
);
})
.unwrap_or_default();
String::from_utf8(buffer)?
} else {
markdown::to_html(text)
};

posts.push(Arc::new(Post {
slug: slug.to_string(),
lang,
idx,
title: meta.title,
published: meta.published,
modified: meta.modified,
description: meta.description,
keywords: meta.keywords,
authors: meta.authors,
image: meta.image,
body,
}));
}

posts.sort_by(|a, b| b.idx.cmp(&a.idx));

let small_posts = posts
.iter()
.map(|post| {
Arc::new(SmallPost {
slug: post.slug.clone(),
lang: post.lang,
idx: post.idx,
title: post.title.clone(),
published: post.published,
modified: post.modified,
description: post.description.clone(),
keywords: post.keywords.clone(),
authors: post.authors.clone(),
image: post.image.clone(),
})
})
.collect();

Ok(News {
posts: Arc::new(posts),
small_posts: Arc::new(small_posts),
})
}

pub(crate) fn posts(&self, lang: Language) -> Vec<Arc<SmallPost>> {
self
.small_posts
.iter()
.filter(|post| post.lang == lang)
.cloned()
.collect()
}

pub(crate) fn find_post(&self, lang: Language, slug: &str) -> Option<Arc<Post>> {
self
.posts
.iter()
.find(|post| post.lang == lang && post.slug == slug)
.cloned()
}

pub(crate) fn search_by_keywords(
&self,
lang: Language,
keywords: &Vec<String>,
) -> Vec<Arc<SmallPost>> {
let posts = self
.small_posts
.iter()
.filter(|post| post.lang == lang)
.collect::<Vec<_>>();

let keywords_set = keywords.iter().collect::<HashSet<_>>();

let mut or = posts
.iter()
.filter(|post| {
post
.keywords
.iter()
.collect::<HashSet<_>>()
.intersection(&keywords_set)
.next()
.is_some()
})
.cloned()
.cloned()
.collect::<Vec<_>>();

let mut and = posts
.iter()
.filter(|post| {
!or.contains(post)
&& post
.keywords
.iter()
.collect::<HashSet<_>>()
.intersection(&keywords_set)
.count()
== keywords.len()
})
.cloned()
.cloned()
.collect::<Vec<_>>();

or.append(&mut and);

or
}

pub(crate) fn keywords(&self) -> HashSet<String> {
self
.small_posts
.iter()
.flat_map(|post| post.keywords.clone())
.collect()
}
}

pub(crate) fn parse_file_name(file_name: &str) -> anyhow::Result<(u32, Language, &str)> {
let mut split = file_name.split('.');

let idx = split
.next()
.ok_or_else(|| anyhow!("Index missing in file name {}", file_name))?
.parse()?;
let slug = split
.next()
.ok_or_else(|| anyhow!("Slug missing in file name {}", file_name))?;
let lang = split
.next()
.ok_or_else(|| anyhow!("Language missing in file name {}", file_name))?
.try_into()?;

Ok((idx, lang, slug))
}

impl Serialize for MyDate {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let s = format!(
"{:0>4}-{:0>2}-{:0>2}",
self.0.year(),
self.0.month() as u8,
self.0.day()
);

serializer.serialize_str(&s)
}
}

impl<'de> Deserialize<'de> for MyDate {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let mut split = s.split('-');

let year = split
.next()
.ok_or_else(|| Error::custom(format!("Invalid date format {}", s)))?
.parse()
.map_err(|e| Error::custom(format!("{}", e)))?;

let month: u8 = split
.next()
.ok_or_else(|| Error::custom(format!("Invalid date format {}", s)))?
.parse()
.map_err(|e| Error::custom(format!("{}", e)))?;

let day = split
.next()
.ok_or_else(|| Error::custom(format!("Invalid date format {}", s)))?
.parse()
.map_err(|e| Error::custom(format!("{}", e)))?;

Date::from_calendar_date(
year,
month
.try_into()
.map_err(|e| Error::custom(format!("{}", e)))?,
day,
)
.map_err(|e| Error::custom(format!("{}", e)))
.map(MyDate)
}
}
20 changes: 16 additions & 4 deletions src/routes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@ use axum::routing::get;
use axum::Router;
use tower_http::services::ServeDir;

use crate::routes::blog::{find_keywords, find_post, list_posts};
use crate::routes::blog::{
find_keywords as blog_find_keywords, find_post as blog_find_post, list_posts as blog_list_posts,
};
use crate::routes::documents::list_documents;
use crate::routes::event::{find_event, list_all_events, list_future_events};
use crate::routes::news::{
find_keywords as news_find_keywords, find_post as news_find_post, list_posts as news_list_posts,
};
use crate::routes::peers::get_peers_and_supporter;
use crate::routes::team::get_team;
use crate::routes::text_blocks::find_text_block;
Expand All @@ -24,8 +29,11 @@ mod stats;
mod team;
mod text_blocks;

mod news;

pub(crate) struct ContentPaths {
pub(crate) blog: PathBuf,
pub(crate) news: PathBuf,
pub(crate) event: PathBuf,
pub(crate) text_blocks: PathBuf,
pub(crate) document: PathBuf,
Expand All @@ -34,9 +42,12 @@ pub(crate) struct ContentPaths {

pub(crate) fn route(content_paths: &ContentPaths) -> Router<FoundationState> {
Router::new()
.route("/blog/:lang", get(list_posts))
.route("/blog/:lang/:slug", get(find_post))
.route("/blog/keywords", get(find_keywords))
.route("/blog/:lang", get(blog_list_posts))
.route("/blog/:lang/:slug", get(blog_find_post))
.route("/blog/keywords", get(blog_find_keywords))
.route("/news/:lang", get(news_list_posts))
.route("/news/:lang/:slug", get(news_find_post))
.route("/news/keywords", get(news_find_keywords))
.route("/event/:lang/all", get(list_all_events))
.route("/event/:lang/upcoming", get(list_future_events))
.route("/event/:lang/:slug", get(find_event))
Expand All @@ -46,6 +57,7 @@ pub(crate) fn route(content_paths: &ContentPaths) -> Router<FoundationState> {
ServeDir::new(&content_paths.text_blocks),
)
.nest_service("/blog/assets", ServeDir::new(&content_paths.blog))
.nest_service("/news/assets", ServeDir::new(&content_paths.news))
.nest_service("/event/assets", ServeDir::new(&content_paths.event))
.route("/documents/:lang", get(list_documents))
.nest_service(
Expand Down
Loading

0 comments on commit 36845f7

Please sign in to comment.