diff --git a/Cargo.toml b/Cargo.toml index c2e1dae..53950ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cargobase" -version = "0.1.0" +version = "0.1.1" edition = "2021" [dependencies] @@ -8,7 +8,7 @@ serde = { version = "1.0.215", features = ["derive"] } serde_json = { version = "1.0.132", features = ["raw_value"] } serde_derive = "1.0.188" base64 = "0.22.1" -tokio = { version = "1", features = ["full"] } +tokio = { version = "1", features = ["full"], optional = true} uuid = {version ="1.11.0", features = ["v4"] } thiserror = "2.0.3" tempfile = "3.14.0" @@ -17,3 +17,9 @@ tracing = "0.1" tracing-subscriber = "0.3" tracing-test = "0.2.5" +[features] +default = ["sync", "async"] # for development +# default = ["sync"] +sync = [] # synchronous features only +async = ["tokio"] # asynchronous features only +full = ["sync", "async"] # all features diff --git a/TestUpdateAndDelete.json b/TestUpdateAndDelete.json deleted file mode 100644 index 72f498a..0000000 --- a/TestUpdateAndDelete.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "name": "TestUpdateAndDelete", - "file_name": "TestUpdateAndDelete.json", - "tables": [ - { - "name": "TestTracingInfo", - "rows": [ - { - "_id": "6efca8a7-3ac5-473f-afa9-7add8365388e", - "data": { - "email": "jondoe@email.com", - "id": "58409c6a-2129-4528-a94f-71685fdfa3c3", - "name": "Jon Doe" - } - }, - { - "_id": "68403104-e228-46ee-9308-9a2013e6c0f8", - "data": { - "email": "janedoe@email.com", - "id": "4bf896a0-f807-47a3-b2fe-6a5453f21578", - "name": "Jane Doe" - } - }, - { - "_id": "8cdefe82-bd2f-4ea5-bacf-ce060df7f794", - "data": { - "email": null, - "id": "c0c0200d-7dc9-4881-9839-b83f6cf4f82a", - "name": "alice cooper" - } - }, - { - "_id": "a2c340db-6f29-42c5-be6c-460cb6877dbe", - "data": { - "email": "jondoe@email.com", - "id": "bf962a49-7a58-43e1-bd63-cd810050194c", - "name": "Jon Doe" - } - }, - { - "_id": "9d271fa4-6a11-43ff-87fb-b8bc0bc18a72", - "data": { - "email": "janedoe@email.com", - "id": "1c6af75d-124c-4db0-b0cf-8c199dedf246", - "name": "Jane Doe" - } - } - ], - "columns": [ - { - "name": "id", - "required": true - }, - { - "name": "name", - "required": true - }, - { - "name": "email", - "required": true - } - ] - } - ] -} \ No newline at end of file diff --git a/src/cargobase/database.rs b/src/cargobase/database.rs index 98c0c2d..b6f8dd0 100644 --- a/src/cargobase/database.rs +++ b/src/cargobase/database.rs @@ -17,9 +17,6 @@ impl Database { let name = name.to_string(); let file_name = format!("{name}.json"); - // find a better way of logging this information for the end user - // -- they might not have tracing enabled - if std::path::Path::new(&file_name).exists() { tracing::info!("Database already exists: {name}, loading database"); @@ -44,15 +41,42 @@ impl Database { } } + #[cfg(feature = "async")] + pub async fn new_async(name: &str) -> Self { + let name = name.to_string(); + let file_name = format!("{name}.json"); + + if tokio::fs::metadata(&file_name).await.is_ok() { + tracing::info!("Database already exists: {name}, loading database"); + + // Load the database from the file + match Database::load_from_file_async(&file_name).await { + Ok(db) => return db, + Err(e) => { + tracing::error!("Failed to load database from file: {file_name}, error: {e}"); + } + } + } else { + tracing::info!("Creating new database: {file_name}"); + // Create an empty JSON file for the new database + if let Err(e) = tokio::fs::write(&file_name, "{}").await { + tracing::error!("Failed to create database file: {e}"); + } + } + + Database { + name, + file_name, + tables: Vec::new(), + } + } + pub fn drop_database(&self) -> Result<(), DatabaseError> { if std::fs::remove_file(&self.file_name).is_err() { tracing::error!( "{}", DatabaseError::DeleteError("Failed to delete database file".to_string()) ); - - // should we crash the program? - // return Err(DatabaseError::DeleteError("Failed to delete database file".to_string(),)); } tracing::info!("Database `{}` dropped successfully", self.name); @@ -98,10 +122,26 @@ impl Database { Ok(()) } + #[cfg(feature = "async")] + pub(crate) async fn save_to_file_async(&self) -> Result<(), tokio::io::Error> { + let json_data = serde_json::to_string_pretty(&self)?; + tokio::fs::write(&self.file_name, json_data).await?; + tracing::info!("Database saved to file: {}", self.file_name); + Ok(()) + } + pub(crate) fn load_from_file(file_name: &str) -> Result { let json_data = std::fs::read_to_string(file_name)?; let db: Database = serde_json::from_str(&json_data)?; - tracing::info!("Database loaded from file: {}", file_name); // needed? + tracing::info!("Database loaded from file: {}", file_name); + Ok(db) + } + + #[cfg(feature = "async")] + pub(crate) async fn load_from_file_async(file_name: &str) -> Result { + let json_data = tokio::fs::read_to_string(file_name).await?; + let db: Database = serde_json::from_str(&json_data)?; + tracing::info!("Database loaded from file: {}", file_name); Ok(db) } @@ -178,6 +218,9 @@ mod tests { use crate::cargobase::setup_temp_db; use crate::{Columns, Table}; + #[cfg(feature = "async")] + use crate::cargobase::setup_temp_db_async; + #[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Default)] struct TestData { id: String, @@ -196,6 +239,19 @@ mod tests { assert_eq!(db.tables.len(), 1); // the setup_temp_db function adds a table } + #[cfg(feature = "async")] + #[tokio::test] + async fn test_database_new_async() { + let db = setup_temp_db_async().await; + + let db_name = &db.name.to_string(); + let fnn = format!("{db_name}.json"); + + assert_eq!(db.name, db_name.to_string()); + assert_eq!(db.file_name, fnn); + assert_eq!(db.tables.len(), 1); // the setup_temp_db function adds a table + } + #[test] fn test_drop_database() { let db = setup_temp_db(); @@ -271,4 +327,67 @@ mod tests { // Ensure no tables were removed assert_eq!(db.tables.len(), 1); } + + #[test] + fn test_save_to_file() { + let db = setup_temp_db(); + let result = db.save_to_file(); + + assert!(result.is_ok()); + assert!(std::path::Path::new(&db.file_name).exists()); + } + + #[test] + fn test_load_from_file() { + let db = setup_temp_db(); + let loaded_db = Database::load_from_file(&db.file_name).expect("Failed to load database"); + + assert_eq!(db, loaded_db); + } + + #[cfg(feature = "async")] + #[tokio::test] + async fn test_save_to_file_async() { + use tempfile::NamedTempFile; + + let temp_file = NamedTempFile::new().expect("Failed to create a temporary file"); + let db_path = temp_file.path().to_str().unwrap().to_string(); + + let db = Database { + name: "test_db".to_string(), + file_name: db_path.clone(), + tables: vec![], + }; + + db.save_to_file_async() + .await + .expect("Failed to save database"); + let loaded_db = Database::load_from_file(&db_path).expect("Failed to load database"); + assert_eq!(db, loaded_db); + } + + #[cfg(feature = "async")] + #[tokio::test] + async fn test_load_from_file_async() { + use tempfile::NamedTempFile; + + let temp_file = NamedTempFile::new().expect("Failed to create a temporary file"); + let db_path = temp_file.path().to_str().unwrap().to_string(); + + let db = Database { + name: "test_db".to_string(), + file_name: db_path.to_string(), + tables: vec![], + }; + + db.save_to_file_async() + .await + .expect("Failed to save database"); + + let loaded_db = Database::load_from_file_async(&db_path) + .await + .expect("Failed to load database"); + + assert_eq!(db, loaded_db); + } } diff --git a/src/cargobase/errors/errors.rs b/src/cargobase/errors/errors.rs index b42c0e9..9155863 100644 --- a/src/cargobase/errors/errors.rs +++ b/src/cargobase/errors/errors.rs @@ -32,4 +32,11 @@ pub enum DatabaseError { #[error("")] // could expand to specify serialization/deserialization error JSONError(#[from] serde_json::Error), + + // #[error("IO error: `{0}`")] + // Io(#[from] std::io::Error), + // + // #[cfg(feature = "async")] + // #[error("Tokio IO error: `{0}`")] + // TokioIo(#[from] tokio::io::Error), } diff --git a/src/cargobase/mod.rs b/src/cargobase/mod.rs index eeb1d03..edf3740 100644 --- a/src/cargobase/mod.rs +++ b/src/cargobase/mod.rs @@ -1,17 +1,20 @@ pub mod columns; pub mod database; +pub mod errors; pub mod query; pub mod row; pub mod table; pub mod util; -pub mod errors; pub mod view; pub use columns::{Column, Columns}; pub use database::Database; +pub use errors::errors::DatabaseError; pub use query::Query; pub use row::Row; pub use table::Table; pub use util::setup_temp_db; -pub use errors::errors::DatabaseError; pub use view::View; + +#[cfg(feature = "async")] +pub use util::setup_temp_db_async; diff --git a/src/cargobase/util.rs b/src/cargobase/util.rs index b4435ca..2bb63f2 100644 --- a/src/cargobase/util.rs +++ b/src/cargobase/util.rs @@ -1,9 +1,11 @@ use serde::{Deserialize, Serialize}; use tempfile::NamedTempFile; -use tracing_subscriber::fmt; use super::{Columns, Database, Table}; +// #[cfg(feature = "async")] +// use tokio::fs; + #[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Default)] struct TestData { id: String, @@ -25,13 +27,84 @@ pub fn setup_temp_db() -> Database { db } -pub fn init_tracing() { - let subscriber = fmt::Subscriber::builder() - .with_max_level(tracing::Level::WARN) - .finish(); - /* - example implementation: - info!(target: "cargobase", "Database `{name}` already exists, loading..."); - */ - tracing::subscriber::set_global_default(subscriber).expect("Failed to set subscriber"); +#[cfg(feature = "async")] +pub async fn setup_temp_db_async() -> Database { + let temp_file = NamedTempFile::new().expect("Failed to create a temporary file"); + let db_path = temp_file.path().to_str().unwrap().to_string(); + + // Initialize the test database + let mut db = Database::new_async(&db_path).await; + let test_columns = Columns::from_struct::(true); + + let mut table = Table::new("TestTable".to_string(), test_columns); + db.add_table(&mut table).unwrap(); + + db.save_to_file_async() + .await + .expect("Failed to save database"); + + db +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + #[test] + fn test_setup_temp_db() { + let db = setup_temp_db(); + assert_eq!(db.tables.len(), 1); + assert_eq!(db.tables[0].name, "TestTable"); + } + + #[test] + fn test_temp_file_cleanup() { + // Create a temporary database + let temp_file = NamedTempFile::new().expect("Failed to create a temporary file"); + let db_path = temp_file.path().to_str().unwrap().to_string(); + + // Drop the file explicitly by dropping the `NamedTempFile` instance + drop(temp_file); + + // Verify that the temporary file is removed + let file_exists = fs::metadata(&db_path).is_ok(); + assert!( + !file_exists, + "Temporary file `{}` should have been removed after being dropped", + db_path + ); + } + + #[cfg(feature = "async")] + #[tokio::test] + async fn test_setup_temp_db_async() { + let db = setup_temp_db_async().await; + assert_eq!(db.tables.len(), 1); + assert_eq!(db.tables[0].name, "TestTable"); + } + + #[cfg(feature = "async")] + #[tokio::test] + async fn test_temp_file_cleanup_async() { + // Create a temporary database + let temp_file = tempfile::Builder::new() + .prefix("test_db") + .suffix(".json") + .tempfile() + .expect("Failed to create a temporary file"); + + let db_path = temp_file.path().to_str().unwrap().to_string(); + + // Drop the file explicitly by dropping the `NamedTempFile` instance + drop(temp_file); + + // Verify that the temporary file is removed + let file_exists = fs::metadata(&db_path).is_ok(); + assert!( + !file_exists, + "Temporary file `{}` should have been removed after being dropped", + db_path + ); + } } diff --git a/src/lib.rs b/src/lib.rs index f42444a..22cd8dc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,3 @@ pub mod cargobase; -pub use cargobase::{Columns, Column, Row, Database, Table, util::init_tracing}; +pub use cargobase::{Column, Columns, Database, Row, Table};