diary_cli/
entry.rs

1pub mod section;
2pub use section::*;
3use toml::Table;
4use soulog::*;
5use lazy_db::*;
6use std::path::Path;
7use crate::search::Searchable;
8pub use crate::{
9    list,
10    unpack_array,
11    unwrap_opt,
12    read_db_container,
13    write_db_container,
14};
15
16// Some ease of life utils for section
17#[macro_export]
18macro_rules! unwrap_opt {
19    (($opt:expr) with $logger:ident, format: $origin:ident$error:tt) => {
20        match $opt {
21            Some(x) => x,
22            None => {
23                log!(($logger.error) $origin$error as Fatal);
24                $logger.crash()
25            }
26        }
27    }
28}
29
30#[macro_export]
31macro_rules! read_db_container {
32    ($key:ident from $name:ident($container:expr) as $func:ident with $logger:ident) => {{
33        let data = if_err!(($logger) [$name, err => ("While reading from archive: {:?}", err)] retry $container.read_data(stringify!($key)));
34        if_err!(($logger) [$name, err => ("While reading from archive: {:?}", err)] {data.$func()} crash {
35            log!(($logger.error) $name("{err:#?}") as Fatal);
36            $logger.crash()
37        })
38    }}
39}
40
41#[macro_export]
42macro_rules! write_db_container {
43    ($name:ident($container:expr) $key:ident = $func:ident($value:expr) with $logger:ident) => {
44        if_err!(($logger) [$name, err => ("While writing to archive: {err:?}")] retry write_container!(($container) $key = $func($value)));
45    }
46}
47
48#[macro_export]
49macro_rules! unpack_array {
50    ($result:ident from $raw:ident with $logger:ident by $x:ident => $code:expr) => {
51        let mut $result = Vec::with_capacity($raw.len());
52        for $x in $raw {
53            $result.push($code)
54        };
55    };
56
57    ($result:ident from $raw:ident with $logger:ident by ($i:ident, $x:ident) => $code:expr) => {
58        let mut $result = Vec::with_capacity($raw.len());
59        for ($i, $x) in $raw.iter().enumerate() {
60            $result.push($code)
61        };
62    };
63}
64
65macro_rules! get {
66    ($key:ident at $entry:ident from $table:ident as $func:ident with $logger:ident) => {{
67        let key = stringify!($key);
68        let obj = unwrap_opt!(($table.get(key)) with $logger, format: Entry("Entry '{0}' must have '{key}' attribute", $entry));
69
70        unwrap_opt!((obj.$func()) with $logger, format: Entry("Entry '{0}'s '{key}' attribute must be of correct type", $entry))
71    }};
72
73    ($var:ident = $key:ident at $entry:ident from $table:ident as $func:ident with $logger:ident or $default:expr) => {
74        let key = stringify!($key);
75        let default = $default;
76        let $var = $table.get(key)
77            .map(|x| unwrap_opt!((x.$func()) with $logger, format: Entry("Entry '{0}'s '{key}' attribute must be of the correct type", $entry)))
78            .unwrap_or(&default);
79    };
80}
81
82pub struct Entry {
83    pub container: LazyContainer,
84    pub uid: String,
85    pub sections: Option<Box<[Section]>>,
86    pub title: Option<String>,
87    pub description: Option<String>,
88    pub tags: Option<Box<[String]>>,
89    pub notes: Option<Box<[String]>>,
90    /// Date goes from `day` to `month` then to `year`
91    pub date: Option<[u16; 3]>,
92}
93
94impl Entry {
95    pub fn new(table: Table, entry_path: &str, database: LazyContainer, mut logger: impl Logger) -> Self {
96        log!((logger) Entry("Reading entry '{entry_path}'s raw unchecked data..."));
97
98        let entry_table = get!(entry at entry_path from table as as_table with logger); // For nice entry nesting
99        let uid = get!(uid at entry_path from entry_table as as_str with logger).to_string();
100
101        let title = get!(title at entry_path from entry_table as as_str with logger).to_string();
102        let description = get!(description at entry_path from entry_table as as_str with logger).to_string();
103        get!(raw_notes = notes at entry_path from entry_table as as_array with logger or Vec::<toml::Value>::with_capacity(0));
104        let raw_tags = get!(tags at entry_path from entry_table as as_array with logger);
105        let raw_sections = get!(section at entry_path from table as as_array with logger);
106
107        // set the container
108        let container =
109            if_err!((logger) [Entry, err => ("While initialising entry: '{err:?}'")] retry database.new_container(&uid));
110
111        // Get date
112        log!((logger) Entry("Parsing date..."));
113        let date: toml::value::Date = unwrap_opt!(
114            (get!(date at entry_path from entry_table as as_datetime with logger).date)
115            with logger,
116            format: Entry("Datetime 'date' must contain the date")
117        ); let date = [ date.day as u16, date.month as u16, date.year ];
118
119        // Parse simple arrays
120        log!((logger) Entry("Parsing notes & tags..."));
121        unpack_array!(notes from raw_notes with logger by x
122            => unwrap_opt!((x.as_str()) with logger, format: Entry("All notes in entry '{entry_path}' must be strings")).to_string()
123        );
124
125        unpack_array!(tags from raw_tags with logger by x
126            => unwrap_opt!((x.as_str()) with logger, format: Entry("All tags in entry '{entry_path}' must be strings")).to_string()
127        );
128
129        // Parse sections
130        log!((logger) Entry("Parsing entry's sections..."));
131        let list = if_err!((logger) [Entry, err => ("While initialising sections: {err:?}")] retry container.new_container("sections"));
132        unpack_array!(sections from raw_sections with logger by (i, x) => {
133            let container = if_err!((logger) [Entry, err => ("While initialising section {i}: {err:?}")] retry list.new_container(i.to_string()));
134            let table = unwrap_opt!((x.as_table()) with logger, format: Entry("Entry '{entry_path}', section {i} must be a toml table"));
135            Section::new(table, container, entry_path, i as u8, logger.hollow()) // Write into that container
136        });
137        if_err!((logger) [Entry, err => ("While writing section list length: {err:?}")] retry write_container!((list) length = new_u16(raw_sections.len() as u16)));
138
139        log!((logger) Entry("Storing entry's parsed and checked data into archive..."));
140
141        let mut this = Self {
142            container,
143            uid,
144            title: Some(title),
145            description: Some(description),
146            date: Some(date),
147            notes: Some(notes.into_boxed_slice()),
148            tags: Some(tags.into_boxed_slice()),
149            sections: Some(sections.into_boxed_slice()),
150        };
151        this.store_lazy(logger.hollow());
152        log!((logger) Entry("Successfully written entry into archive"));
153        log!((logger) Entry("")); // spacer
154        this.clear_cache();
155        this
156    }
157
158    pub fn pull(&mut self, path: &Path, one_file: bool, logger: impl Logger) -> Table {
159        let mut map = Table::new();
160        let mut entry = Table::new();
161
162        // Insert uid, title, description, notes, tags, and date
163        entry.insert("uid".into(), self.uid.clone().into());
164        entry.insert("title".into(), self.title(logger.hollow()).clone().into());
165        entry.insert("description".into(), self.description(logger.hollow()).clone().into());
166        entry.insert("notes".into(), self.notes(logger.hollow()).to_vec().into());
167        entry.insert("tags".into(), self.tags(logger.hollow()).to_vec().into());
168        entry.insert("date".into(), Self::array_to_date(self.date(logger.hollow()), logger.hollow()));
169        map.insert("entry".into(), entry.into());
170
171        self.clear_cache();
172
173        map.insert("section".into(), self.sections(logger.hollow())
174            .iter_mut()
175            .enumerate()
176            .map(|(i, x)| x.pull(i as u8, path, one_file, logger.hollow()))
177            .collect::<Vec<Table>>()
178            .into()
179        );
180
181        self.clear_cache();
182
183        map
184    }
185
186    fn array_to_date(arr: &[u16; 3], mut logger: impl Logger) -> toml::Value {
187        // Format the array of u16s to a string in the RFC 3339 date format
188        let date_string = format!("{:04}-{:02}-{:02}",
189            arr[2], // Year
190            arr[1], // Month
191            arr[0], // Day
192        );
193    
194        // Parse the string to a toml::Value::Datetime
195        toml::Value::Datetime(if_err!((logger) [Pull, _err => ("Invalid entry date")]
196            {date_string.parse()}
197            crash logger.crash()
198        ))
199    }
200
201    pub fn store_lazy(&self, mut logger: impl Logger) {
202        log!((logger) Entry("Storing entry into archive..."));
203        // Only store them if modified
204        if let Some(x) = &self.title { write_db_container!(Entry(self.container) title = new_string(x) with logger); }
205        if let Some(x) = &self.description { write_db_container!(Entry(self.container) description = new_string(x) with logger); }
206        if let Some(x) = &self.date { write_db_container!(Entry(self.container) date = new_u16_array(x) with logger); }
207
208        // The bloody lists & arrays
209        if let Some(x) = &self.notes {
210            list::write(
211                x.as_ref(),
212                |file, data| LazyData::new_string(file, data),
213                &if_err!((logger) [Entry, err => ("While writing notes to archive: {:?}", err)] retry self.container.new_container("notes")),
214                logger.hollow()
215            );
216        }
217
218        if let Some(x) = &self.tags {
219            list::write(
220                x.as_ref(),
221                |file, data| LazyData::new_string(file, data),
222                &if_err!((logger) [Entry, err => ("While writing tags to archive: {:?}", err)] retry self.container.new_container("tags")),
223                logger.hollow()
224            );
225        }
226    }
227
228    pub fn load_lazy(uid: String, container: LazyContainer) -> Self {
229        Self {
230            container,
231            uid,
232            title: None,
233            sections: None,
234            description: None,
235            tags: None,
236            notes: None,
237            date: None,
238        }
239    }
240
241    pub fn clear_cache(&mut self) {
242        self.title = None;
243        self.sections = None;
244        self.description = None;
245        self.tags = None;
246        self.notes = None;
247        self.date = None;
248    }
249
250    pub fn fill_cache(&mut self, logger: impl Logger) {
251        self.title(logger.hollow());
252        self.sections(logger.hollow());
253        self.tags(logger.hollow());
254        self.notes(logger.hollow());
255        self.date(logger.hollow());
256    }
257
258    cache_field!(title(this, logger) -> String {
259        read_db_container!(title from EntrySection(this.container) as collect_string with logger)
260    });
261
262    cache_field!(description(this, logger) -> String {
263        read_db_container!(description from Entry(this.container) as collect_string with logger)
264    });
265
266    cache_field!(notes(this, logger) -> Box<[String]> {
267        list::read(
268            |data| data.collect_string(),
269            &if_err!((logger) [Entry, err => ("While reading from entry's notes: {err:?}")] retry this.container.child_container("notes")),
270            logger
271        )
272    });
273
274    cache_field!(tags(this, logger) -> Box<[String]> {
275        list::read(
276            |data| data.collect_string(),
277            &if_err!((logger) [Entry, err => ("While reading from entry's tags: {err:?}")] retry this.container.child_container("tags")),
278            logger
279        )
280    });
281
282    cache_field!(date(this, logger) -> [u16; 3] {
283        let array = read_db_container!(date from Entry(this.container) as collect_u16_array with logger);
284        [array[0], array[1], array[2]]
285    });
286
287    cache_field!(sections(this, logger) -> Box<[Section]> {
288        let container = if_err!((logger) [Entry, err => ("While reading from entry's sections: {err:?}")] retry this.container.child_container("sections"));
289        let length = if_err!((logger) [Entry, err => ("While reading from entry's sections' length: {err:?}")] retry container.read_data("length"));
290        let length = if_err!((logger) [Entry, err => ("While reading from entry's sections' length: {err:?}")] {length.collect_u16()} crash {
291            log!((logger) Entry("{err:#?}") as Fatal);
292            logger.crash()
293        });
294        let mut sections = Vec::with_capacity(length as usize);
295
296        for i in 0..length {
297            sections.push(Section::load_lazy(
298                if_err!((logger) [Entry, err => ("While reading entry section {i}: {err:?}")] retry container.child_container(i.to_string()))
299            ));
300        }
301
302        sections.into_boxed_slice()
303    });
304}
305
306impl Searchable for Entry {
307    fn get_uid(&self) -> String {
308        self.uid.clone()
309    }
310
311    fn contains_tag(&mut self, tag: &String, logger: impl Logger) -> bool {
312        let result = self.tags(logger).contains(tag);
313        self.tags = None;
314        result
315    }
316}