Implement get/set functionality for database
This commit is contained in:
		
							parent
							
								
									185f7ced8d
								
							
						
					
					
						commit
						1572d7fb5a
					
				| @ -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, serde_json::Value>, String), | ||||
|     Array(&'a mut Vec<serde_json::Value>, usize), | ||||
| } | ||||
| 
 | ||||
| struct Group { | ||||
|     name: String, | ||||
|     files: Vec<File>, | ||||
| } | ||||
| type FileID<'a> = (&'a str, &'a str); | ||||
| 
 | ||||
| struct Category { | ||||
| pub struct Group { | ||||
|     name: String, | ||||
|     groups: Vec<Group>, | ||||
|     files: HashMap<String, serde_json::Value>, | ||||
| } | ||||
| 
 | ||||
| pub struct Database { | ||||
|     storage: path::PathBuf, | ||||
|     contents: Vec<Category>, | ||||
|     storage_path: path::PathBuf, | ||||
|     groups: Vec<Group>, | ||||
| } | ||||
| 
 | ||||
| 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,45 +50,23 @@ 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<Database, Box<dyn Error>> { | ||||
|         if !storage.is_dir() { | ||||
|             return Err(Box::new(DatabaseError::NotDirectory { | ||||
|                 path: storage.display().to_string(), | ||||
|     pub fn new(storage_path: &path::Path) -> Result<Database, Box<dyn Error>> { | ||||
|         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 entry = entry?; | ||||
|             let path = entry.path(); | ||||
|             if !path.is_dir() { | ||||
|                 warn!( | ||||
|                     r#"File {} found where only category directories are supposed to be"#, | ||||
|                     path.display() | ||||
|                 ); | ||||
|             } else { | ||||
|                 let category = load_category(&path)?; | ||||
|                 contents.push(category); | ||||
|             } | ||||
|         } | ||||
|         Ok(Database { | ||||
|             storage: storage.to_path_buf(), | ||||
|             contents, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| fn load_category(category_path: &path::Path) -> Result<Category, Box<dyn Error>> { | ||||
|         let mut groups = Vec::new(); | ||||
|     for entry in fs::read_dir(category_path)? { | ||||
|         for entry in fs::read_dir(storage_path)? { | ||||
|             let entry = entry?; | ||||
|             let path = entry.path(); | ||||
|             if !path.is_dir() { | ||||
| @ -105,14 +79,124 @@ fn load_category(category_path: &path::Path) -> Result<Category, Box<dyn Error>> | ||||
|                 groups.push(group); | ||||
|             } | ||||
|         } | ||||
|     Ok(Category { | ||||
|         name: get_file_name(category_path), | ||||
|         Ok(Database { | ||||
|             storage_path: storage_path.to_path_buf(), | ||||
|             groups, | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     pub fn group_names(&self) -> Vec<String> { | ||||
|         self.groups.iter().map(|x| x.name.clone()).collect() | ||||
|     } | ||||
| 
 | ||||
|     pub fn file_names_in(&self, group_name: &str) -> Vec<String> { | ||||
|         match self.groups.iter().find(|x| x.name.eq(group_name)) { | ||||
|             Some(group) => group.files.keys().map(|x| x.clone()).collect(), | ||||
|             None => Vec::new(), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn group_index(&self, group_name: &str) -> Option<usize> { | ||||
|         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<String> { | ||||
|         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<String> { | ||||
|         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<dyn Error>> { | ||||
|         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<dyn Error>> { | ||||
|         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<Group, Box<dyn Error>> { | ||||
|     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<Group, Box<dyn Error>> { | ||||
|             ); | ||||
|         } 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<Group, Box<dyn Error>> { | ||||
|     }) | ||||
| } | ||||
| 
 | ||||
| fn touch(json_root: &mut serde_json::Value, pointer: &str) -> (Result<(), Box<dyn Error>>) { | ||||
|     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<ValueReference<'a>> { | ||||
|     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
 | ||||
|  | ||||
							
								
								
									
										160
									
								
								src/database/tests.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								src/database/tests.rs
									
									
									
									
									
										Normal file
									
								
							| @ -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<dyn Error>> { | ||||
|     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); | ||||
| } | ||||
| @ -5,8 +5,8 @@ mod database; | ||||
| fn main() { | ||||
|     let args: Vec<String> = 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), | ||||
|     } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user