#![doc(
html_logo_url = "https://github.com/scOwez/tinydb/raw/master/logo.png",
html_favicon_url = "https://github.com/scOwez/tinydb/raw/master/logo.png"
)]
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::collections::HashSet;
use std::fs::File;
use std::hash;
use std::io::prelude::*;
use std::path::PathBuf;
pub mod error;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Database<T: hash::Hash + Eq> {
pub label: String,
pub save_path: Option<PathBuf>,
pub strict_dupes: bool,
items: HashSet<T>,
}
impl<T: hash::Hash + Eq + Serialize + DeserializeOwned> Database<T> {
pub fn new(label: String, save_path: Option<PathBuf>, strict_dupes: bool) -> Self {
Database {
label: label,
save_path: save_path,
strict_dupes: strict_dupes,
items: HashSet::new(),
}
}
pub fn from(path: PathBuf) -> Result<Self, error::DatabaseError> {
let stream = get_stream_from_path(path)?;
let decoded: Database<T> = bincode::deserialize(&stream[..]).unwrap();
Ok(decoded)
}
pub fn auto_from(path: PathBuf, strict_dupes: bool) -> Result<Self, error::DatabaseError> {
if path.exists() {
Database::from(path)
} else {
let db_name = match path.file_stem() {
Some(x) => match x.to_str() {
Some(y) => String::from(y),
None => return Err(error::DatabaseError::BadDbName),
},
None => return Err(error::DatabaseError::BadDbName),
};
Ok(Database::new(db_name, Some(path), strict_dupes))
}
}
pub fn add_item(&mut self, item: T) -> Result<(), error::DatabaseError> {
if self.strict_dupes {
if self.items.contains(&item) {
return Err(error::DatabaseError::DupeFound);
}
}
self.items.insert(item);
return Ok(());
}
pub fn update_item(&mut self, item: &T, new: T) -> Result<(), error::DatabaseError> {
self.remove_item(item)?;
self.add_item(new)?;
Ok(())
}
pub fn remove_item(&mut self, item: &T) -> Result<(), error::DatabaseError> {
if self.items.remove(item) {
Ok(())
} else {
Err(error::DatabaseError::ItemNotFound)
}
}
pub fn read_db(&self) -> &HashSet<T> {
&self.items
}
pub fn dump_db(&self) -> Result<(), error::DatabaseError> {
let mut dump_file = self.open_db_path()?;
bincode::serialize_into(&mut dump_file, self).unwrap();
Ok(())
}
pub fn query_item<Q: PartialEq, V: Fn(&T) -> &Q>(
&self,
value: V,
query: Q,
) -> Result<&T, error::QueryError> {
for item in self.items.iter() {
if value(item) == &query {
return Ok(item);
}
}
Err(error::QueryError::ItemNotFound)
}
pub fn contains(&self, query: &T) -> bool {
self.items.contains(query)
}
fn open_db_path(&self) -> Result<File, error::DatabaseError> {
let definate_path = self.smart_path_get();
if definate_path.exists() {
io_to_dberror(std::fs::remove_file(&definate_path))?;
}
io_to_dberror(File::create(&definate_path))
}
fn smart_path_get(&self) -> PathBuf {
if self.save_path.is_none() {
return PathBuf::from(format!("{}.tinydb", self.label));
}
PathBuf::from(self.save_path.as_ref().unwrap())
}
}
fn get_stream_from_path(path: PathBuf) -> Result<Vec<u8>, error::DatabaseError> {
if !path.exists() {
return Err(error::DatabaseError::DatabaseNotFound);
}
let mut file = io_to_dberror(File::open(path))?;
let mut buffer = Vec::new();
io_to_dberror(file.read_to_end(&mut buffer))?;
Ok(buffer)
}
fn io_to_dberror<T>(io_res: Result<T, std::io::Error>) -> Result<T, error::DatabaseError> {
match io_res {
Ok(x) => Ok(x),
Err(e) => Err(error::DatabaseError::IOError(e)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Clone, Hash, Eq, PartialEq, Debug, Serialize, Deserialize)]
struct DemoStruct {
name: String,
age: i32,
}
#[test]
fn item_add() -> Result<(), error::DatabaseError> {
let mut my_db = Database::new(String::from("Adding test"), None, true);
my_db.add_item(DemoStruct {
name: String::from("John"),
age: 16,
})?;
Ok(())
}
#[test]
fn item_remove() -> Result<(), error::DatabaseError> {
let mut my_db = Database::new(String::from("Removal test"), None, true);
let testing_struct = DemoStruct {
name: String::from("Xander"),
age: 33,
};
my_db.add_item(testing_struct.clone())?;
my_db.remove_item(&testing_struct)?;
Ok(())
}
#[test]
fn db_dump() -> Result<(), error::DatabaseError> {
let mut my_db = Database::new(
String::from("Dumping test"),
Some(PathBuf::from("test.tinydb")),
true,
);
my_db.add_item(DemoStruct {
name: String::from("Xander"),
age: 33,
})?;
my_db.add_item(DemoStruct {
name: String::from("John"),
age: 54,
})?;
my_db.dump_db()?;
Ok(())
}
#[test]
fn query_item_db() {
let mut my_db = Database::new(
String::from("Query test"),
Some(PathBuf::from("test.tinydb")),
true,
);
my_db
.add_item(DemoStruct {
name: String::from("Rimmer"),
age: 5,
})
.unwrap();
my_db
.add_item(DemoStruct {
name: String::from("Cat"),
age: 10,
})
.unwrap();
my_db
.add_item(DemoStruct {
name: String::from("Kryten"),
age: 3000,
})
.unwrap();
my_db
.add_item(DemoStruct {
name: String::from("Lister"),
age: 62,
})
.unwrap();
assert_eq!(
my_db.query_item(|f| &f.age, 62).unwrap(),
&DemoStruct {
name: String::from("Lister"),
age: 62,
}
); assert_eq!(
my_db.query_item(|f| &f.name, String::from("Cat")).unwrap(),
&DemoStruct {
name: String::from("Cat"),
age: 10,
}
); }
#[test]
fn db_from() -> Result<(), error::DatabaseError> {
db_dump()?;
let my_db: Database<DemoStruct> = Database::from(PathBuf::from("test.tinydb"))?;
assert_eq!(my_db.label, String::from("Dumping test"));
Ok(())
}
#[test]
fn db_contains() {
let exp_struct = DemoStruct {
name: String::from("Xander"),
age: 33,
};
let mut db = Database::new(String::from("Contains example"), None, false);
db.add_item(exp_struct.clone()).unwrap();
assert_eq!(db.contains(&exp_struct), true);
}
#[test]
fn get_all_items() -> Result<(), error::DatabaseError> {
let mut db = Database::new(String::from("Items test"), None, false);
db.add_item(DemoStruct {
name: String::from("Peter"),
age: 56,
})?;
db.add_item(DemoStruct {
name: String::from("Mandy"),
age: 24,
})?;
let exp_set: HashSet<DemoStruct> = vec![
DemoStruct {
name: String::from("Peter"),
age: 56,
},
DemoStruct {
name: String::from("Mandy"),
age: 24,
},
]
.iter()
.cloned()
.collect();
assert_eq!(db.read_db(), &exp_set);
Ok(())
}
#[test]
fn auto_from_creation() {
let _dummy_db: Database<DemoStruct> =
Database::new(String::from("alreadyexists"), None, false);
let from_db_path = PathBuf::from("alreadyexists.tinydb");
let _from_db: Database<DemoStruct> = Database::auto_from(from_db_path, false).unwrap();
let new_db_path = PathBuf::from("nonexistant.tinydb");
let _net_db: Database<DemoStruct> = Database::auto_from(new_db_path, false).unwrap();
}
}