From 3432ea98578b0415f392600f57f36e7132992086 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sun, 22 Nov 2020 19:45:43 +0700 Subject: [PATCH] Add group/file management methods --- src/database/mod.rs | 125 +++++++++++++++++++------ src/database/tests.rs | 209 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 289 insertions(+), 45 deletions(-) diff --git a/src/database/mod.rs b/src/database/mod.rs index 541d991..9fde7d8 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -15,10 +15,14 @@ use custom_error::custom_error; const JSON_POINTER_SEPARATOR: &str = "/"; -custom_error! {DBError - NotDirectory{path: String} = "Path to database should point at the directory: {path}", - NoFile{group_name: String, file_name: String} = r#"There is no "{file_name}" file in group "{group_name}"."#, - IncorrectPointer{pointer: String} = "Incorrect pointer is specified: {pointer}.", +custom_error! { pub DBError + NotDirectory{path: String} = "Path to the database should point at a directory: {path}", + InvalidEntityName{entity_name: String} = r#"Cannot use {entity_name} for file or group"#, + 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}""#, + IncorrectPointer{pointer: String} = "Incorrect pointer is specified: {pointer}", } enum ValueReference<'a> { @@ -100,6 +104,77 @@ impl Database { self.groups.iter().position(|x| x.name.eq(group_name)) } + pub fn contains_group(&self, group_name: &str) -> bool { + self.group_index(group_name).is_some() + } + + pub fn create_group(&mut self, group_name: &str) -> Result<(), DBError> { + verify_name(group_name)?; + if self.group_index(group_name).is_some() { + return Err(DBError::GroupAlreadyExists { + group_name: group_name.to_owned(), + }); + } + self.groups.push(Group { + name: group_name.to_owned(), + files: HashMap::new(), + }); + Ok(()) + } + + pub fn remove_group(&mut self, group_name: &str) -> Result<(), DBError> { + match self.group_index(group_name) { + Some(index) => self.groups.remove(index), + _ => { + return Err(DBError::NoGroup { + group_name: group_name.to_owned(), + }) + } + }; + Ok(()) + } + + pub fn contains_file(&self, (group_name, file_name): FileID) -> bool { + match self.group_index(group_name) { + Some(index) => self.groups[index].files.contains_key(&file_name.to_owned()), + _ => false, + } + } + + pub fn create_file(&mut self, (group_name, file_name): FileID) -> Result<(), DBError> { + let group_name = group_name.to_owned(); + let file_name = file_name.to_owned(); + verify_name(&file_name)?; + let group_index = match self.group_index(&group_name) { + Some(index) => index, + _ => return Err(DBError::NoGroup { group_name }), + }; + if self.groups[group_index].files.contains_key(&file_name) { + return Err(DBError::FileAlreadyExists { + group_name, + file_name, + }); + } + self.groups[group_index].files.insert(file_name, json!({})); + Ok(()) + } + + pub fn remove_file(&mut self, (group_name, file_name): FileID) -> Result<(), DBError> { + let group_name = group_name.to_owned(); + let file_name = file_name.to_owned(); + let group_index = match self.group_index(&group_name) { + Some(index) => index, + _ => return Err(DBError::NoGroup { group_name }), + }; + if self.groups[group_index].files.remove(&file_name).is_none() { + return Err(DBError::NoFile { + group_name, + file_name, + }); + } + Ok(()) + } + fn as_json_mut(&mut self, (group_name, file_name): FileID) -> Option<&mut serde_json::Value> { match self.group_index(group_name) { Some(index) => self.groups[index].files.get_mut(&file_name.to_owned()), @@ -129,7 +204,7 @@ impl Database { self.get_json(file_id, pointer).map(|x| x.to_string()) } - pub fn contains(&self, file_id: FileID, pointer: &str) -> bool { + pub fn contains_value(&self, file_id: FileID, pointer: &str) -> bool { self.get_json(file_id, pointer) != None } @@ -166,35 +241,36 @@ impl Database { &mut self, (group_name, file_name): FileID, pointer: &str, - ) -> Result<(), Box> { + ) -> Option { let file_json = match self.as_json_mut((group_name, file_name)) { Some(file_json) => file_json, - _ => { - return Err(Box::new(DBError::NoFile { - group_name: group_name.to_owned(), - file_name: file_name.to_owned(), - })) - } + _ => return None, }; match pointer_to_reference(file_json, pointer) { - Some(ValueReference::Object(map, variable_name)) => { - map.remove(&variable_name); - } + Some(ValueReference::Object(map, variable_name)) => map.remove(&variable_name), Some(ValueReference::Array(vec, variable_index)) => { if variable_index < vec.len() { - vec.remove(variable_index); + return Some(vec.remove(variable_index)); } + None } - _ => { - return Err(Box::new(DBError::IncorrectPointer { - pointer: pointer.to_owned(), - })) - } - }; - Ok(()) + _ => None, + } } } +fn verify_name(entity_name: &str) -> Result<(), DBError> { + let is_valid = entity_name + .chars() + .all(|x| x.is_ascii_alphabetic() || x.is_ascii_digit()); + if is_valid { + return Ok(()); + } + return Err(DBError::InvalidEntityName { + entity_name: entity_name.to_owned(), + }); +} + fn load_group(group_path: &path::Path) -> Result> { let mut files = HashMap::new(); for entry in fs::read_dir(group_path)? { @@ -293,9 +369,6 @@ fn get_file_name(path: &path::Path) -> String { .unwrap_or_default() .to_string() } -// TODO add tests for remove -// TODO add tests for panics (both add and remove) -// TODO add file addition/removal // TODO add db saving // TODO make sure file's main value not being an object won't break anything diff --git a/src/database/tests.rs b/src/database/tests.rs index 5347949..037b7bf 100644 --- a/src/database/tests.rs +++ b/src/database/tests.rs @@ -7,7 +7,7 @@ const TEST_DB_PATH: &str = "./fixtures/database"; const NO_DB_MESSAGE: &str = "Can not find/load test database"; #[test] -fn group_names() { +fn db_group_names() { let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); let names = db.group_names(); @@ -17,7 +17,7 @@ fn group_names() { } #[test] -fn file_names() { +fn db_file_names() { let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); let names_admin = db.file_names_in("administration"); @@ -29,6 +29,110 @@ fn file_names() { assert_eq!(names_game.len(), 2); } +#[test] +fn db_group_check() { + let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + + assert!(db.contains_group("game")); + assert!(db.contains_group("administration")); + assert!(!db.contains_group("perks")); + assert!(!db.contains_group("7random7")); + assert!(!db.contains_group("")); +} + +#[test] +fn db_group_remove() -> Result<(), Box> { + let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + // Success + db.remove_group("administration")?; + db.remove_group("game")?; + assert!(!db.contains_group("administration")); + assert!(!db.contains_group("game")); + // Failure + db.remove_group("test") + .expect_err("Testing whether removing non-existent groups with incorrect ASCII characters causes errors."); + db.remove_group("administration") + .expect_err("Testing whether removing non-existent groups with incorrect ASCII characters causes errors."); + db.remove_group("game group").expect_err( + "Testing whether removing non-existent groups with whitespace characters causes errors.", + ); + Ok(()) +} + +#[test] +fn db_group_create() -> Result<(), Box> { + let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + // Success + db.create_group("7random7")?; + assert!(db.contains_group("7random7")); + assert!(db.contains_group("game")); + // Failure + db.create_group("my_group").expect_err( + "Testing whether creating groups with incorrect ASCII characters causes errors.", + ); + db.create_group("my group") + .expect_err("Testing whether creating groups with whitespace characters causes errors."); + db.create_group("Š–group") + .expect_err("Testing whether creating groups with non-ASCII characters causes errors."); + // Create after removal + db.remove_group("game")?; + assert!(!db.contains_group("game")); + db.create_group("game")?; + assert!(db.contains_group("game")); + assert!(db.file_names_in("game").is_empty()); + Ok(()) +} + +#[test] +fn db_file_check() { + let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + // Success + assert!(db.contains_file(("administration", "registered"))); + assert!(db.contains_file(("game", "general"))); + assert!(db.contains_file(("game", "perks"))); + // Failure + assert!(!db.contains_file(("game", "perk"))); + assert!(!db.contains_file(("games", "perks"))); + assert!(!db.contains_file(("random", "rnd_file"))); +} + +#[test] +fn db_file_create() -> Result<(), Box> { + let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + // Success + db.create_file(("administration", "secrets"))?; + assert!(db.contains_file(("administration", "secrets"))); + assert_eq!(db.as_string(("administration", "secrets")).unwrap(), "{}"); + assert!(db.contains_file(("game", "perks"))); + // Failure + db.create_file(("administration", "secrets")) + .expect_err("Testing whether creating existing file causes errors."); + db.create_file(("none", "secrets")) + .expect_err("Testing whether creating existing file in non-existent group causes errors."); + db.create_file(("game", "sec_rets")) + .expect_err("Testing whether creating existing file with invalid name causes errors."); + Ok(()) +} + +#[test] +fn db_file_remove() -> Result<(), Box> { + let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + // Success + db.remove_file(("administration", "registered"))?; + assert!(!db.contains_file(("administration", "registered"))); + assert!(db.contains_group("administration")); + db.remove_file(("game", "perks"))?; + assert_eq!(db.file_names_in("game").len(), 1); + // Failure + db.remove_file(("administration", "registered")) + .expect_err("Testing whether removing non-existent files causes errors."); + db.remove_file(("administration", "never")) + .expect_err("Testing whether removing non-existent files causes errors."); + db.remove_file(("never", "file")) + .expect_err("Testing whether removing non-existent files causes errors."); + Ok(()) +} + #[test] fn db_json_contents() { let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); @@ -99,50 +203,117 @@ fn db_contains_check() { let registered_id = ("administration", "registered"); let perks_id = ("game", "perks"); // These exist - assert!(db.contains(registered_id, "/76561198025127722/password_hash")); - assert!(db.contains(registered_id, "/76561198044316328/groups")); - assert!(db.contains(perks_id, "/76561198025127722/headshots")); - assert!(db.contains(perks_id, "/76561198044316328")); + assert!(db.contains_value(registered_id, "/76561198025127722/password_hash")); + assert!(db.contains_value(registered_id, "/76561198044316328/groups/0")); + assert!(db.contains_value(perks_id, "/76561198025127722/headshots")); + assert!(db.contains_value(perks_id, "/76561198044316328")); // These do not exist - assert!(!db.contains(registered_id, "/76561198025127722/password/")); - assert!(!db.contains(registered_id, "/76561198044316328/groups/2")); - assert!(!db.contains(perks_id, "/76561198025127722/assault_rifle_damage/9067")); - assert!(!db.contains(perks_id, "/76561198044316328/headshots")); + assert!(!db.contains_value(registered_id, "/76561198025127722/password/")); + assert!(!db.contains_value(registered_id, "/76561198044316328/groups/2")); + assert!(!db.contains_value(perks_id, "/76561198025127722/assault_rifle_damage/9067")); + assert!(!db.contains_value(perks_id, "/76561198044316328/headshots")); } #[test] fn db_set_success() -> Result<(), Box> { let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); - let file_id = ("administration", "registered"); + let registered_id = ("administration", "registered"); + let general_id = ("game", "general"); // Modify existing - db.set_json(file_id, "/76561198025127722/ip_lock", json!(false))?; + db.set_json(registered_id, "/76561198025127722/ip_lock", json!(false))?; assert_eq!( - db.get_string(file_id, "/76561198025127722/ip_lock") + db.get_string(registered_id, "/76561198025127722/ip_lock") .unwrap(), "false" ); db.set_json( - file_id, + registered_id, "/76561198044316328/password_hash", json!({"var":13524}), )?; assert_eq!( - db.get_string(file_id, "/76561198044316328/password_hash") + db.get_string(registered_id, "/76561198044316328/password_hash") .unwrap(), r#"{"var":13524}"# ); // Reset whole file - db.set_json(file_id, "", json!({}))?; - assert_eq!(db.as_json(file_id).unwrap().to_string(), "{}"); + db.set_json(registered_id, "", json!({}))?; + assert_eq!(db.as_json(registered_id).unwrap().to_string(), "{}"); // Add new values - db.set_json(file_id, "/new_var", json!([42, {"word":"life"}, null]))?; + db.set_json( + registered_id, + "/new_var", + json!([42, {"word":"life"}, null]), + )?; assert_eq!( - db.as_json(file_id).unwrap().to_string(), + db.as_json(registered_id).unwrap().to_string(), r#"{"new_var":[42,{"word":"life"},null]}"# ); + db.set_json( + general_id, + "/76561198025127722/achievements/5", + json!("kf:bugged"), + )?; + assert_eq!( + db.get_string(general_id, "/76561198025127722/achievements") + .unwrap(), + r#"["kf:LabCleaner","kf:ChickenFarmer","scrn:playedscrn",null,null,"kf:bugged"]"# + ); Ok(()) } +#[test] +fn db_set_failure() { + let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + let file_id = ("administration", "registered"); + let imaginary_file_id = ("general", "everything"); + db.set_json(imaginary_file_id, "", json!(null)) + .expect_err("Testing panic at missing file."); + db.set_json(file_id, "/76561198025127722/dir/var", json!(null)) + .expect_err("Testing panic at trying to set a value in non-existing object/array."); + db.set_json(file_id, "/76561198044316328/groups/d", json!(null)) + .expect_err("Testing panic at trying to set a value at non-numeric index in an array."); + db.set_json(file_id, "/76561198044316328/groups/-1", json!(null)) + .expect_err("Testing panic at trying to set a value at negative index in an array."); +} + +#[test] +fn db_remove() { + let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + let file_id = ("administration", "registered"); + // Removing non-existent value + assert_eq!(db.remove(file_id, "/76561198025127722/something"), None); + // Remove simple value + assert_eq!( + db.remove(file_id, "/76561198025127722/password_hash") + .unwrap(), + json!("fce798e0804dfb217f929bdba26745024f37f6b6ba7406f3775176e20dd5089d") + ); + assert!(!db.contains_value(file_id, "/76561198025127722/password_hash")); + // Remove complex value (array) + assert_eq!( + db.remove(file_id, "/76561198044316328/groups").unwrap(), + json!(["admin"]) + ); + assert!(!db.contains_value(file_id, "/76561198044316328/groups/0")); + assert!(!db.contains_value(file_id, "/76561198044316328/groups")); + // Remove array elements + assert_eq!( + db.remove(file_id, "/76561198025127722/allowed_ips/0") + .unwrap(), + json!("127.0.0.1") + ); + assert!(db.contains_value(file_id, "/76561198025127722/allowed_ips/0")); + assert!(!db.contains_value(file_id, "/76561198025127722/allowed_ips/1")); + assert_eq!( + db.remove(file_id, "/76561198025127722/allowed_ips/0") + .unwrap(), + json!("192.168.0.100") + ); + assert!(db.contains_value(file_id, "/76561198025127722/allowed_ips")); + assert!(!db.contains_value(file_id, "/76561198025127722/allowed_ips/0")); +} + #[test] fn test_pop_json_pointer() { assert_eq!(