leafslug_jnl 0.4.0

A personal journaling system that is not inteded to be used by others... yet.
Documentation
use std::{fmt::Display, path::PathBuf, sync::Arc};

use clap::ValueEnum;
use color_eyre::owo_colors::OwoColorize;
use time::{formatting::Formattable, OffsetDateTime};

use leafslug_effects::files::ToFileName;

use super::Error;

#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Entry {
    #[serde(with = "time::serde::rfc3339")]
    pub at: OffsetDateTime,
    pub body: Arc<String>,
    pub tag: Vec<String>,
    pub mood: Mood,
    pub people: Vec<String>,
}

#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default, ValueEnum)]
pub enum Mood {
    Good,
    Bad,
    #[default]
    Neutral,
}

impl Display for Mood {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}",
            match self {
                Self::Good => "Good",
                Self::Bad => "Bad",
                Self::Neutral => "Neutral",
            }
        )
    }
}

// const FILE_NAME_FORMAT: &str = "[year]-[month]-[day]-[hour]-[minute]-[second].json";

impl ToFileName for Entry {
    type Error = Error;
    fn to_file_name(
        &self,
        time_format_descriptor_for_file_name: &(impl Formattable + ?Sized),
    ) -> Result<String, Self::Error> {
        Ok(self.at.format(time_format_descriptor_for_file_name)?)
    }
}

impl PartialOrd for Entry {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(self.cmp(other))
    }
}
impl Ord for Entry {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        self.at.cmp(&other.at)
    }
}

impl TryFrom<PathBuf> for Entry {
    type Error = Error;

    fn try_from(value: PathBuf) -> Result<Self, Self::Error> {
        let content = fs_extra::file::read_to_string(value).map_err(Error::FileCouldNotBeRead)?;

        let entry: Self = serde_json::from_str(&content)
            .map_err(|e| Error::FileCouldNotDeserializeEntryFromJson(e, content))?;
        Ok(entry)
    }
}

impl Entry {
    pub fn pretty_formated(
        &self,
        time_format_descriptor_for_display: &(impl Formattable + ?Sized),
    ) -> Result<String, Error> {
        let date = format!(
            "{}",
            self.at
                .format(&time_format_descriptor_for_display)?
                .dimmed()
        );

        let body = format!("{}", self.body.bold());

        let tags = if self.tag.is_empty() {
            String::new()
        } else {
            self.tag.iter().fold(String::new(), |accu, item| {
                format!("{}#{} ", accu, item.italic())
            })
        };

        Ok(format!("{date}\n{body}\n{tags}"))
    }
}

#[cfg(test)]
mod testing {
    #[allow(clippy::wildcard_imports)]
    use super::*;

    const fn is_normal<T: Sized + Send + Sync + Unpin>() {}

    #[test]
    const fn normal_types() {
        is_normal::<Entry>();
    }
}