diff --git a/src/database/mod.rs b/src/database/mod.rs index 08e1b2d..659120b 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -19,35 +19,68 @@ custom_error! { pub DBError NoGroup{group_name: String} = r#"Group "{group_name}" does not exist"#, NoFile{group_name: String, file_name: String} = r#"There is no "{file_name}" file in group "{group_name}""#, GroupAlreadyExists{group_name: String} = r#"Group "{group_name}" already exists"#, - FileAlreadyExists{group_name: String, file_name: String} = r#"File "{file_name}" already exists in the group "{group_name}""#, -} - -pub struct Group { - name: String, - files: HashMap, + FileAlreadyExists{group_name: String, file_name: String} = r#"File "{file_name}" already exists\ + in the group "{group_name}""#, } +/// Avarice database is a collection of named JSON values (by default objects), +/// separated into different names groups. Names of such groups and JSON values must only contain +/// numbers and latin letters (ASCII subset). +/// +/// This database is only supposed to hold a relatively small amount of data +/// that: +/// +/// 1. can be freely and full loaded into memory; +/// 2. then saved all at once. +/// +/// Database is loaded and saved on the disk as a directory, +/// that contains subdirectories (with valid names) for each group; +/// those subdirectories in turn must contain "*.json" files (with valid names) +/// that correspond to the stored JSON values. +/// +/// Database directory should not contain any other files, but their presence +/// should not prevent database from loading (such files should be ignored). pub struct Database { + /// Path to the database's directory storage_path: path::PathBuf, + /// Collection of groups (of JSON values) inside of database groups: Vec, } +/// Represents a database's group: a ste of named files +pub struct Group { + /// Name of the group + name: String, + /// Maps file names with their contents (as file::File structures) + files: HashMap, +} + impl Database { - pub fn new(storage_path: &path::Path) -> Result> { + /// Creates new database by loading it from the specified directory. + /// Directory must contain valid database and be readable. + pub fn load(storage_path: &path::Path) -> Result> { Ok(Database { storage_path: storage_path.to_path_buf(), groups: io::read(storage_path)?, }) } + /// Removes all data from the database. pub fn clear(&mut self) { self.groups = Vec::new(); } + /// Returns path from which this database was loaded (can be changed with `change_path()`). pub fn path(&mut self) -> path::PathBuf { self.storage_path.clone() } + /// Changes current path of this database. All operations with files will use this path, + /// unless stated otherwise. + /// Directory must contain valid database and be readable. + /// + /// This method will also remove all data from the current database's path + /// and fail if it can't. pub fn change_path(&mut self, new_path: &path::Path) -> Result<(), Box> { self.write_copy(new_path)?; io::clear_dir(&self.storage_path)?; @@ -55,26 +88,36 @@ impl Database { Ok(()) } + /// Erases database's data on disk. + pub fn erase(self) -> Result<(), Box> { + io::clear_dir(&self.storage_path)?; + Ok(()) + } + + /// Writes copy of the current database into specified directory. + /// Erases any preexisting database files. + /// + /// Empty group won't be saved. pub fn write_copy(&self, new_path: &path::Path) -> Result<(), Box> { io::clear_dir(new_path)?; io::write(new_path, &self)?; Ok(()) } + /// Writes current state of the database on the disk. + /// + /// Empty group won't be saved. pub fn save(&self) -> Result<(), Box> { self.write_copy(&self.storage_path)?; Ok(()) } - pub fn erase(self) -> Result<(), Box> { - io::clear_dir(&self.storage_path)?; - Ok(()) - } - + /// Returns names of all the groups in the database. pub fn group_names(&self) -> Vec { self.groups.iter().map(|x| x.name.clone()).collect() } + /// Returns names of all the files in a particular group. pub fn file_names_in(&self, group_name: &str) -> Vec { match self.groups.iter().find(|x| x.name.eq(group_name)) { Some(group) => group.files.keys().map(|x| x.clone()).collect(), @@ -82,10 +125,13 @@ impl Database { } } + /// Checks if specified group exists in the database pub fn contains_group(&self, group_name: &str) -> bool { self.group_index(group_name).is_ok() } + /// Creates a new empty group. + /// Will produce error if group already exists. pub fn create_group(&mut self, group_name: &str) -> Result<(), DBError> { assert_name_is_valid(group_name)?; self.assert_no_group(group_name)?; @@ -96,18 +142,23 @@ impl Database { Ok(()) } + /// Removes specified group. + /// Will produce error if group does not exist. pub fn remove_group(&mut self, group_name: &str) -> Result<(), DBError> { let index = self.group_index(group_name)?; self.groups.remove(index); Ok(()) } + /// Checks if specified file (in a specified group) is contained in the database. pub fn contains_file(&self, group_name: &str, file_name: &str) -> bool { self.group_files(group_name) .and_then(|x| Ok(x.contains_key(&file_name.to_owned()))) .unwrap_or(false) } + /// Creates new file in the specified group that will contain an empty JSON object. + /// Will produce error if file already exists. pub fn create_file(&mut self, group_name: &str, file_name: &str) -> Result<&mut File, DBError> { assert_name_is_valid(&file_name)?; let files = self.group_files_mut(group_name)?; @@ -124,6 +175,8 @@ impl Database { .expect("Missing value that was just inserted.")) } + /// Removes specified file (in a specified group). + /// Will produce error if file does not exist. pub fn remove_file(&mut self, group_name: &str, file_name: &str) -> Result<(), DBError> { if self .group_files_mut(group_name)? @@ -138,6 +191,8 @@ impl Database { Ok(()) } + /// Returns immutable reference to the specified file (in a specified group) as `file::File`. + /// `None` if file does not exist. pub fn file_mut(&mut self, group_name: &str, file_name: &str) -> Option<&mut File> { match self.group_files_mut(group_name) { Ok(files) => files.get_mut(&file_name.to_owned()), @@ -145,6 +200,8 @@ impl Database { } } + /// Returns mutable reference to the specified file (in a specified group) as `file::File`. + /// `None` if file does not exist. pub fn file(&self, group_name: &str, file_name: &str) -> Option<&File> { match self.group_files(group_name) { Ok(files) => files.get(&file_name.to_owned()), @@ -152,6 +209,7 @@ impl Database { } } + /// Helper method that raises error if specified group exists. fn assert_no_group(&self, group_name: &str) -> Result<(), DBError> { if self.group_index(group_name).is_ok() { return Err(DBError::GroupAlreadyExists { @@ -161,6 +219,7 @@ impl Database { Ok(()) } + /// Returns current index of the specified group in`groups` vector. fn group_index(&self, group_name: &str) -> Result { match self.groups.iter().position(|x| x.name.eq(group_name)) { Some(index) => Ok(index), @@ -172,6 +231,8 @@ impl Database { } } + // Helper methods that return (im)mutable reference to `HashMap` + // (of 'file_name -> file' map) for a particular group. fn group_files_mut(&mut self, group_name: &str) -> Result<&mut HashMap, DBError> { let group_index = self.group_index(group_name)?; Ok(&mut (&mut self.groups[group_index]).files) @@ -183,12 +244,14 @@ impl Database { } } +/// Name validity check (for groups and files) fn is_name_valid(entity_name: &str) -> bool { entity_name .chars() .all(|x| x.is_ascii_alphabetic() || x.is_ascii_digit()) } +/// Helper function that raises error if passed name is invalid fn assert_name_is_valid(entity_name: &str) -> Result<(), DBError> { if is_name_valid(entity_name) { return Ok(()); @@ -197,8 +260,3 @@ fn assert_name_is_valid(entity_name: &str) -> Result<(), DBError> { entity_name: entity_name.to_owned(), }); } - -// TODO make sure file's main value not being an object won't break anything -// TODO handle parsing errors differently -// TODO add logs -// TODO add docs diff --git a/src/database/tests.rs b/src/database/tests.rs index 3dccc60..974dc99 100644 --- a/src/database/tests.rs +++ b/src/database/tests.rs @@ -21,7 +21,7 @@ impl Drop for TestCleanup { fn prepare_db_copy() -> TestCleanup { clear_test_db(); - let original_db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + let original_db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); original_db .write_copy(path::Path::new(TEST_DB_COPY_PATH)) .expect("Should be able to create a new copy of the fixture database."); @@ -35,13 +35,13 @@ fn clear_test_db() { #[test] fn db_path() { - let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + let mut db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); assert!(db.path() == path::Path::new(TEST_DB_PATH)); } #[test] fn db_clear() { - let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + let mut db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); db.clear(); assert!(!db.contains_group("game")); assert!(!db.contains_file("administration", "registered")); @@ -51,7 +51,7 @@ fn db_clear() { #[test] fn db_group_names() { - let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + let db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); let names = db.group_names(); assert!(names.contains(&"administration".to_owned())); @@ -61,7 +61,7 @@ fn db_group_names() { #[test] fn db_file_names() { - let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + let db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); let names_admin = db.file_names_in("administration"); let names_game = db.file_names_in("game"); @@ -74,7 +74,7 @@ fn db_file_names() { #[test] fn db_group_check() { - let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + let db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); assert!(db.contains_group("game")); assert!(db.contains_group("administration")); @@ -85,7 +85,7 @@ fn db_group_check() { #[test] fn db_group_remove() -> Result<(), Box> { - let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + let mut db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); // Success db.remove_group("administration")?; db.remove_group("game")?; @@ -104,7 +104,7 @@ fn db_group_remove() -> Result<(), Box> { #[test] fn db_group_create() -> Result<(), Box> { - let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + let mut db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); // Success db.create_group("7random7")?; assert!(db.contains_group("7random7")); @@ -128,7 +128,7 @@ fn db_group_create() -> Result<(), Box> { #[test] fn db_file_check() { - let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + let mut db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); // Success assert!(db.contains_file("administration", "registered")); assert!(db.file("game", "general").is_some()); @@ -141,7 +141,7 @@ fn db_file_check() { #[test] fn db_file_create() -> Result<(), Box> { - let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + let mut db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); // Success let file = db.create_file("administration", "secrets")?; assert_eq!(file.to_string(), "{}"); @@ -159,7 +159,7 @@ fn db_file_create() -> Result<(), Box> { #[test] fn db_file_remove() -> Result<(), Box> { - let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + let mut db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); // Success db.remove_file("administration", "registered")?; assert!(!db.contains_file("administration", "registered")); @@ -178,7 +178,7 @@ fn db_file_remove() -> Result<(), Box> { #[test] fn file_json_contents() { - let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + let db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); let registered = db.file("administration", "registered").unwrap().root(); let user_map = registered .as_object() @@ -204,7 +204,7 @@ fn file_json_contents() { #[test] fn file_json_get() { - let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + let db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); // Test empty path let file = db.file("administration", "registered").unwrap(); assert_eq!(file.root(), file.get("").unwrap()); @@ -218,7 +218,7 @@ fn file_json_get() { #[test] fn file_contains_check() { - let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + let db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); let registered_file = db.file("administration", "registered").unwrap(); let perks_file = db.file("game", "perks").unwrap(); // These exist @@ -235,7 +235,7 @@ fn file_contains_check() { #[test] fn db_insert_success() -> Result<(), Box> { - let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + let mut db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); let registered_file = db.file_mut("administration", "registered").unwrap(); // Modify existing registered_file.insert("/76561198025127722/ip_lock", json!(false))?; @@ -277,7 +277,7 @@ fn db_insert_success() -> Result<(), Box> { #[test] fn db_set_failure() { - let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + let mut db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); let file = db.file_mut("administration", "registered").unwrap(); file.insert("/76561198025127722/dir/var", json!(null)) .expect_err("Testing panic at trying to set a value in non-existing object/array."); @@ -289,7 +289,7 @@ fn db_set_failure() { #[test] fn db_remove_value() { - let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + let mut db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); let file = db.file_mut("administration", "registered").unwrap(); // Removing non-existent value assert_eq!(file.remove("/76561198025127722/something"), None); @@ -326,13 +326,13 @@ fn db_remove_value() { fn db_save() { let _cleanup = prepare_db_copy(); // Change something up and save - let mut db = Database::new(path::Path::new(TEST_DB_COPY_PATH)).expect(NO_DB_MESSAGE); + let mut db = Database::load(path::Path::new(TEST_DB_COPY_PATH)).expect(NO_DB_MESSAGE); db.remove_group("administration") .expect(r#"Should be able to remove "administration" group"#); db.save() .expect("Should be able to save copy of the database."); // Reload and check changes - let db = Database::new(path::Path::new(TEST_DB_COPY_PATH)).expect(NO_DB_MESSAGE); + let db = Database::load(path::Path::new(TEST_DB_COPY_PATH)).expect(NO_DB_MESSAGE); assert_eq!(db.group_names().len(), 1); assert_eq!(db.group_names().get(0), Some(&"game".to_owned())); assert_eq!(db.file_names_in("game").len(), 2); @@ -345,7 +345,7 @@ fn db_save() { fn db_change_path() { let _cleanup = prepare_db_copy(); // Change something up and move - let mut db = Database::new(path::Path::new(TEST_DB_COPY_PATH)).expect(NO_DB_MESSAGE); + let mut db = Database::load(path::Path::new(TEST_DB_COPY_PATH)).expect(NO_DB_MESSAGE); db.remove_group("administration") .expect(r#"Should be able to remove "administration" group"#); db.file_mut("game", "perks") @@ -357,7 +357,7 @@ fn db_change_path() { assert!(!path::Path::new(TEST_DB_COPY_PATH).exists()); assert!(path::Path::new(TEST_DB_MOVED_COPY_PATH).exists()); // Reload and check the changes - let db = Database::new(path::Path::new(TEST_DB_MOVED_COPY_PATH)).expect(NO_DB_MESSAGE); + let db = Database::load(path::Path::new(TEST_DB_MOVED_COPY_PATH)).expect(NO_DB_MESSAGE); assert_eq!(db.group_names().len(), 1); assert_eq!(db.group_names().get(0), Some(&"game".to_owned())); assert_eq!(db.file_names_in("game").len(), 2); @@ -373,7 +373,7 @@ fn db_change_path() { #[serial] fn db_erase() { let _cleanup = prepare_db_copy(); - let db = Database::new(path::Path::new(TEST_DB_COPY_PATH)).expect(NO_DB_MESSAGE); + let db = Database::load(path::Path::new(TEST_DB_COPY_PATH)).expect(NO_DB_MESSAGE); db.erase().expect("Should be able to erase data."); assert!(!path::Path::new(TEST_DB_COPY_PATH).exists()); }