Skip to content

Commit

Permalink
add common abstraction for api service
Browse files Browse the repository at this point in the history
  • Loading branch information
tanneberger committed Dec 1, 2024
1 parent 05102b6 commit 83941a5
Show file tree
Hide file tree
Showing 10 changed files with 493 additions and 595 deletions.
297 changes: 62 additions & 235 deletions src/blog.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,14 @@
use std::collections::HashSet;
use std::path::Path;
use std::sync::Arc;

use anyhow::anyhow;
use asciidork_parser::prelude::Bump;
use asciidork_parser::Parser;
use serde::de::Error;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use time::Date;
use tracing::info;

use crate::lang::Language;
use crate::posts::{
post_provider::{LongPostFromMeta, PostMeta, PostProvider, SmallPostFromLong},
MyDate,
};
use serde::{Deserialize, Serialize};

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
struct MyDate(Date);
pub type Blogs = PostProvider<BlogMeta, SmallBlogPost, BlogPost>;

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

#[derive(Deserialize)]
pub(crate) struct WrittenPostMeta {
#[derive(Deserialize, Clone)]
pub(crate) struct BlogMeta {
title: String,
published: MyDate,
modified: Option<MyDate>,
Expand All @@ -33,7 +19,7 @@ pub(crate) struct WrittenPostMeta {
}

#[derive(Serialize, Debug, Clone)]
pub(crate) struct Post {
pub(crate) struct BlogPost {
slug: String,
lang: Language,
idx: u32,
Expand All @@ -48,7 +34,7 @@ pub(crate) struct Post {
}

#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
pub(crate) struct SmallPost {
pub(crate) struct SmallBlogPost {
slug: String,
lang: Language,
idx: u32,
Expand All @@ -61,233 +47,74 @@ pub(crate) struct SmallPost {
image: Option<String>,
}

impl Blog {
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: WrittenPostMeta = serde_yaml::from_str(meta).expect("cannot parse header in blog");
let file_name = path.file_name().unwrap().to_str().unwrap();

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

let is_adoc_file = file_name.ends_with(".adoc");

info!(
"reading blog post: {} is adoc: {}",
&file_name, &is_adoc_file
);
let (idx, lang, slug) = parse_file_name(file_name).expect("cannot parse file name");

let body = if is_adoc_file {
let bump = &Bump::with_capacity(text.len());

let x = Parser::new_settings(bump, text, Default::default())
.parse()
.unwrap();

asciidork_dr_html_backend::convert(x.document).unwrap()
} 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,
}));
impl LongPostFromMeta<BlogMeta> for BlogPost {
fn from(slug: &str, lang: Language, idx: u32, meta: BlogMeta, body: String) -> Self {
Self {
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(Blog {
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()
impl SmallPostFromLong<BlogPost> for SmallBlogPost {
fn from(post: &BlogPost) -> Self {
let blog_clone = post.clone();
Self {
slug: blog_clone.slug,
lang: blog_clone.lang,
idx: blog_clone.idx,
title: blog_clone.title,
published: blog_clone.published,
modified: blog_clone.modified,
description: blog_clone.description,
keywords: blog_clone.keywords,
authors: blog_clone.authors,
image: blog_clone.image,
}
}
}

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()
impl PostMeta for BlogPost {
fn idx(&self) -> u32 {
self.idx
}

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
fn lang(&self) -> Language {
self.lang
}

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

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)
fn keywords(&self) -> &Vec<String> {
&self.keywords
}
}

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)))?;
impl PostMeta for SmallBlogPost {
fn idx(&self) -> u32 {
self.idx
}

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

let day = split
.next()
.ok_or_else(|| Error::custom(format!("Invalid date format {}", s)))?
.parse()
.map_err(|e| Error::custom(format!("{}", e)))?;
fn slug(&self) -> &str {
&self.slug
}

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)
fn keywords(&self) -> &Vec<String> {
&self.keywords
}
}
Loading

0 comments on commit 83941a5

Please sign in to comment.