Skip to content

Commit 36845f7

Browse files
committed
factoring out extra news service
1 parent bcaf883 commit 36845f7

File tree

5 files changed

+376
-4
lines changed

5 files changed

+376
-4
lines changed

src/main.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use crate::bird::Bird;
1111
use crate::blog::Blog;
1212
use crate::documents::Documents;
1313
use crate::event::EventHandler;
14+
use crate::news::News;
1415
use crate::peers::NetworkService;
1516
use crate::routes::{route, ContentPaths};
1617
use crate::state::FoundationState;
@@ -25,6 +26,7 @@ mod cache;
2526
mod documents;
2627
mod event;
2728
mod lang;
29+
mod news;
2830
mod peers;
2931
mod routes;
3032
mod state;
@@ -53,6 +55,7 @@ async fn main() -> anyhow::Result<()> {
5355

5456
let state = FoundationState {
5557
blog: Blog::load(&args.content_directory.join("blog")).await?,
58+
news: News::load(&args.content_directory.join("news")).await?,
5659
text_blocks: TextBlocks::load(&args.content_directory.join("text_blocks"), &args.base_url)
5760
.await?,
5861
documents: Documents::load(&args.content_directory.join("documents")).await?,
@@ -75,6 +78,7 @@ async fn main() -> anyhow::Result<()> {
7578
let router = route(&ContentPaths {
7679
blog: args.content_directory.join("blog/assets"),
7780
event: args.content_directory.join("event/assets"),
81+
news: args.content_directory.join("news/assets"),
7882
text_blocks: args.content_directory.join("text_blocks/assets"),
7983
document: args.content_directory.join("documents/download"),
8084
team: args.content_directory.join("team/assets"),

src/news.rs

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
use std::collections::HashSet;
2+
use std::path::Path;
3+
use std::sync::Arc;
4+
5+
use anyhow::anyhow;
6+
use rst_parser::parse;
7+
use rst_renderer::render_html;
8+
use serde::de::Error;
9+
use serde::{Deserialize, Deserializer, Serialize, Serializer};
10+
use time::Date;
11+
12+
use crate::lang::Language;
13+
14+
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
15+
struct MyDate(Date);
16+
17+
#[derive(Debug, Clone)]
18+
pub(crate) struct News {
19+
posts: Arc<Vec<Arc<Post>>>,
20+
small_posts: Arc<Vec<Arc<SmallPost>>>,
21+
}
22+
23+
#[derive(Deserialize)]
24+
pub(crate) struct WrittenNewsMeta {
25+
title: String,
26+
published: MyDate,
27+
modified: Option<MyDate>,
28+
description: String,
29+
keywords: Vec<String>,
30+
authors: Vec<String>,
31+
image: Option<String>,
32+
}
33+
34+
#[derive(Serialize, Debug, Clone)]
35+
pub(crate) struct Post {
36+
slug: String,
37+
lang: Language,
38+
idx: u32,
39+
title: String,
40+
published: MyDate,
41+
modified: Option<MyDate>,
42+
description: String,
43+
keywords: Vec<String>,
44+
authors: Vec<String>,
45+
image: Option<String>,
46+
body: String,
47+
}
48+
49+
#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
50+
pub(crate) struct SmallPost {
51+
slug: String,
52+
lang: Language,
53+
idx: u32,
54+
title: String,
55+
published: MyDate,
56+
modified: Option<MyDate>,
57+
description: String,
58+
keywords: Vec<String>,
59+
authors: Vec<String>,
60+
image: Option<String>,
61+
}
62+
63+
impl News {
64+
pub(crate) async fn load(directory: &Path) -> anyhow::Result<Self> {
65+
let mut posts = Vec::new();
66+
67+
let mut dir = tokio::fs::read_dir(directory).await?;
68+
while let Some(entry) = dir.next_entry().await? {
69+
if entry.file_type().await?.is_dir() {
70+
continue;
71+
}
72+
73+
let path = entry.path();
74+
let content = tokio::fs::read_to_string(path.as_path()).await?;
75+
let content = content.trim_start();
76+
let content = content.strip_prefix("---").unwrap();
77+
let (meta, text) = content.split_once("---").unwrap();
78+
79+
let meta: WrittenNewsMeta = serde_yaml::from_str(meta)?;
80+
let file_name = path.file_name().unwrap().to_str().unwrap();
81+
82+
if file_name.starts_with('_') {
83+
continue;
84+
}
85+
86+
let is_rst_file = file_name.ends_with(".rst");
87+
88+
let (idx, lang, slug) = parse_file_name(file_name)?;
89+
90+
let body = if is_rst_file {
91+
let mut buffer: Vec<u8> = Vec::new();
92+
let parsed_rst = parse(text)
93+
.map_err(|e| {
94+
eprintln!("cannot parse rst file {} with error {}", &file_name, e);
95+
})
96+
.unwrap_or_default();
97+
render_html(&parsed_rst, &mut buffer, true)
98+
.map_err(|e| {
99+
eprintln!(
100+
"cannot render rst file to html {} with error {}",
101+
&file_name, e
102+
);
103+
})
104+
.unwrap_or_default();
105+
String::from_utf8(buffer)?
106+
} else {
107+
markdown::to_html(text)
108+
};
109+
110+
posts.push(Arc::new(Post {
111+
slug: slug.to_string(),
112+
lang,
113+
idx,
114+
title: meta.title,
115+
published: meta.published,
116+
modified: meta.modified,
117+
description: meta.description,
118+
keywords: meta.keywords,
119+
authors: meta.authors,
120+
image: meta.image,
121+
body,
122+
}));
123+
}
124+
125+
posts.sort_by(|a, b| b.idx.cmp(&a.idx));
126+
127+
let small_posts = posts
128+
.iter()
129+
.map(|post| {
130+
Arc::new(SmallPost {
131+
slug: post.slug.clone(),
132+
lang: post.lang,
133+
idx: post.idx,
134+
title: post.title.clone(),
135+
published: post.published,
136+
modified: post.modified,
137+
description: post.description.clone(),
138+
keywords: post.keywords.clone(),
139+
authors: post.authors.clone(),
140+
image: post.image.clone(),
141+
})
142+
})
143+
.collect();
144+
145+
Ok(News {
146+
posts: Arc::new(posts),
147+
small_posts: Arc::new(small_posts),
148+
})
149+
}
150+
151+
pub(crate) fn posts(&self, lang: Language) -> Vec<Arc<SmallPost>> {
152+
self
153+
.small_posts
154+
.iter()
155+
.filter(|post| post.lang == lang)
156+
.cloned()
157+
.collect()
158+
}
159+
160+
pub(crate) fn find_post(&self, lang: Language, slug: &str) -> Option<Arc<Post>> {
161+
self
162+
.posts
163+
.iter()
164+
.find(|post| post.lang == lang && post.slug == slug)
165+
.cloned()
166+
}
167+
168+
pub(crate) fn search_by_keywords(
169+
&self,
170+
lang: Language,
171+
keywords: &Vec<String>,
172+
) -> Vec<Arc<SmallPost>> {
173+
let posts = self
174+
.small_posts
175+
.iter()
176+
.filter(|post| post.lang == lang)
177+
.collect::<Vec<_>>();
178+
179+
let keywords_set = keywords.iter().collect::<HashSet<_>>();
180+
181+
let mut or = posts
182+
.iter()
183+
.filter(|post| {
184+
post
185+
.keywords
186+
.iter()
187+
.collect::<HashSet<_>>()
188+
.intersection(&keywords_set)
189+
.next()
190+
.is_some()
191+
})
192+
.cloned()
193+
.cloned()
194+
.collect::<Vec<_>>();
195+
196+
let mut and = posts
197+
.iter()
198+
.filter(|post| {
199+
!or.contains(post)
200+
&& post
201+
.keywords
202+
.iter()
203+
.collect::<HashSet<_>>()
204+
.intersection(&keywords_set)
205+
.count()
206+
== keywords.len()
207+
})
208+
.cloned()
209+
.cloned()
210+
.collect::<Vec<_>>();
211+
212+
or.append(&mut and);
213+
214+
or
215+
}
216+
217+
pub(crate) fn keywords(&self) -> HashSet<String> {
218+
self
219+
.small_posts
220+
.iter()
221+
.flat_map(|post| post.keywords.clone())
222+
.collect()
223+
}
224+
}
225+
226+
pub(crate) fn parse_file_name(file_name: &str) -> anyhow::Result<(u32, Language, &str)> {
227+
let mut split = file_name.split('.');
228+
229+
let idx = split
230+
.next()
231+
.ok_or_else(|| anyhow!("Index missing in file name {}", file_name))?
232+
.parse()?;
233+
let slug = split
234+
.next()
235+
.ok_or_else(|| anyhow!("Slug missing in file name {}", file_name))?;
236+
let lang = split
237+
.next()
238+
.ok_or_else(|| anyhow!("Language missing in file name {}", file_name))?
239+
.try_into()?;
240+
241+
Ok((idx, lang, slug))
242+
}
243+
244+
impl Serialize for MyDate {
245+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
246+
where
247+
S: Serializer,
248+
{
249+
let s = format!(
250+
"{:0>4}-{:0>2}-{:0>2}",
251+
self.0.year(),
252+
self.0.month() as u8,
253+
self.0.day()
254+
);
255+
256+
serializer.serialize_str(&s)
257+
}
258+
}
259+
260+
impl<'de> Deserialize<'de> for MyDate {
261+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
262+
where
263+
D: Deserializer<'de>,
264+
{
265+
let s = String::deserialize(deserializer)?;
266+
let mut split = s.split('-');
267+
268+
let year = split
269+
.next()
270+
.ok_or_else(|| Error::custom(format!("Invalid date format {}", s)))?
271+
.parse()
272+
.map_err(|e| Error::custom(format!("{}", e)))?;
273+
274+
let month: u8 = split
275+
.next()
276+
.ok_or_else(|| Error::custom(format!("Invalid date format {}", s)))?
277+
.parse()
278+
.map_err(|e| Error::custom(format!("{}", e)))?;
279+
280+
let day = split
281+
.next()
282+
.ok_or_else(|| Error::custom(format!("Invalid date format {}", s)))?
283+
.parse()
284+
.map_err(|e| Error::custom(format!("{}", e)))?;
285+
286+
Date::from_calendar_date(
287+
year,
288+
month
289+
.try_into()
290+
.map_err(|e| Error::custom(format!("{}", e)))?,
291+
day,
292+
)
293+
.map_err(|e| Error::custom(format!("{}", e)))
294+
.map(MyDate)
295+
}
296+
}

src/routes/mod.rs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@ use axum::routing::get;
55
use axum::Router;
66
use tower_http::services::ServeDir;
77

8-
use crate::routes::blog::{find_keywords, find_post, list_posts};
8+
use crate::routes::blog::{
9+
find_keywords as blog_find_keywords, find_post as blog_find_post, list_posts as blog_list_posts,
10+
};
911
use crate::routes::documents::list_documents;
1012
use crate::routes::event::{find_event, list_all_events, list_future_events};
13+
use crate::routes::news::{
14+
find_keywords as news_find_keywords, find_post as news_find_post, list_posts as news_list_posts,
15+
};
1116
use crate::routes::peers::get_peers_and_supporter;
1217
use crate::routes::team::get_team;
1318
use crate::routes::text_blocks::find_text_block;
@@ -24,8 +29,11 @@ mod stats;
2429
mod team;
2530
mod text_blocks;
2631

32+
mod news;
33+
2734
pub(crate) struct ContentPaths {
2835
pub(crate) blog: PathBuf,
36+
pub(crate) news: PathBuf,
2937
pub(crate) event: PathBuf,
3038
pub(crate) text_blocks: PathBuf,
3139
pub(crate) document: PathBuf,
@@ -34,9 +42,12 @@ pub(crate) struct ContentPaths {
3442

3543
pub(crate) fn route(content_paths: &ContentPaths) -> Router<FoundationState> {
3644
Router::new()
37-
.route("/blog/:lang", get(list_posts))
38-
.route("/blog/:lang/:slug", get(find_post))
39-
.route("/blog/keywords", get(find_keywords))
45+
.route("/blog/:lang", get(blog_list_posts))
46+
.route("/blog/:lang/:slug", get(blog_find_post))
47+
.route("/blog/keywords", get(blog_find_keywords))
48+
.route("/news/:lang", get(news_list_posts))
49+
.route("/news/:lang/:slug", get(news_find_post))
50+
.route("/news/keywords", get(news_find_keywords))
4051
.route("/event/:lang/all", get(list_all_events))
4152
.route("/event/:lang/upcoming", get(list_future_events))
4253
.route("/event/:lang/:slug", get(find_event))
@@ -46,6 +57,7 @@ pub(crate) fn route(content_paths: &ContentPaths) -> Router<FoundationState> {
4657
ServeDir::new(&content_paths.text_blocks),
4758
)
4859
.nest_service("/blog/assets", ServeDir::new(&content_paths.blog))
60+
.nest_service("/news/assets", ServeDir::new(&content_paths.news))
4961
.nest_service("/event/assets", ServeDir::new(&content_paths.event))
5062
.route("/documents/:lang", get(list_documents))
5163
.nest_service(

0 commit comments

Comments
 (0)