mod article;
mod note;
pub mod syndication;
use std::collections::HashSet;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use markdown::{mdast::Node, Constructs, Options, ParseOptions};
use rocket::http::Status;
use serde::{ser::SerializeStruct, Deserialize, Serialize};
use tokio::fs;
use tokio::io::AsyncReadExt;
use crate::{error::BlossomError, Result};
#[derive(Serialize, Debug)]
enum PostType {
Article,
Note,
}
#[derive(Debug)]
pub struct Post<D: Content> {
// id: i64,
subject: Option<String>,
created_at: DateTime<Utc>,
updated_at: Option<DateTime<Utc>>,
tags: Vec<String>,
post_type: PostType,
render: Option<String>,
data: D,
}
impl<D: Content> Post<D> {
pub async fn render(&mut self) -> Result<()> {
self.render = Some(self.data.render().await?);
Ok(())
}
}
impl<D: Content + Serialize> Serialize for Post<D> {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut state = serializer.serialize_struct("Post", 7)?;
state.serialize_field("subject", &self.subject)?;
state.serialize_field("created_at", &self.created_at.to_string())?;
state.serialize_field(
"updated_at",
&self.updated_at.and_then(|time| Some(time.to_string())),
)?;
state.serialize_field("tags", &self.tags)?;
state.serialize_field("post_type", &self.post_type)?;
state.serialize_field("render", &self.render)?;
state.serialize_field("data", &self.data)?;
state.end()
}
}
pub async fn get_blogposts() -> Result<Vec<Post<Article>>> {
let mut blogposts: Vec<Post<Article>> = Vec::new();
let mut articles_dir = fs::read_dir("./articles").await?;
while let Some(file) = articles_dir.next_entry().await? {
let name = file.file_name();
let name = name.to_str().unwrap_or_default()[..name.len() - 3].to_owned();
let blogpost: Article = Article {
path: file.path().to_str().unwrap_or_default().to_owned(),
name,
};
let blogpost = Post::try_from(blogpost).await.unwrap();
blogposts.push(blogpost);
}
blogposts.sort_by_key(|post| post.created_at);
blogposts.reverse();
Ok(blogposts)
}
pub async fn get_blogpost(post_name: &str) -> Result<Post<Article>> {
let mut articles_dir = fs::read_dir("./articles").await?;
while let Some(file) = articles_dir.next_entry().await? {
let name = file.file_name();
let name = &name.to_str().unwrap_or_default()[..name.len() - 3];
if name == post_name {
let blogpost = Article {
path: file.path().to_str().unwrap_or_default().to_owned(),
name: name.to_owned(),
};
let blogpost = Post::try_from(blogpost).await?;
return Ok(blogpost);
}
}
Err(BlossomError::NotFound(Status::new(404)))
}
pub fn get_tags<D: Content>(posts: &Vec<Post<D>>) -> Vec<&String> {
let mut tags = posts
.into_iter()
.fold(HashSet::new(), |mut acc, post| {
let tags = &post.tags;
for tag in tags {
acc.insert(tag);
}
acc
})
.into_iter()
.collect::<Vec<_>>();
tags.sort();
tags
}
pub fn filter_by_tags<'p, D: Content>(
posts: Vec<Post<D>>,
filter_tags: &HashSet<String>,
) -> Vec<Post<D>> {
posts
.into_iter()
.filter(|post| {
for tag in &post.tags {
match filter_tags.contains(tag) {
true => return true,
false => continue,
}
}
false
})
.collect()
}
#[async_trait]
pub trait Content {
async fn render(&self) -> Result<String>;
}
#[derive(Serialize, Debug)]
pub struct Article {
path: String,
name: String,
}
impl Article {
async fn tree(&self) -> Result<Node> {
let mut file = fs::File::open(&self.path).await?;
let mut buf = String::new();
file.read_to_string(&mut buf).await?;
Ok(markdown::to_mdast(
&buf,
&ParseOptions {
constructs: Constructs {
frontmatter: true,
..Constructs::default()
},
..ParseOptions::default()
},
)
.unwrap())
}
async fn frontmatter(&self) -> Result<String> {
let tree = self.tree().await?;
let children = tree.children();
if let Some(children) = children {
if let Some(toml) = children.into_iter().find_map(|el| match el {
Node::Toml(toml) => Some(toml.value.to_owned()),
_ => None,
}) {
return Ok(toml);
};
}
Err(BlossomError::NoMetadata(Status::new(500)))
}
}
#[async_trait]
impl Content for Article {
async fn render(&self) -> Result<String> {
let mut file = fs::File::open(&self.path).await?;
let mut buf = String::new();
file.read_to_string(&mut buf).await?;
let options = Options {
parse: ParseOptions {
constructs: Constructs {
frontmatter: true,
..Constructs::default()
},
..ParseOptions::default()
},
..Options::default()
};
Ok(markdown::to_html_with_options(&buf, &options).unwrap())
}
}
#[derive(Deserialize)]
struct ArticleMetadata {
title: String,
created_at: String,
updated_at: Option<String>,
tags: Vec<String>,
}
impl Post<Article> {
async fn try_from(article: Article) -> Result<Post<Article>> {
let metadata = article.frontmatter().await?;
let metadata: ArticleMetadata = toml::from_str(&metadata)?;
let updated_at = if let Some(updated_at) = metadata.updated_at {
Some(updated_at.parse::<DateTime<Utc>>()?)
} else {
None
};
Ok(Post {
subject: Some(metadata.title),
created_at: metadata.created_at.parse::<DateTime<Utc>>()?,
updated_at,
tags: metadata.tags,
post_type: PostType::Article,
render: None,
data: article,
})
}
}