aboutsummaryrefslogblamecommitdiffstats
path: root/src/posts/mod.rs
blob: b188ee03a9d2727f73d3997e1f113058e5ea4ac8 (plain) (tree)
1
2
3
4
5
6
7
8
                             
                            




                                                          
 


                                         










                 



                             

                                      
                      



















                                                                                 

 


















                                                                    
                   
                                             

 











































































                                                                             
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use markdown::{mdast::Node, Constructs, 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,
}

enum TextFormat {
    Markdown,
    Plaintext,
    Html,
}

#[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,
    data: D,
}

impl<D: Content> 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", 5)?;
        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.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(dir) = articles_dir.next_entry().await? {
        let mut file_path = dir.path();
        file_path.push(dir.file_name());
        let file_path = file_path.with_extension("md");
        println!("{:?}", file_path);
        let blogpost: Article = Article {
            path: file_path.to_str().unwrap_or_default().to_owned(),
        };
        let blogpost = Post::try_from(blogpost).await.unwrap();
        println!("{:?}", blogpost);
        blogposts.push(blogpost);
    }
    Ok(blogposts)
}

#[async_trait]
pub trait Content {
    async fn render(&self) -> Result<String>;
}

#[derive(Debug)]
pub struct Article {
    path: 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?;
        Ok(markdown::to_html(&buf))
    }
}

#[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,
            data: article,
        })
    }
}