use async_std::{fs::{read, remove_file, write}, path::PathBuf}; use super::{DataType, DBError, ErrorCode, FileData, SessionData}; use std::{cell::Cell, time::{Duration, Instant}}; pub struct Entry { data: DataType, filename: PathBuf, last_used: Cell, } impl Entry { pub async fn new

(filename: P, data: DataType) -> Result where P: Into, { let pathbuf = filename.into(); if pathbuf.as_path().exists().await { return Err(DBError::from_code(ErrorCode::EntryExists(pathbuf))); } else { match write(&pathbuf, data.to_bytes()).await { Ok(_) => (), Err(err) => { let mut error = DBError::from_code(ErrorCode::EntryWriteFailure(pathbuf)); error.add_source(err); return Err(error); } } } Ok(Self { data: data, filename: pathbuf, last_used: Cell::new(Instant::now()), }) } pub async fn get

(filename: P) -> Result where P: Into, { let pathbuf = filename.into(); let content = match read(&pathbuf).await { Ok(text) => text, Err(err) => { let mut error = DBError::from_code(ErrorCode::EntryReadFailure(pathbuf)); error.add_source(err); return Err(error); } }; let data = match DataType::from_bytes(&mut content.iter()) { Ok(raw) => raw, Err(err) => { let mut error = DBError::from_code(ErrorCode::EntryReadFailure(pathbuf)); error.add_source(err); return Err(error); } }; Ok(Self { data: data, filename: pathbuf, last_used: Cell::new(Instant::now()), }) } pub fn elapsed(&self) -> Duration { self.last_used.get().elapsed() } pub fn data(&self) -> DataType { self.last_used.set(Instant::now()); self.data.clone() } async fn update(&mut self, data: DataType) -> Result<(), DBError> { self.last_used.set(Instant::now()); match write(&self.filename, data.to_bytes()).await { Ok(_) => (), Err(err) => { let mut error = DBError::from_code(ErrorCode::EntryWriteFailure(self.filename.clone())); error.add_source(err); return Err(error); } }; self.data = data; Ok(()) } async fn remove(&self) -> Result<(), DBError> { match remove_file(&self.filename).await { Ok(_) => Ok(()), Err(err) => { let mut error = DBError::from_code(ErrorCode::EntryDeleteFailure(self.filename.clone())); error.add_source(err); Err(error) } } } } #[cfg(test)] mod entry { use super::*; use std::error::Error; use tempfile::tempdir; #[async_std::test] async fn get_elapsed_time() { let dir = tempdir().unwrap(); let data = DataType::new("store").unwrap(); let filepath = dir.path().join("count"); let filename = filepath.to_str().unwrap(); let item = Entry::new(filename.to_string(), data).await.unwrap(); assert!( Duration::from_secs(1) > item.elapsed(), "last_used should have been now." ); item.last_used .set(Instant::now() - Duration::from_secs(500)); assert!( Duration::from_secs(499) < item.elapsed(), "The duration should have increased." ); } #[async_std::test] async fn create() { let dir = tempdir().unwrap(); let mut data = DataType::new("store").unwrap(); data.add("database", "roger", "moore").unwrap(); let filepath = dir.path().join("wiliam"); let filename = filepath.to_str().unwrap(); let item = Entry::new(filename.to_string(), data.clone()) .await .unwrap(); assert!( Duration::from_secs(1) > item.elapsed(), "last_used should have been now." ); let output = item.data(); assert_eq!( data.list(["database"].to_vec()).unwrap(), output.list(["database"].to_vec()).unwrap() ); assert!(filepath.is_file(), "Should have created the entry file."); let content = read(&filepath).await.unwrap(); assert_eq!(content, data.to_bytes()); } #[async_std::test] async fn create_errors_on_bad_files() -> Result<(), DBError> { let dir = tempdir().unwrap(); let data = DataType::new("store").unwrap(); let filepath = dir.path().join("bad").join("path"); let filename = filepath.to_str().unwrap(); match Entry::new(filename.to_string(), data).await { Ok(_) => Err(DBError::new("bad file names should raise an error")), Err(err) => match err.code { ErrorCode::EntryWriteFailure(_) => { assert!(err.source().is_some(), "Must include the source error."); assert!(err .source() .unwrap() .to_string() .contains("could not write to file")); Ok(()) } _ => Err(DBError::new("incorrect error code")), }, } } #[async_std::test] async fn create_does_not_over_writes() -> Result<(), DBError> { let dir = tempdir().unwrap(); let id = "wicked"; let file = dir.path().join(id); let filename = file.to_str().unwrap(); write(&file, b"previous").await.unwrap(); let data = DataType::new("store").unwrap(); match Entry::new(filename.to_string(), data).await { Ok(_) => { return Err(DBError::new( "Should produce an error for an existing Entry", )) } Err(err) => match err.code { ErrorCode::EntryExists(_) => Ok(()), _ => Err(DBError::new("incorrect error code")), }, } } #[async_std::test] async fn get_updates_last_used() { let dir = tempdir().unwrap(); let data = DataType::new("store").unwrap(); let filepath = dir.path().join("holder"); let filename = filepath.to_str().unwrap(); let item = Entry::new(filename.to_string(), data).await.unwrap(); item.last_used .set(Instant::now() - Duration::from_secs(300)); item.data(); assert!( Duration::from_secs(1) > item.elapsed(), "last_used should have been reset." ); } #[async_std::test] async fn update_entry() { let dir = tempdir().unwrap(); let mut data = DataType::new("store").unwrap(); let filepath = dir.path().join("changing"); let filename = filepath.to_str().unwrap(); let mut item = Entry::new(filename.to_string(), data.clone()) .await .unwrap(); item.last_used .set(Instant::now() - Duration::from_secs(500)); data.add("database", "new", "stuff").unwrap(); item.update(data.clone()).await.unwrap(); assert!( Duration::from_secs(1) > item.elapsed(), "last_used should have been reset." ); let output = item.data(); assert_eq!( data.list(["database"].to_vec()).unwrap(), output.list(["database"].to_vec()).unwrap() ); let content = read(&filepath).await.unwrap(); assert_eq!(content, data.to_bytes()); } #[async_std::test] async fn update_write_errors() -> Result<(), DBError> { let dir = tempdir().unwrap(); let data = DataType::new("store").unwrap(); let filepath = dir.path().join("changing"); let filename = filepath.to_str().unwrap(); let mut item = Entry::new(filename.to_string(), data.clone()) .await .unwrap(); drop(dir); match item.update(data).await { Ok(_) => Err(DBError::new("file writes should return an error")), Err(err) => match err.code { ErrorCode::EntryWriteFailure(_) => { assert!(err.source().is_some(), "Must include the source error."); assert!(err .source() .unwrap() .to_string() .contains("could not write to file")); Ok(()) } _ => Err(DBError::new("incorrect error code")), }, } } #[async_std::test] async fn retrieve() { let dir = tempdir().unwrap(); let mut data = DataType::new("store").unwrap(); data.add("database", "something_old", "3.14159").unwrap(); let filepath = dir.path().join("existing"); let filename = filepath.to_str().unwrap(); let item = Entry::new(filename.to_string(), data.clone()) .await .unwrap(); let output = Entry::get(filename).await.unwrap(); assert_eq!( output.data().list(["database"].to_vec()).unwrap(), data.list(["database"].to_vec()).unwrap() ); assert_eq!(output.filename.to_str().unwrap(), filename); assert!( Duration::from_secs(1) > item.elapsed(), "last_used should have been reset." ); } #[async_std::test] async fn retrieve_file_missing() -> Result<(), DBError> { let dir = tempdir().unwrap(); let filepath = dir.path().join("justnotthere"); let filename = filepath.to_str().unwrap(); match Entry::get(filename).await { Ok(_) => Err(DBError::new("should have returned an error")), Err(err) => match err.code { ErrorCode::EntryReadFailure(_) => { assert!(err.source().is_some(), "Error should have a source."); assert!( err.source() .unwrap() .to_string() .contains("could not read file"), "Source Error Message: {}", err.source().unwrap().to_string() ); Ok(()) } _ => Err(DBError::new("incorrect error code")), }, } } #[async_std::test] async fn retrieve_corrupt_file() -> Result<(), DBError> { let dir = tempdir().unwrap(); let filepath = dir.path().join("garbage"); let filename = filepath.to_str().unwrap(); write(&filepath, b"jhsdfghlsdf").await.unwrap(); match Entry::get(filename).await { Ok(_) => Err(DBError::new("should have returned an error")), Err(err) => match err.code { ErrorCode::EntryReadFailure(_) => { assert!(err.source().is_some(), "Error should have a source."); assert!( err.source().unwrap().to_string().contains("corrupt file"), "Source Error Message: {}", err.source().unwrap().to_string() ); Ok(()) } _ => Err(DBError::new("incorrect error code")), }, } } #[async_std::test] async fn delete() { let dir = tempdir().unwrap(); let filepath = dir.path().join("byebye"); let filename = filepath.to_str().unwrap(); let data = DataType::new("store").unwrap(); let item = Entry::new(filename.to_string(), data.clone()) .await .unwrap(); item.remove().await.unwrap(); assert!(!filepath.exists(), "Entry file should be removed."); } #[async_std::test] async fn delete_bad_file() -> Result<(), DBError> { let dir = tempdir().unwrap(); let filepath = dir.path().join("itsnotthere"); let filename = filepath.to_str().unwrap(); let data = DataType::new("store").unwrap(); let item = Entry::new(filename.to_string(), data.clone()) .await .unwrap(); remove_file(filename).await.unwrap(); match item.remove().await { Ok(_) => Err(DBError::new("should have produced an error")), Err(err) => match err.code { ErrorCode::EntryDeleteFailure(_) => { assert!(err.source().is_some(), "Error should have a source."); assert!( err.source() .unwrap() .to_string() .contains("could not remove file"), "Source Error Message: {}", err.source().unwrap().to_string() ); Ok(()) } _ => Err(DBError::new("incorrect error code")), }, } } }