1#![doc = include_str!("../../README.md")]
2#![warn(clippy::all, clippy::nursery)]
3extern crate confy;
4
5#[macro_use]
6extern crate serde_derive;
7extern crate serde;
8
9pub mod config;
10pub mod errors;
11pub mod ops;
12pub mod utils;
13
14pub type CliResult = Result<(), errors::CliError>;
15
16use std::{
17 fs::{File, OpenOptions},
18 io,
19 path::{Path, PathBuf},
20 str::FromStr,
21};
22
23use chrono::prelude::*;
24use enum_dispatch::enum_dispatch;
25
26use crate::{
27 config::Config,
28 errors::DiaryError,
29 utils::{date, file_system},
30};
31
32fn title_elements(date: DateTime<Local>) -> (String, String, String) {
33 let start_title = date.format("%A %-e").to_string();
34 let date_superscript = date::date_superscript(date.day()).to_owned();
35 let end_title = date.format("%B %Y").to_string();
36
37 (start_title, date_superscript, end_title)
38}
39
40#[enum_dispatch]
41pub trait EntryContent {
42 fn extension(&self) -> &'static str;
43
44 fn title(&self, date: &DateTime<Local>) -> String;
45
46 fn tag(&self, tag_name: String) -> String;
47}
48
49pub struct MarkdownDiary {}
50
51impl EntryContent for MarkdownDiary {
52 fn extension(&self) -> &'static str {
53 "md"
54 }
55
56 fn title(&self, date: &DateTime<Local>) -> String {
57 let (start_title, date_superscript, end_title) = title_elements(*date);
58
59 format!(
60 "# {}<sup>{}</sup> {}\n\n",
61 start_title, date_superscript, end_title
62 )
63 }
64
65 fn tag(&self, tag_name: String) -> String {
66 format!("## {}\n\n", tag_name)
67 }
68}
69pub struct RstDiary {}
70
71impl EntryContent for RstDiary {
72 fn extension(&self) -> &'static str {
73 "rst"
74 }
75
76 fn title(&self, date: &DateTime<Local>) -> String {
77 let (start_title, date_superscript, end_title) = title_elements(*date);
78
79 let first_line = format!(
80 "{}\\ :sup:`{}` {}",
81 start_title, date_superscript, end_title
82 );
83 let first_line_len = first_line.chars().count();
84
85 let second_line = format!("{:=<1$}", "", first_line_len);
86
87 format!("{}\n{}\n\n", first_line, second_line)
88 }
89
90 fn tag(&self, tag_name: String) -> String {
91 let first_line_len = tag_name.chars().count();
92
93 let second_line = format!("{:^<1$}", "", first_line_len);
94
95 format!("{}\n{}\n\n", tag_name, second_line)
96 }
97}
98
99#[enum_dispatch(EntryContent)]
100#[non_exhaustive]
101pub enum EntryFileType {
102 MarkdownDiary,
103 RstDiary,
104}
105
106impl FromStr for EntryFileType {
107 type Err = DiaryError;
108
109 fn from_str(s: &str) -> Result<Self, Self::Err> {
110 match s {
111 "md" => Ok(MarkdownDiary {}.into()),
112 "rst" => Ok(RstDiary {}.into()),
113 _ => Err(DiaryError::BadFileType),
114 }
115 }
116}
117
118pub fn process_file_type(potential_file_type: Option<&str>) -> Result<Option<&str>, DiaryError> {
119 potential_file_type.map_or_else(
120 || Ok(None),
121 |file_type| match EntryFileType::from_str(file_type) {
122 Err(_) => Err(DiaryError::BadFileType),
123 Ok(_) => Ok(Some(file_type)),
124 },
125 )
126}
127
128pub struct Diary {
129 prefix: String,
130 diary_path: PathBuf,
131 file_type: EntryFileType,
132}
133
134impl Diary {
135 pub fn new(prefix: &str, diary_path: &Path, file_type: &str) -> Result<Box<Self>, DiaryError> {
136 if !diary_path.exists() {
137 return Err(DiaryError::UnInitialised { source: None });
138 }
139 let entry_file_type = EntryFileType::from_str(file_type)?;
140 Ok(Box::new(Self {
141 prefix: prefix.to_owned(),
142 diary_path: diary_path.to_path_buf(),
143 file_type: entry_file_type,
144 }))
145 }
146
147 pub fn from_config(cfg: &Config) -> Result<Box<Self>, DiaryError> {
148 Self::new(cfg.prefix(), cfg.diary_path(), cfg.file_type())
149 }
150
151 pub const fn prefix(&self) -> &String {
152 &self.prefix
153 }
154 pub const fn diary_path(&self) -> &PathBuf {
155 &self.diary_path
156 }
157 pub const fn file_type(&self) -> &EntryFileType {
158 &self.file_type
159 }
160 pub fn file_name(&self, date: &DateTime<Local>) -> PathBuf {
161 let entry_suffix = date.format("%Y-%m-%d").to_string();
162 let file_name = format!(
163 "{}_{}.{}",
164 self.prefix,
165 entry_suffix,
166 self.file_type.extension()
167 );
168 PathBuf::from(file_name)
169 }
170 pub fn get_entry_path(&self, date: &DateTime<Local>) -> PathBuf {
171 let mut entry_path = file_system::month_folder(self.diary_path(), date);
172 let entry_name = self.file_name(date);
173 entry_path.push(entry_name);
174 entry_path
175 }
176 pub fn get_entry_file(&self, date: &DateTime<Local>) -> io::Result<File> {
177 let entry_path = self.get_entry_path(date);
178 return OpenOptions::new().append(true).open(entry_path);
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use std::{path::PathBuf, str::FromStr};
185
186 use chrono::prelude::*;
187
188 use super::{process_file_type, Diary, EntryContent, EntryFileType, MarkdownDiary, RstDiary};
189 use crate::config::Config;
190
191 #[test]
192 fn get_extension() {
193 let entry_file = EntryFileType::from_str("rst").unwrap();
194
195 assert_eq!(entry_file.extension(), "rst");
196
197 let entry_file = EntryFileType::from_str("md").unwrap();
198
199 assert_eq!(entry_file.extension(), "md")
200 }
201
202 #[test]
203 #[should_panic(expected = "value: BadFileType")]
204 fn unsupported_file_extension() {
205 process_file_type(Some("xyz")).unwrap();
206 }
207
208 #[test]
209 fn rst_title() {
210 let entry_file = RstDiary {};
211 let entry_date = Local.with_ymd_and_hms(2021, 11, 6, 0, 0, 0).unwrap();
212
213 let actual_header = entry_file.title(&entry_date);
214
215 let expected_header =
216 "Saturday 6\\ :sup:`th` November 2021\n===================================\n\n";
217
218 assert_eq!(actual_header, expected_header)
219 }
220
221 #[test]
222 fn rst_tag() {
223 let entry_file = RstDiary {};
224
225 let actual_tag = entry_file.tag("Meeting".to_string());
226
227 let expected_tag = "Meeting\n^^^^^^^\n\n";
228
229 assert_eq!(actual_tag, expected_tag)
230 }
231
232 #[test]
233 fn md_title() {
234 let entry_file = MarkdownDiary {};
235 let entry_date = Local.with_ymd_and_hms(2021, 11, 6, 0, 0, 0).unwrap();
236
237 let actual_header = entry_file.title(&entry_date);
238
239 let expected_header = "# Saturday 6<sup>th</sup> November 2021\n\n";
240
241 assert_eq!(actual_header, expected_header)
242 }
243
244 #[test]
245 fn markdown_tag() {
246 let entry_file = MarkdownDiary {};
247
248 let actual_tag = entry_file.tag("Meeting".to_string());
249
250 let expected_tag = "## Meeting\n\n";
251
252 assert_eq!(actual_tag, expected_tag)
253 }
254
255 #[test]
256 fn diary_file_from_config() {
257 let cfg = Config::builder()
258 .file_type("md")
259 .diary_path("/".into())
260 .build();
261
262 let diary_file = Diary::from_config(&cfg).unwrap();
263
264 assert_eq!(diary_file.file_type().extension(), "md");
265 assert_eq!(diary_file.diary_path(), &PathBuf::from("/"));
266 assert_eq!(diary_file.prefix(), "diary")
267 }
268}