srctrait_note/
kind.rs

1use std::{path::Path, str::FromStr};
2
3use chrono::NaiveDate;
4use srctrait_common_chronox::DateTimeFormat;
5
6use crate::*;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq,
9    strum::Display, strum::IntoStaticStr, strum::AsRefStr, strum::EnumIter, strum::EnumString)]
10#[strum(serialize_all = "kebab-case")]
11pub enum NoteKind {
12    Today,
13    Idea,
14    Todo,
15    Plan,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq, strum::Display)]
19#[strum(serialize_all = "kebab-case")]
20pub enum NoteType {
21    Today(Date),
22    Idea(String),
23    Todo(String),
24    Plan(Option<String>),
25}
26
27impl NoteType {
28    pub fn kind(&self) -> NoteKind {
29        match self {
30            Self::Today(_) => NoteKind::Today,
31            Self::Idea(_) => NoteKind::Idea,
32            Self::Todo(_) => NoteKind::Todo,
33            Self::Plan(_) => NoteKind::Plan,
34        }
35    }
36    
37    pub fn topic(&self) -> Option<&str> {
38        match self {
39            Self::Today(_) => None,
40            Self::Idea(topic) => Some(topic),
41            Self::Todo(topic) => Some(topic),
42            Self::Plan(topic) => topic.as_deref(),
43        }
44    }
45    
46    pub fn date(&self) -> Option<&Date> {
47        match self {
48            Self::Today(date) => Some(date),
49            _ => None,
50        }
51    }
52    
53    pub fn is_topical(&self) -> bool {
54        match self {
55            Self::Today(_) => false,
56            Self::Idea(_) => true,
57            Self::Todo(_) => true,
58            Self::Plan(topic) => topic.is_some(),
59        }
60    }
61    
62    pub fn is_topical_optional(&self) -> bool {
63        match self {
64            Self::Today(_) => false,
65            Self::Idea(_) => false,
66            Self::Todo(_) => false,
67            Self::Plan(_) => true,
68        }
69    }
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct Note(NoteType);
74
75impl Note {
76    pub fn new(note_type: NoteType) -> Self {
77        Self (note_type)
78    }
79    
80    pub fn from_filepath(notes_dir: &NotesDir, file: &Path) -> Result<Self> {
81        let dir: &Path = notes_dir;
82        let relpath = file.strip_prefix(dir)
83            .map_err(|_| Error::InvalidNote(format!("File is not in the notes directory: {}", file.display())))?;
84        
85        let mut path_components = relpath.components();
86        let kind_dir_name = path_components.next()
87            .ok_or_else(|| Error::InvalidNote(format!("Unexpected note type: {}", file.display())))?
88            .as_os_str().to_string_lossy();
89        
90        let kind = NoteKind::from_str(&kind_dir_name)
91            .map_err(|_| Error::InvalidNote(format!("Unable to determine note type for file path: {}", file.display())))?;
92        
93        if relpath.extension().is_none_or(|s| s != "md") {
94            return Err(Error::InvalidNote(format!("Not a markdown file: {}", file.display())));
95        }
96        
97        let filestem = relpath.file_stem()
98            .ok_or_else(|| Error::InvalidNote(format!("Unable to determine note type for file path: {}", file.display())))?
99            .to_string_lossy();
100        
101        let mut parts = filestem.split('-');
102        let prefix = parts.next();
103        let rest = parts.collect::<Vec<_>>().join("-");
104        
105        if prefix.is_none() || prefix.is_some_and(|s| s != kind.as_ref()) {
106            return Err(Error::InvalidNote(format!("Unable to determine note type for file name: {filestem}")));
107        }
108        
109        let note_type = match kind {
110            NoteKind::Today => {
111                let date = NaiveDate::parse_from_str(&rest, DateTimeFormat::YmdDash.strftime_format())
112                    .map_err(|_| Error::InvalidNote(format!("Unable to determine note type for file path: {}", file.display())))?;
113                
114                NoteType::Today(Date(date))
115            },
116            NoteKind::Idea => NoteType::Idea(rest),
117            NoteKind::Todo => NoteType::Todo(rest),
118            NoteKind::Plan => if rest.is_empty() {
119                NoteType::Plan(None)
120            } else {
121                NoteType::Plan(Some(rest))
122            }
123        };
124        
125        Ok(Note::new(note_type))
126    }
127    
128    pub fn note_type(&self) -> &NoteType {
129        &self.0
130    }
131    
132    pub fn kind(&self) -> NoteKind {
133        self.0.kind()
134    }
135    
136    pub fn topic(&self) -> Option<&str> {
137        self.0.topic()
138    }
139    
140    pub fn date(&self) -> Option<&Date> {
141        self.0.date()
142    }
143}