pub mod error; use async_std::{ fs::{create_dir, write}, path::Path, sync::{Arc, Mutex}, }; use error::DBError; use rand::{distributions::Alphanumeric, thread_rng, Rng}; use std::{collections::HashMap, fmt}; const DATA: &str = "data"; #[derive(Clone)] enum CacheEntry { Raw(String), } impl CacheEntry { fn entry_type(&self) -> String { match self { CacheEntry::Raw(_) => "raw".to_string(), } } } impl fmt::Display for CacheEntry { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { CacheEntry::Raw(s) => write!(f, "{}", s), } } } #[derive(Clone)] pub struct MoreThanText { cache: Arc>>, dir: String, } impl MoreThanText { pub async fn new(dir: &str) -> Result { let data_dir = Path::new(dir).join(DATA); if !data_dir.is_dir().await { match create_dir(&data_dir).await { Ok(_) => (), Err(err) => { let mut error = DBError::new("failed to create data directory"); error.add_source(err); return Err(error); } } } Ok(Self { cache: Arc::new(Mutex::new(HashMap::new())), dir: data_dir.to_str().unwrap().to_string(), }) } async fn add_entry(&self, entry: CacheEntry) -> String { let id: String = thread_rng().sample_iter(&Alphanumeric).take(32).collect(); let file = Path::new(&self.dir).join(&id); write(file, "a").await.unwrap(); let mut cache = self.cache.lock().await; cache.insert(id.clone(), entry); return id; } async fn get_entry(&self, id: &str) -> Result { let cache = self.cache.lock().await; match cache.get(id) { Some(id) => Ok(id.clone()), None => Err(DBError::new("cache entry not found")), } } async fn update_entry(&self, id: &str, entry: CacheEntry) -> Result<(), DBError> { match self.get_entry(id).await { Ok(_) => (), Err(err) => return Err(err), } let mut cache = self.cache.lock().await; cache.insert(id.to_string(), entry); Ok(()) } } #[cfg(test)] mod setup { use super::*; use tempfile::{tempdir, TempDir}; pub struct MTT { pub db: MoreThanText, pub dir: TempDir, } impl MTT { pub async fn new() -> Self { let dir = tempdir().unwrap(); let db = MoreThanText::new(dir.path().to_str().unwrap()) .await .unwrap(); Self { db: db, dir: dir } } } } #[cfg(test)] mod init { use super::*; use std::error::Error; use tempfile::tempdir; #[async_std::test] async fn create_data_dir() { let dir = tempdir().unwrap(); MoreThanText::new(dir.path().to_str().unwrap()) .await .unwrap(); let data_dir = dir.path().join(DATA); assert!(data_dir.is_dir(), "Did not create the data directory."); dir.close().unwrap(); } #[async_std::test] async fn existing_data_dir() { let dir = tempdir().unwrap(); let data_dir = dir.path().join(DATA); create_dir(data_dir).await.unwrap(); MoreThanText::new(dir.path().to_str().unwrap()) .await .unwrap(); dir.close().unwrap(); } #[async_std::test] async fn bad_data_dir() { match MoreThanText::new("kljsdgfhslkfrh").await { Ok(_) => assert!(false, "This test should fail to create a data directory"), Err(err) => { assert_eq!(err.to_string(), "failed to create data directory"); assert!(err.source().is_some(), "Must include the source error."); } }; } } #[cfg(test)] mod cache { use super::*; use setup::MTT; #[async_std::test] async fn ids_are_random() { let mtt = MTT::new().await; let data1 = CacheEntry::Raw("one".to_string()); let data2 = CacheEntry::Raw("two".to_string()); let id1 = mtt.db.add_entry(data1).await; let id2 = mtt.db.add_entry(data2).await; assert_ne!(id1, id2, "Ids should be unique.") } #[async_std::test] async fn retrieve_cache() { let mtt = MTT::new().await; let data = "something"; let expected = CacheEntry::Raw(data.to_string()); let id = mtt.db.add_entry(expected).await; let output = mtt.db.get_entry(&id).await.unwrap(); assert_eq!(output.to_string(), data); let dfile = mtt.dir.path().join(DATA).join(&id); assert!(dfile.is_file(), "Cache file should exist."); } #[async_std::test] async fn retrieve_bad_id() { let mtt = MTT::new().await; match mtt.db.get_entry(&"Not Valid").await { Ok(_) => assert!(false, "Should have raised an error."), Err(err) => assert_eq!(err.to_string(), "cache entry not found"), } } #[async_std::test] async fn update_cache_entry() { let mtt = MTT::new().await; let id = mtt.db.add_entry(CacheEntry::Raw("same".to_string())).await; let expected = "different"; mtt.db .update_entry(&id, CacheEntry::Raw(expected.to_string())) .await .unwrap(); let output = mtt.db.get_entry(&id).await.unwrap(); assert_eq!(output.to_string(), expected); } #[async_std::test] async fn update_bad_id() { let mtt = MTT::new().await; match mtt .db .update_entry("wilma", CacheEntry::Raw("wrong".to_string())) .await { Ok(_) => assert!(false, "Bad id should raise an error."), Err(err) => assert_eq!(err.to_string(), "cache entry not found"), } } } #[cfg(test)] mod cache_entry { use super::*; #[test] fn raw_type() { let holder = CacheEntry::Raw("nothing important".to_string()); assert_eq!(holder.entry_type(), "raw"); } }