From 1572d7fb5a40a1a81125ea32f7fa017fe5ba6504 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 21 Nov 2020 23:53:03 +0700 Subject: [PATCH] Implement get/set functionality for database --- src/database/mod.rs | 285 +++++++++++++++++++++++++++++++++--------- src/database/tests.rs | 160 ++++++++++++++++++++++++ src/main.rs | 4 +- 3 files changed, 385 insertions(+), 64 deletions(-) create mode 100644 src/database/tests.rs diff --git a/src/database/mod.rs b/src/database/mod.rs index 43169df..541d991 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,52 +1,48 @@ +#[cfg(test)] +mod tests; + use log::warn; use serde_json; +use serde_json::json; +use std::collections::HashMap; use std::error::Error; -use std::fs; use std::fmt; +use std::fs; use std::path; extern crate custom_error; use custom_error::custom_error; -custom_error! {DatabaseError +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}.", } -struct File { - name: String, - contents: serde_json::Value, +enum ValueReference<'a> { + Object(&'a mut serde_json::Map, String), + Array(&'a mut Vec, usize), } -struct Group { - name: String, - files: Vec, -} +type FileID<'a> = (&'a str, &'a str); -struct Category { +pub struct Group { name: String, - groups: Vec, + files: HashMap, } pub struct Database { - storage: path::PathBuf, - contents: Vec, + storage_path: path::PathBuf, + groups: Vec, } impl fmt::Display for Group { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!(f, " ({})", self.name)?; - for file in self.files.iter() { - writeln!(f, r#" File "{}": {}"#, file.name, file.contents.to_string())?; - } - Ok(()) - } -} - -impl fmt::Display for Category { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, "[{}]", self.name)?; - for g in self.groups.iter() { - write!(f, "{}", g)?; + for (name, contents) in self.files.iter() { + writeln!(f, r#" File "{}": {}"#, name, contents.to_string())?; } Ok(()) } @@ -54,65 +50,153 @@ impl fmt::Display for Category { impl fmt::Display for Database { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!(f, "DB: {}", self.storage.display())?; - for c in self.contents.iter() { - writeln!(f, "{}", c)?; + writeln!(f, "DB: {}", self.storage_path.display())?; + for g in self.groups.iter() { + writeln!(f, "{}", g)?; } Ok(()) } } impl Database { - pub fn new(storage: &path::Path) -> Result> { - if !storage.is_dir() { - return Err(Box::new(DatabaseError::NotDirectory { - path: storage.display().to_string(), + pub fn new(storage_path: &path::Path) -> Result> { + if !storage_path.is_dir() { + return Err(Box::new(DBError::NotDirectory { + path: storage_path.display().to_string(), })); } - let mut contents = Vec::new(); - for entry in fs::read_dir(storage)? { + let mut groups = Vec::new(); + for entry in fs::read_dir(storage_path)? { let entry = entry?; let path = entry.path(); if !path.is_dir() { warn!( - r#"File {} found where only category directories are supposed to be"#, + r#"File {} found where only group directories are supposed to be"#, path.display() ); } else { - let category = load_category(&path)?; - contents.push(category); + let group = load_group(&path)?; + groups.push(group); } } Ok(Database { - storage: storage.to_path_buf(), - contents, + storage_path: storage_path.to_path_buf(), + groups, }) } -} -fn load_category(category_path: &path::Path) -> Result> { - let mut groups = Vec::new(); - for entry in fs::read_dir(category_path)? { - let entry = entry?; - let path = entry.path(); - if !path.is_dir() { - warn!( - r#"File {} found where only group directories are supposed to be"#, - path.display() - ); - } else { - let group = load_group(&path)?; - groups.push(group); + pub fn group_names(&self) -> Vec { + self.groups.iter().map(|x| x.name.clone()).collect() + } + + 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(), + None => Vec::new(), } } - Ok(Category { - name: get_file_name(category_path), - groups, - }) + + fn group_index(&self, group_name: &str) -> Option { + self.groups.iter().position(|x| x.name.eq(group_name)) + } + + 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()), + _ => None, + } + } + + pub fn as_json(&self, (group_name, file_name): FileID) -> Option<&serde_json::Value> { + match self.group_index(group_name) { + Some(index) => self.groups[index].files.get(&file_name.to_owned()), + _ => None, + } + } + + pub fn as_string(&self, file_id: FileID) -> Option { + self.as_json(file_id).map(|x| x.to_string()) + } + + pub fn get_json(&self, file_id: FileID, pointer: &str) -> Option<&serde_json::Value> { + match self.as_json(file_id) { + Some(v) => v.pointer(pointer), + _ => None, + } + } + + pub fn get_string(&self, file_id: FileID, pointer: &str) -> Option { + self.get_json(file_id, pointer).map(|x| x.to_string()) + } + + pub fn contains(&self, file_id: FileID, pointer: &str) -> bool { + self.get_json(file_id, pointer) != None + } + + pub fn set_json( + &mut self, + (group_name, file_name): FileID, + pointer: &str, + new_value: serde_json::Value, + ) -> Result<(), Box> { + 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(), + })) + } + }; + touch(file_json, pointer)?; + match file_json.pointer_mut(pointer) { + Some(v) => *v = new_value, + _ => { + // If after `touch()` call we still don't have an existing value - + // something is wrong with the `pointer` + return Err(Box::new(DBError::IncorrectPointer { + pointer: pointer.to_owned(), + })); + } + }; + Ok(()) + } + + pub fn remove( + &mut self, + (group_name, file_name): FileID, + pointer: &str, + ) -> Result<(), Box> { + 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(), + })) + } + }; + match pointer_to_reference(file_json, pointer) { + 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 Err(Box::new(DBError::IncorrectPointer { + pointer: pointer.to_owned(), + })) + } + }; + Ok(()) + } } fn load_group(group_path: &path::Path) -> Result> { - let mut files = Vec::new(); + let mut files = HashMap::new(); for entry in fs::read_dir(group_path)? { let entry = entry?; let path = entry.path(); @@ -123,10 +207,9 @@ fn load_group(group_path: &path::Path) -> Result> { ); } else { let file_contents = fs::read_to_string(&path)?; - files.push(File { - name: get_file_name(path.as_path()), - contents: serde_json::from_str(&file_contents)?, - }); + let file_name = get_file_name(path.as_path()); + let file_contents = serde_json::from_str(&file_contents)?; + files.insert(file_name, file_contents); } } Ok(Group { @@ -135,9 +218,87 @@ fn load_group(group_path: &path::Path) -> Result> { }) } +fn touch(json_root: &mut serde_json::Value, pointer: &str) -> (Result<(), Box>) { + if pointer.is_empty() || json_root.pointer_mut(pointer).is_some() { + return Ok(()); + } + match pointer_to_reference(json_root, pointer) { + Some(ValueReference::Object(map, variable_name)) => { + map.insert(variable_name, json!(null)); + } + Some(ValueReference::Array(vec, variable_index)) => { + // Since values at the index does not exist - resize will increase the sizeof the array + vec.resize(variable_index + 1, json!(null)); + } + _ => { + return Err(Box::new(DBError::IncorrectPointer { + pointer: pointer.to_owned(), + })) + } + }; + Ok(()) +} + +fn pointer_to_reference<'a>( + json_root: &'a mut serde_json::Value, + pointer: &str, +) -> Option> { + if pointer.is_empty() { + return None; + } + let container_variable_pair = + pop_json_pointer(pointer).and_then(move |(path, variable_name)| { + match json_root.pointer_mut(&path) { + Some(v) => Some((v, variable_name)), + _ => None, + } + }); + let (json_container, variable_name) = match container_variable_pair { + Some(v) => v, + _ => return None, + }; + match json_container { + serde_json::Value::Object(map) => Some(ValueReference::Object(map, variable_name)), + serde_json::Value::Array(vec) => { + let index: usize = match variable_name.parse() { + Ok(v) => v, + _ => return None, + }; + Some(ValueReference::Array(vec, index)) + } + _ => None, + } +} + +fn pop_json_pointer(pointer: &str) -> Option<(String, String)> { + let mut pointer = pointer.to_string(); + let last_separator_index = match pointer.rfind(JSON_POINTER_SEPARATOR) { + Some(v) => v, + _ => { + return None; + } + }; + if last_separator_index >= pointer.len() { + pointer.pop(); + return Some((pointer, String::new())); + } + let var_name = pointer.split_off(last_separator_index + 1); + pointer.pop(); + Some((pointer, var_name)) +} + fn get_file_name(path: &path::Path) -> String { path.file_stem() .and_then(|x| x.to_str()) .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 +// TODO check that file name is appropriate +// TODO handle parsing errors differently +// TODO add logs diff --git a/src/database/tests.rs b/src/database/tests.rs new file mode 100644 index 0000000..5347949 --- /dev/null +++ b/src/database/tests.rs @@ -0,0 +1,160 @@ +use super::*; +use serde_json::json; +use std::path; + +const TEST_DB_PATH: &str = "./fixtures/database"; + +const NO_DB_MESSAGE: &str = "Can not find/load test database"; + +#[test] +fn group_names() { + let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + + let names = db.group_names(); + assert!(names.contains(&"administration".to_owned())); + assert!(names.contains(&"game".to_owned())); + assert_eq!(names.len(), 2); +} + +#[test] +fn file_names() { + let db = Database::new(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"); + assert!(names_admin.contains(&"registered".to_owned())); + assert!(names_game.contains(&"general".to_owned())); + assert!(names_game.contains(&"perks".to_owned())); + assert_eq!(names_admin.len(), 1); + assert_eq!(names_game.len(), 2); +} + +#[test] +fn db_json_contents() { + let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + let registered = db.as_json(("administration", "registered")).unwrap(); + let user_map = registered + .as_object() + .expect("Read value is not an object."); + assert_eq!(user_map.len(), 2); + assert!(user_map.contains_key("76561198025127722")); + let user_record = user_map + .get("76561198044316328") + .unwrap() + .as_object() + .unwrap(); + assert_eq!(user_record.len(), 3); + assert_eq!(user_record.get("ip_lock").unwrap().as_bool(), Some(false)); + assert_eq!( + user_record.get("password_hash").unwrap().as_str(), + Some("fce798e0804dfb217f929bdba26745024f37f6b6ba7406f3775176e20dd5089d") + ); + + let groups_arrays = user_record.get("groups").unwrap().as_array().unwrap(); + assert_eq!(groups_arrays.len(), 1); + assert_eq!(groups_arrays[0], "admin"); +} + +#[test] +fn db_string_contents() { + let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + let general = db.as_string(("game", "general")).unwrap(); + assert!(general.contains(r#""dosh_thrown":527624"#)); + assert!(general + .contains(r#""achievements":["kf:LabCleaner","kf:ChickenFarmer","scrn:playedscrn"]"#)); +} + +#[test] +fn db_json_sub_contents() { + let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + // Test empty path + let file_id = ("administration", "registered"); + assert_eq!(db.as_json(file_id), db.get_json(file_id, "")); + // Test complex path + let received = db + .get_json(file_id, "/76561198025127722/allowed_ips/1") + .unwrap(); + assert_eq!(received.as_str().unwrap(), "192.168.0.100"); + // Test bad paths + assert!(db.get_json(file_id, "/777") == None); + assert!(db.get_json(file_id, "/76561198025127722/allowed_ips/2") == None); +} + +#[test] +fn db_string_sub_contents() { + let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + // Test empty path + let file_id = ("administration", "registered"); + assert_eq!(db.as_string(file_id), db.get_string(file_id, "")); + // Test complex path + let received = db + .get_string(file_id, "/76561198025127722/allowed_ips/0") + .unwrap(); + assert_eq!(received, r#""127.0.0.1""#); +} + +#[test] +fn db_contains_check() { + let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + 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")); + // 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")); +} + +#[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"); + // Modify existing + db.set_json(file_id, "/76561198025127722/ip_lock", json!(false))?; + assert_eq!( + db.get_string(file_id, "/76561198025127722/ip_lock") + .unwrap(), + "false" + ); + db.set_json( + file_id, + "/76561198044316328/password_hash", + json!({"var":13524}), + )?; + assert_eq!( + db.get_string(file_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(), "{}"); + // Add new values + db.set_json(file_id, "/new_var", json!([42, {"word":"life"}, null]))?; + assert_eq!( + db.as_json(file_id).unwrap().to_string(), + r#"{"new_var":[42,{"word":"life"},null]}"# + ); + Ok(()) +} + +#[test] +fn test_pop_json_pointer() { + assert_eq!( + pop_json_pointer("/a/b/c/d"), + Some(("/a/b/c".to_owned(), "d".to_owned())) + ); + assert_eq!(pop_json_pointer("/"), Some(("".to_owned(), "".to_owned()))); + assert_eq!( + pop_json_pointer("/a/b/"), + Some(("/a/b".to_owned(), "".to_owned())) + ); + assert_eq!(pop_json_pointer(""), None); + // This pointer is incorrect + assert_eq!(pop_json_pointer("var"), None); +} diff --git a/src/main.rs b/src/main.rs index 9aff605..5a5bd11 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,8 +5,8 @@ mod database; fn main() { let args: Vec = env::args().collect(); let filename = &args[1]; - let config = database::Database::new(Path::new(filename)); - match config { + let db = database::Database::new(Path::new(filename)); + match db { Ok(db) => print!("{}", db), Err(error) => println!("OH NO: {}", error), }