keydata/
lib.rs

1//! Keydata is a lib for storing data. 
2//!     Data is stored as key-value pairs and organized into named sections
3//!     Data is stored in files created by the lib, in a hidden folder in users home directory
4//! 
5//! # Example
6//! ```
7//!use std::{error::Error, fs};
8//!
9//!fn main() -> Result<(), Box<dyn Error>> {
10//!    let mut file = keydata::KeynoteFile::new("kntest.dat")?;   
11//!    file.load_data()?;
12//!    file.add_section("sectionname")?;
13//!    file.add_entry("sectionname", "somekey", "somevalue")?;
14//!     
15//!    for (_, section) in file.get_sections() {   
16//!        if section.data.len() != 0 {
17//!           println!("{}", section.name)
18//!        }    
19//!      
20//!        for (k, _) in section.data.iter() {
21//!            println!("\t{}", k);
22//!        }
23//!    }    
24//
25//!    fs::remove_file(file.filepath);  // remove the test file
26//!     
27//!    Ok(()) 
28//!}
29//! ```
30
31use std::{fs, fs::{OpenOptions, File}, io, io::{Write, prelude::*}, collections::HashMap, path::PathBuf, error::Error};
32
33mod section;
34
35use aoutils::*;
36pub use section::*;
37
38/// A data structure to represent the keynotes data file
39pub struct KeynoteFile {
40    /// path to the file as a PathBuf
41    pub filepath : PathBuf,
42    /// hashmap to store Section instances
43    sections : HashMap<String, Section> 
44}
45
46impl KeynoteFile {
47    /// Creates a new KeynoteFile
48    ///
49    /// # Arguments
50    ///
51    /// * `filename` - name of file to create in keynotes folder  
52    ///
53    /// # Examples    ///
54    /// ```
55    /// use keydata::*;
56    /// let kn_file = KeynoteFile::new("kntest.dat").unwrap();
57    /// 
58    /// assert!(kn_file.filepath.ends_with("kntest.dat"));
59    ///  
60    /// ```
61    pub fn new<'a>(filename: &str) -> Result<KeynoteFile, &'a str> {
62        // build path to keynotes.dat file        
63        let mut data_filepath = match home::home_dir() {
64            Some(path_buffer) => path_buffer,
65            None => {            
66                return Err("error: unable to find home directory") 
67            }
68        };        
69        
70        data_filepath.push(format!(".keynotes/{}", filename));
71        
72        Ok(KeynoteFile {
73            sections: HashMap::new(),
74            filepath: data_filepath 
75        })
76    }
77
78    /// Loads data from file into KeynoteFile structure     
79    ///
80    /// # Examples
81    /// ```
82    /// use std::fs;
83    /// use keydata::*;
84    /// 
85    /// let mut file = KeynoteFile::new("kntest.dat").unwrap();
86    /// file.load_data(); 
87    /// fs::remove_file(file.filepath);  // remove the test file
88    /// ```
89    pub fn load_data(&mut self) -> Result<(), Box<dyn Error>> {
90        let file = KeynoteFile::open_keynote_file(&self.filepath)?;
91
92        // read lines one at a time, checking for sections and reading them into the data structure
93        let reader = io::BufReader::new(file);         
94        let mut curr_section_name = String::new();
95        for line in reader.lines() {
96            if let Err(_) = line { return Err("error: unable to load data".into()) }
97
98            let line = line.unwrap();            
99            if let Some(section_name) = Section::get_section_name_from_string(&line) {        // handle sections           
100                self.add_section_to_data_structure(section_name);
101                curr_section_name = section_name.to_string();
102            }
103            else if let Some((k, v)) = KeynoteFile::get_entry_from_string(&line) {          // handle entries
104                let section = self.get_section(&curr_section_name);
105                match section {
106                    Some(section) => section.add_entry(k, v), 
107                    None => { 
108                        return Err("error: file format corrupted".into());                            
109                    }
110                };
111            }                        
112        }
113        Ok(())
114    }   
115
116    /// Add a key-value entry into the file
117    ///
118    /// # Arguments
119    ///
120    /// * `section_to_add_to` - section to add entry to as string slice 
121    /// * `key` - key for the entry as string slice 
122    /// * `value` - value of the entry as string slice 
123    ///
124    /// # Examples    ///
125    /// ```
126    /// use std::fs;
127    /// use keydata::*;
128    /// 
129    /// let mut kn_file = KeynoteFile::new("kntest.dat").unwrap();     
130    /// kn_file.add_section("leaders").unwrap();
131    /// 
132    /// kn_file.add_entry("leaders", "atreides", "leto");
133    ///  
134    /// fs::remove_file(kn_file.filepath); // remove the test file
135    /// ```
136    pub fn add_entry<'a>(&mut self, section_to_add_to: &str, key: &str, value: &str) -> Result<(), Box<dyn Error>> {
137        if self.contains_key(key) {
138            return Err(format!("key: {} already exists. no key added.", key).into());            
139        }      
140        
141        // insert into data structure
142        if let Some(section) = self.get_section(section_to_add_to){
143            section.add_entry(key, value);
144        }
145        else {
146            return Err(format!("cannot add to '{}'. that section does not exist", section_to_add_to).into());
147            
148        }
149
150        // write the new key to the file        
151        let file = KeynoteFile::open_keynote_file(&self.filepath)?;
152        let reader = io::BufReader::new(file);
153            
154        let tmp_filepath = self.filepath.with_file_name("_kntemp.dat");
155        let mut tmp_file = KeynoteFile::open_keynote_file(&tmp_filepath)?;      
156
157        for line in reader.lines() {
158            let line = line.unwrap();                
159            let line = ensure_newline(&line);             
160
161            tmp_file.write_all(line.as_bytes())?;               
162               
163            if let Some(section_name) = Section::get_section_name_from_string(&line) {
164                if section_name == section_to_add_to {
165                    // add new entry to file
166                    let entry = KeynoteFile::build_entry_string(key, value);
167
168                    tmp_file.write_all(entry.as_bytes())?;                            
169                }
170            }
171        }
172
173        // now we need to delete the old file and rename the temp one
174        fs::remove_file(self.filepath.clone())?;
175        fs::rename(tmp_filepath, self.filepath.clone())?;
176       
177        Ok(())
178    }
179
180    /// Remove a key-value entry from the file
181    ///
182    /// # Arguments
183    /// 
184    /// * `key` - key for the entry to remove as string slice  
185    ///
186    /// # Examples    ///
187    /// ```
188    /// use std::fs;
189    /// use keydata::*;
190    /// 
191    /// let mut kn_file = KeynoteFile::new("kntest.dat").unwrap();    
192    /// kn_file.add_section("leaders").unwrap();   
193    /// kn_file.add_entry("leaders", "atreides", "leto");
194    /// 
195    /// kn_file.remove_entry("atreides");
196    /// 
197    /// fs::remove_file(kn_file.filepath);  // remove the test file  
198    /// ```
199    pub fn remove_entry(&mut self, key: &str) -> Result<(), Box<dyn Error>>{
200        if !self.contains_key(key) {
201            return Err(format!("key: '{}' does not exist. nothing removed.", key).into());            
202        }     
203              
204        let file = KeynoteFile::open_keynote_file(&self.filepath)?;
205        let reader = io::BufReader::new(file);
206            
207        let tmp_filepath = self.filepath.with_file_name("_kntemp.dat");
208        let mut tmp_file = KeynoteFile::open_keynote_file(&tmp_filepath)?;    
209
210        let mut curr_section_name = String::new();
211            
212        for line in reader.lines() {
213            let line = line.unwrap();                
214            let line = ensure_newline(&line);
215
216            if let Some((k, _)) = KeynoteFile::get_entry_from_string(&line) {
217                // line is an entry, only write if it's not the key we're removing
218                if k != key {
219                    tmp_file.write_all(line.as_bytes())?;                             
220                } 
221                else {
222                    // it is the key we're removing... remove from data structure
223                    if let Some(section) = self.get_section(&curr_section_name) {
224                        section.data.remove(key);                            
225                    }
226                }
227            } else {    // line is a section, write for sure
228                let curr_section_opt = Section::get_section_name_from_string(&line);
229                match curr_section_opt {
230                    Some(v) => curr_section_name = v.to_string(),
231                    None => {                           
232                        return Err("error: file corrupted".into());                            
233                    }
234                };
235
236                tmp_file.write_all(line.as_bytes())?;
237            };                                
238        }
239            
240        // now we need to delete the old file and rename the temp one
241        fs::remove_file(self.filepath.clone())?;
242        fs::rename(tmp_filepath, self.filepath.clone())?;
243        
244        Ok(())
245    }
246
247    /// Remove a section from the file
248    ///
249    /// # Arguments
250    /// 
251    /// * `section_to_remove` - section to remove as string slice  
252    ///
253    /// # Examples    ///
254    /// ```
255    /// use std::fs;
256    /// use keydata::*;
257    /// 
258    /// let mut kn_file = KeynoteFile::new("kntest.dat").unwrap();    
259    /// kn_file.add_section("leaders").unwrap();   
260    /// 
261    /// kn_file.remove_section("leaders");
262    /// 
263    /// fs::remove_file(kn_file.filepath);  // remove the test file  
264    /// ```
265    pub fn remove_section(&mut self, section_to_remove: &str) -> Result<(), Box<dyn Error>> {    
266        let file = KeynoteFile::open_keynote_file(&self.filepath)?;
267        let reader = io::BufReader::new(file);
268            
269        let tmp_filepath = self.filepath.with_file_name("_kntemp.dat");
270        let mut tmp_file = KeynoteFile::open_keynote_file(&tmp_filepath)?;
271
272        let mut writing = true;
273        for line in reader.lines() {
274            let line = line.unwrap();               
275            let line = ensure_newline(&line);
276
277            let section_name = Section::get_section_name_from_string(&line);
278            if let Some(section_name) = section_name {
279                if section_name == section_to_remove {
280                    writing = false;    // found the section to remove, stop copying
281                    continue;
282                }
283            }
284
285            if writing || (!writing &&  Section::get_section_name_from_string(&line).is_some()){
286                // !writing in here means we just found a new section after skipping the last, start writing again
287                if !writing { writing = true; } 
288                tmp_file.write_all(line.as_bytes())?;                    
289            }
290        }
291
292        // now we need to delete the old file and rename the temp one
293        fs::remove_file(self.filepath.clone())?;
294        fs::rename(tmp_filepath, self.filepath.clone())?;        
295        
296        // remove from data structure
297        self.sections.remove(section_to_remove);
298
299        Ok(())
300    }
301    
302    /// Returns a reference to this files sections hashmap    
303    ///
304    /// # Examples    ///
305    /// ```
306    /// use std::fs;
307    /// use keydata::*;
308    /// 
309    /// let mut kn_file = KeynoteFile::new("kntest.dat").unwrap();    
310    /// kn_file.add_section("leaders").unwrap();   
311    /// 
312    /// let sections = kn_file.get_sections();
313    /// 
314    /// fs::remove_file(kn_file.filepath);  // remove the test file  
315    /// ```
316    pub fn get_sections(&self) -> &HashMap<String, Section> {
317        return &self.sections;
318    }
319
320    /// Adds a new section to the file   
321    /// # Arguments
322    /// 
323    /// * `section_name` - name of the section to add
324    /// 
325    /// # Examples    
326    /// ```
327    /// use std::fs;
328    /// use keydata::*;
329    /// 
330    /// let mut kn_file = KeynoteFile::new("kntest.dat").unwrap(); 
331    ///    
332    /// kn_file.add_section("leaders").unwrap();   
333    /// kn_file.add_section("villains").unwrap();  
334    ///     
335    /// fs::remove_file(kn_file.filepath);  // remove the test file 
336    /// ```
337    pub fn add_section(&mut self, section_name : &str) -> Result<(), Box<dyn Error>> {       
338        if !is_alphabetic(section_name) {
339            return Err(format!("'{}' is not a valid section name", section_name).into());            
340        }   
341
342        if let Some(_) = self.get_section(section_name) {
343            return Err("section already exists".into());            
344        }        
345        
346        self.add_section_to_data_structure(section_name);
347    
348        let section_header_str = Section::build_section_string(section_name);
349        let mut file = KeynoteFile::open_keynote_file(&self.filepath)?;
350
351        // write the section header
352        file.write(section_header_str.as_bytes())?;        
353
354        Ok(())
355    }  
356
357    /// Gets the value of an entry in the file from a key   
358    /// # Arguments
359    /// 
360    /// * `key` - key to search the file for
361    /// 
362    /// # Examples    
363    /// ```
364    /// use std::fs;
365    /// use keydata::*;
366    /// 
367    /// let mut kn_file = KeynoteFile::new("kntest.dat").unwrap(); 
368    ///    
369    /// kn_file.add_section("leaders").unwrap();   
370    /// kn_file.add_entry("leaders", "atreides", "leto");
371    /// 
372    /// let value = kn_file.get_value_from_key("atreides");  
373    /// 
374    /// println!("{}", value.unwrap());     // "leto"
375    /// 
376    /// fs::remove_file(kn_file.filepath);  // remove the test file     
377    /// ```
378    pub fn get_value_from_key(&mut self, key: &str) -> Option<&str>{           
379        for (_, section) in &self.sections {
380            if let Some(value) = section.data.get(key) {
381                return Some(value)
382            }
383        } 
384        None
385    }
386    
387    /// Checks if a key is present in the file   
388    /// # Arguments
389    /// 
390    /// * `key` - key to search the file for
391    /// 
392    /// # Examples    
393    /// ```
394    /// use std::fs;
395    /// use keydata::*;
396    /// 
397    /// let mut kn_file = KeynoteFile::new("kntest.dat").unwrap(); 
398    ///    
399    /// kn_file.add_section("leaders").unwrap();   
400    /// kn_file.add_entry("leaders", "atreides", "leto");
401    /// 
402    /// println!("{}", kn_file.contains_key("atreides"));
403    /// 
404    /// 
405    /// fs::remove_file(kn_file.filepath);  // remove the test file     
406    /// ```
407    pub fn contains_key(&mut self, key: &str) -> bool {           
408        for (_, section) in &self.sections {
409            if section.data.contains_key(key) {
410                return true;
411            }
412        }
413        return false
414    }
415
416    /// Returns a Section from the file based on section name   
417    /// # Arguments
418    /// 
419    /// * `section_name` - name of section to return if it exists
420    /// 
421    /// # Examples    
422    /// ```
423    /// use std::fs;
424    /// use keydata::*;
425    /// 
426    /// let mut kn_file = KeynoteFile::new("kntest.dat").unwrap(); 
427    ///    
428    /// kn_file.add_section("leaders").unwrap();
429    /// 
430    /// println!("{}", kn_file.get_section("leaders").unwrap().name);
431    /// 
432    /// 
433    /// fs::remove_file(kn_file.filepath);  // remove the test file     
434    /// ```
435    pub fn get_section(&mut self, section_name : &str) -> Option<&mut Section> {
436        match self.sections.get_mut(section_name) {
437            Some(section) => Some(section),
438            None => None
439        }
440    }
441
442    // ---------------------------------------------------- private functions
443    fn open_keynote_file(filepath : &PathBuf) -> Result<File, Box<dyn Error>>{
444        // obtain the path to the path_buf parent folder
445        let mut folder = filepath.clone();
446        folder.pop();        
447  
448        // if folder doesn't exist, create it
449        if !folder.exists() {
450            fs::create_dir(folder)?;
451        }   
452
453        // open file as append and read, and return
454        let file = OpenOptions::new().append(true).read(true).create(true).open(filepath.as_path())?;     
455     
456        Ok(file)       
457    }
458
459    fn build_entry_string(key: &str, value: &str) -> String {
460        let mut entry: String = String::from("\t<");
461        entry.push_str(key);
462        entry.push('>');
463        entry.push_str(value);
464        entry.push_str("<~>");
465        entry.push('\n');
466
467        entry
468    } 
469
470    fn get_entry_from_string(line: &str) -> Option<(&str, &str)>{
471        if line.starts_with("\t") {
472            if let Some(i) = line.find(">") {
473                let k = &line[2..i];
474                let v = &line[i+1..line.len()-3];
475                return Some((k, v));
476            }
477        }
478        None
479    }    
480    
481    fn add_section_to_data_structure(&mut self, section_name: &str) {
482        self.sections.insert(section_name.to_string(), Section::new(section_name));
483    }
484    
485}
486
487// ---------------------------------------------------- tests
488#[cfg(test)]
489mod tests {
490    use super::*;
491
492    fn get_path_to_test_file() -> PathBuf {
493        let mut data_filepath = match home::home_dir() {
494            Some(path_buffer) => path_buffer,
495            None => panic!("error: unable to find home directory") 
496        };            
497        
498        data_filepath.push(".keynotes/kntest.dat");
499        data_filepath
500    }
501    
502    fn get_path_to_file_in_nonexistant_folder() -> PathBuf {
503        let mut data_filepath = match home::home_dir() {
504            Some(path_buffer) => path_buffer,
505            None => panic!("error: unable to find home directory") 
506        };            
507        
508        data_filepath.push(".keynotes/fakefolder/onemore/kntest.dat");
509
510        data_filepath
511    }
512
513    #[test]
514    fn open_keynote_file_success() {
515        // create test file
516        let path_to_test_file = get_path_to_test_file();        
517
518        let result = KeynoteFile::open_keynote_file(&path_to_test_file);
519
520        assert!(result.is_ok());
521
522        // delete test file
523        fs::remove_file(path_to_test_file).expect("error: unable to remove test file");
524    }
525
526    #[test]
527    #[should_panic]
528    fn open_keynote_file_nonexistant_location() {
529        // create test file
530        let path_to_test_file = get_path_to_file_in_nonexistant_folder();        
531
532        match KeynoteFile::open_keynote_file(&path_to_test_file) {
533            Ok(_) => {  // delete test file
534                fs::remove_file(path_to_test_file).expect("error: unable to remove test file");
535            },
536            Err(e) => {
537                panic!("{}", e.to_string());
538            }
539        };       
540    }
541
542    #[test]
543    fn get_section_success() {
544        // setup
545        let mut test_file = KeynoteFile {
546            filepath : PathBuf::new(), // not used for this test, can leave uninitialized
547            sections : HashMap::new() 
548        };
549        test_file.sections.insert("test_section".to_string(), Section::new("test_section"));
550
551        // execute
552        let section = test_file.get_section("test_section");
553        
554        // assert
555        assert!(section.is_some());
556        assert_eq!(section.unwrap().name, "test_section");        
557    }
558
559    #[test]
560    fn get_section_not_found() {
561        // setup
562        let mut test_file = KeynoteFile {
563            filepath : PathBuf::new(), // not used for this test, can leave uninitialized
564            sections : HashMap::new() 
565        };
566
567        // execute
568        let result = test_file.get_section("nonexistant_section");
569        
570        //assert
571        assert!(result.is_none());
572    }    
573}