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}