#[cfg(test)] mod tests; use serde_json; use std::collections::HashMap; use std::error::Error; use std::path; extern crate custom_error; use custom_error::custom_error; pub mod file; pub use file::File; pub mod io; custom_error! { pub DBError 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}""#, } /// 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 { /// 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)?; self.storage_path = new_path.to_path_buf(); 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(()) } /// 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(), None => Vec::new(), } } /// 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)?; self.groups.push(Group { name: group_name.to_owned(), files: HashMap::new(), }); 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)?; if files.contains_key(&file_name.to_owned()) { return Err(DBError::FileAlreadyExists { group_name: group_name.to_owned(), file_name: file_name.to_owned(), }); } let new_file = File::empty(); files.insert(file_name.to_owned(), new_file); Ok(files .get_mut(file_name) .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)? .remove(file_name) .is_none() { return Err(DBError::NoFile { group_name: group_name.to_owned(), file_name: file_name.to_owned(), }); } 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()), _ => None, } } /// 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()), _ => None, } } /// 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 { group_name: group_name.to_owned(), }); } 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), _ => { return Err(DBError::NoGroup { group_name: group_name.to_owned(), }) } } } // 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) } fn group_files(&self, group_name: &str) -> Result<&HashMap, DBError> { let group_index = self.group_index(group_name)?; Ok(&self.groups[group_index].files) } } /// 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(()); } return Err(DBError::InvalidEntityName { entity_name: entity_name.to_owned(), }); }