From 6b10385c0d7723f5480a71975c1fe4ba79375aa6 Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Sat, 22 Apr 2023 14:25:02 +0100 Subject: [PATCH] feat: implement bots including tests this starts work on #78 --- Cargo.lock | 1 + crates/core/database/Cargo.toml | 5 +- crates/core/database/src/drivers/mongodb.rs | 35 ++++-- crates/core/database/src/lib.rs | 18 +++- crates/core/database/src/models/bots/mod.rs | 4 +- crates/core/database/src/models/bots/model.rs | 58 +++++++++- crates/core/database/src/models/bots/ops.rs | 13 ++- .../database/src/models/bots/ops/mongodb.rs | 101 +++++++++++++++++- .../database/src/models/bots/ops/reference.rs | 79 +++++++++++++- crates/core/database/src/models/mod.rs | 6 +- crates/core/result/src/lib.rs | 22 +++- 11 files changed, 311 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d7f8209..24e4e775 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2836,6 +2836,7 @@ dependencies = [ "mongodb", "nanoid", "optional_struct", + "revolt-result", "serde", "serde_json", ] diff --git a/crates/core/database/Cargo.toml b/crates/core/database/Cargo.toml index f60154e8..b17ddb45 100644 --- a/crates/core/database/Cargo.toml +++ b/crates/core/database/Cargo.toml @@ -16,6 +16,9 @@ async-std-runtime = [ "async-std" ] default = [ "mongodb", "async-std-runtime" ] [dependencies] +# Repo +revolt-result = { path = "../result" } + # Utility log = "*" nanoid = "0.4.0" @@ -37,4 +40,4 @@ async-recursion = "1.0.4" async-std = { version = "1.8.0", features = ["attributes"], optional = true } # Authifier -authifier = { version = "1.0", default-features = false } +authifier = { version = "1.0" } diff --git a/crates/core/database/src/drivers/mongodb.rs b/crates/core/database/src/drivers/mongodb.rs index 6fb99135..afe22ac7 100644 --- a/crates/core/database/src/drivers/mongodb.rs +++ b/crates/core/database/src/drivers/mongodb.rs @@ -36,7 +36,7 @@ impl MongoDb { } /// Insert one document into a collection - async fn insert_one( + pub async fn insert_one( &self, collection: &'static str, document: T, @@ -44,8 +44,19 @@ impl MongoDb { self.col::(collection).insert_one(document, None).await } + /// Count documents by projection + pub async fn count_documents( + &self, + collection: &'static str, + projection: Document, + ) -> Result { + self.col::(collection) + .count_documents(projection, None) + .await + } + /// Find multiple documents in a collection with options - async fn find_with_options( + pub async fn find_with_options( &self, collection: &'static str, projection: Document, @@ -71,7 +82,7 @@ impl MongoDb { } /// Find multiple documents in a collection - async fn find( + pub async fn find( &self, collection: &'static str, projection: Document, @@ -80,7 +91,7 @@ impl MongoDb { } /// Find one document with options - async fn find_one_with_options( + pub async fn find_one_with_options( &self, collection: &'static str, projection: Document, @@ -95,7 +106,7 @@ impl MongoDb { } /// Find one document - async fn find_one( + pub async fn find_one( &self, collection: &'static str, projection: Document, @@ -105,7 +116,7 @@ impl MongoDb { } /// Find one document by its ID - async fn find_one_by_id( + pub async fn find_one_by_id( &self, collection: &'static str, id: &str, @@ -120,7 +131,7 @@ impl MongoDb { } /// Update one document given a projection, partial document, and list of paths to unset - async fn update_one( + pub async fn update_one( &self, collection: &'static str, projection: Document, @@ -159,7 +170,7 @@ impl MongoDb { } /// Update one document given an ID, partial document, and list of paths to unset - async fn update_one_by_id( + pub async fn update_one_by_id( &self, collection: &'static str, id: &str, @@ -183,7 +194,7 @@ impl MongoDb { } /// Delete one document by the given projection - async fn delete_one( + pub async fn delete_one( &self, collection: &'static str, projection: Document, @@ -194,7 +205,11 @@ impl MongoDb { } /// Delete one document by the given ID - async fn delete_one_by_id(&self, collection: &'static str, id: &str) -> Result { + pub async fn delete_one_by_id( + &self, + collection: &'static str, + id: &str, + ) -> Result { self.delete_one( collection, doc! { diff --git a/crates/core/database/src/lib.rs b/crates/core/database/src/lib.rs index 4a3aa36f..257abe96 100644 --- a/crates/core/database/src/lib.rs +++ b/crates/core/database/src/lib.rs @@ -13,6 +13,9 @@ extern crate log; #[macro_use] extern crate optional_struct; +#[macro_use] +extern crate revolt_result; + #[cfg(feature = "mongodb")] pub use mongodb; @@ -28,7 +31,7 @@ macro_rules! database_derived { macro_rules! auto_derived { ( $( $item:item )+ ) => { $( - #[derive(Serialize, Deserialize, Debug, Clone)] + #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] $item )+ }; @@ -36,8 +39,8 @@ macro_rules! auto_derived { macro_rules! auto_derived_partial { ( $item:item, $name:expr ) => { - #[derive(OptionalStruct, Serialize, Deserialize, Debug, Clone)] - #[optional_derive(Serialize, Deserialize, Debug, Clone)] + #[derive(OptionalStruct, Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)] + #[optional_derive(Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)] #[optional_name = $name] #[opt_skip_serializing_none] #[opt_some_priority] @@ -48,6 +51,15 @@ macro_rules! auto_derived_partial { mod drivers; pub use drivers::*; +macro_rules! database_test { + ( $name: expr ) => { + $crate::DatabaseInfo::Reference + .connect() + .await + .expect("Database connection failed.") + }; +} + mod models; pub use models::*; diff --git a/crates/core/database/src/models/bots/mod.rs b/crates/core/database/src/models/bots/mod.rs index 3037c2f0..4d801b73 100644 --- a/crates/core/database/src/models/bots/mod.rs +++ b/crates/core/database/src/models/bots/mod.rs @@ -1,5 +1,5 @@ mod model; -// mod ops; +mod ops; pub use model::*; -// pub use ops::*; +pub use ops::*; diff --git a/crates/core/database/src/models/bots/model.rs b/crates/core/database/src/models/bots/model.rs index a143340a..05fed343 100644 --- a/crates/core/database/src/models/bots/model.rs +++ b/crates/core/database/src/models/bots/model.rs @@ -1,3 +1,5 @@ +use revolt_result::Result; + use crate::Database; auto_derived_partial!( @@ -66,9 +68,59 @@ impl Bot { } /// Delete this bot - pub async fn delete(&self, db: &Database) -> Result<(), ()> { + pub async fn delete(&self, db: &Database) -> Result<()> { // db.fetch_user(&self.id).await?.mark_deleted(db).await?; - // db.delete_bot(&self.id).await - Ok(()) + db.delete_bot(&self.id).await + } +} + +#[cfg(test)] +mod tests { + use crate::{Bot, FieldsBot}; + + #[async_std::test] + async fn crud() { + let db = database_test!("bot_crud"); + + let bot_id = "bot"; + let user_id = "user"; + let token = "my_token"; + + let bot = Bot { + id: bot_id.to_string(), + owner: user_id.to_string(), + token: token.to_string(), + interactions_url: Some("some url".to_string()), + ..Default::default() + }; + + db.insert_bot(&bot).await.unwrap(); + db.update_bot( + bot_id, + &crate::PartialBot { + public: Some(true), + ..Default::default() + }, + vec![FieldsBot::Token, FieldsBot::InteractionsURL], + ) + .await + .unwrap(); + + let fetched_bot1 = db.fetch_bot(bot_id).await.unwrap(); + let fetched_bot2 = db.fetch_bot_by_token(&fetched_bot1.token).await.unwrap(); + let fetched_bots = db.fetch_bots_by_user(user_id).await.unwrap(); + + assert!(!bot.public); + assert!(fetched_bot1.public); + assert!(bot.interactions_url.is_some()); + assert!(fetched_bot1.interactions_url.is_none()); + assert_ne!(bot.token, fetched_bot1.token); + assert_eq!(fetched_bot1, fetched_bot2); + assert_eq!(fetched_bot1, fetched_bots[0]); + assert_eq!(1, db.get_number_of_bots_by_user(user_id).await.unwrap()); + + bot.delete(&db).await.unwrap(); + assert!(db.fetch_bot(bot_id).await.is_err()); + assert_eq!(0, db.get_number_of_bots_by_user(user_id).await.unwrap()); } } diff --git a/crates/core/database/src/models/bots/ops.rs b/crates/core/database/src/models/bots/ops.rs index 96056f90..3a618253 100644 --- a/crates/core/database/src/models/bots/ops.rs +++ b/crates/core/database/src/models/bots/ops.rs @@ -1,5 +1,9 @@ -mod dummy; +use revolt_result::Result; + +use crate::{Bot, FieldsBot, PartialBot}; + mod mongodb; +mod reference; #[async_trait] pub trait AbstractBots: Sync + Send { @@ -13,7 +17,12 @@ pub trait AbstractBots: Sync + Send { async fn insert_bot(&self, bot: &Bot) -> Result<()>; /// Update bot with new information - async fn update_bot(&self, id: &str, bot: &PartialBot, remove: Vec) -> Result<()>; + async fn update_bot( + &self, + id: &str, + partial: &PartialBot, + remove: Vec, + ) -> Result<()>; /// Delete a bot from the database async fn delete_bot(&self, id: &str) -> Result<()>; diff --git a/crates/core/database/src/models/bots/ops/mongodb.rs b/crates/core/database/src/models/bots/ops/mongodb.rs index 631e6bc7..7b4c7eee 100644 --- a/crates/core/database/src/models/bots/ops/mongodb.rs +++ b/crates/core/database/src/models/bots/ops/mongodb.rs @@ -1,9 +1,102 @@ -use revolt_database::MongoDb; +use ::mongodb::bson::doc; +use revolt_result::Result; + +use crate::{Bot, FieldsBot, PartialBot}; +use crate::{IntoDocumentPath, MongoDb}; use super::AbstractBots; -mod init; -mod scripts; +static COL: &str = "bots"; #[async_trait] -impl AbstractBots for MongoDb {} +impl AbstractBots for MongoDb { + /// Fetch a bot by its id + async fn fetch_bot(&self, id: &str) -> Result { + self.find_one_by_id(COL, id) + .await + .map_err(|_| create_database_error!("find_one", COL))? + .ok_or_else(|| create_error!(NotFound)) + } + + /// Fetch a bot by its token + async fn fetch_bot_by_token(&self, token: &str) -> Result { + self.find_one( + COL, + doc! { + "token": token + }, + ) + .await + .map_err(|_| create_database_error!("find_one", COL))? + .ok_or_else(|| create_error!(NotFound)) + } + + /// Insert new bot into the database + async fn insert_bot(&self, bot: &Bot) -> Result<()> { + self.insert_one(COL, &bot) + .await + .map(|_| ()) + .map_err(|_| create_database_error!("insert_one", COL)) + } + + /// Update bot with new information + async fn update_bot( + &self, + id: &str, + partial: &PartialBot, + remove: Vec, + ) -> Result<()> { + self.update_one_by_id( + COL, + id, + partial, + remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(), + None, + ) + .await + .map(|_| ()) + .map_err(|_| create_database_error!("update_one", COL)) + } + + /// Delete a bot from the database + async fn delete_bot(&self, id: &str) -> Result<()> { + self.delete_one_by_id(COL, id) + .await + .map(|_| ()) + .map_err(|_| create_database_error!("delete_one", COL)) + } + + /// Fetch bots owned by a user + async fn fetch_bots_by_user(&self, user_id: &str) -> Result> { + self.find( + COL, + doc! { + "owner": user_id + }, + ) + .await + .map_err(|_| create_database_error!("find", COL)) + } + + /// Get the number of bots owned by a user + async fn get_number_of_bots_by_user(&self, user_id: &str) -> Result { + self.count_documents( + COL, + doc! { + "owner": user_id + }, + ) + .await + .map(|v| v as usize) + .map_err(|_| create_database_error!("count", COL)) + } +} + +impl IntoDocumentPath for FieldsBot { + fn as_path(&self) -> Option<&'static str> { + match self { + FieldsBot::InteractionsURL => Some("interactions_url"), + FieldsBot::Token => None, + } + } +} diff --git a/crates/core/database/src/models/bots/ops/reference.rs b/crates/core/database/src/models/bots/ops/reference.rs index 30a56a45..26ce98c6 100644 --- a/crates/core/database/src/models/bots/ops/reference.rs +++ b/crates/core/database/src/models/bots/ops/reference.rs @@ -1,6 +1,81 @@ -use revolt_database::DummyDb; +use revolt_result::Result; + +use crate::ReferenceDb; +use crate::{Bot, FieldsBot, PartialBot}; use super::AbstractBots; #[async_trait] -impl AbstractBots for DummyDb {} +impl AbstractBots for ReferenceDb { + /// Fetch a bot by its id + async fn fetch_bot(&self, id: &str) -> Result { + let bots = self.bots.lock().await; + bots.get(id).cloned().ok_or_else(|| create_error!(NotFound)) + } + + /// Fetch a bot by its token + async fn fetch_bot_by_token(&self, token: &str) -> Result { + let bots = self.bots.lock().await; + bots.values() + .find(|bot| bot.token == token) + .cloned() + .ok_or_else(|| create_error!(NotFound)) + } + + /// Insert new bot into the database + async fn insert_bot(&self, bot: &Bot) -> Result<()> { + let mut bots = self.bots.lock().await; + if bots.contains_key(&bot.id) { + Err(create_database_error!("insert", "bot")) + } else { + bots.insert(bot.id.to_string(), bot.clone()); + Ok(()) + } + } + + /// Update bot with new information + async fn update_bot( + &self, + id: &str, + partial: &PartialBot, + remove: Vec, + ) -> Result<()> { + let mut bots = self.bots.lock().await; + if let Some(bot) = bots.get_mut(id) { + for field in remove { + bot.remove(&field); + } + + bot.apply_options(partial.clone()); + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } + + /// Delete a bot from the database + async fn delete_bot(&self, id: &str) -> Result<()> { + let mut bots = self.bots.lock().await; + if bots.remove(id).is_some() { + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } + + /// Fetch bots owned by a user + async fn fetch_bots_by_user(&self, user_id: &str) -> Result> { + let bots = self.bots.lock().await; + Ok(bots + .values() + .filter(|bot| bot.owner == user_id) + .cloned() + .collect()) + } + + /// Get the number of bots owned by a user + async fn get_number_of_bots_by_user(&self, user_id: &str) -> Result { + let bots = self.bots.lock().await; + Ok(bots.values().filter(|bot| bot.owner == user_id).count()) + } +} diff --git a/crates/core/database/src/models/mod.rs b/crates/core/database/src/models/mod.rs index 4ee16f0e..d6b2a21c 100644 --- a/crates/core/database/src/models/mod.rs +++ b/crates/core/database/src/models/mod.rs @@ -6,7 +6,11 @@ pub use bots::*; use crate::{Database, MongoDb, ReferenceDb}; -pub trait AbstractDatabase: Sync + Send + admin_migrations::AbstractMigrations {} +pub trait AbstractDatabase: + Sync + Send + admin_migrations::AbstractMigrations + bots::AbstractBots +{ +} + impl AbstractDatabase for ReferenceDb {} impl AbstractDatabase for MongoDb {} diff --git a/crates/core/result/src/lib.rs b/crates/core/result/src/lib.rs index 7f0eea5d..ada4d55f 100644 --- a/crates/core/result/src/lib.rs +++ b/crates/core/result/src/lib.rs @@ -99,7 +99,7 @@ pub enum ErrorType { // ? General errors DatabaseError { operation: String, - with: String, + collection: String, }, InternalError, InvalidOperation, @@ -119,14 +119,24 @@ pub enum ErrorType { #[macro_export] macro_rules! create_error { - ( $error:ident ) => { + ( $error:ident $( $tt:tt )? ) => { $crate::Error { - error_type: $crate::ErrorType::$error, + error_type: $crate::ErrorType::$error $( $tt )?, location: format!("{}:{}:{}", file!(), line!(), column!()), } }; } +#[macro_export] +macro_rules! create_database_error { + ( $operation:expr, $collection:expr ) => { + create_error!(DatabaseError { + operation: $operation.to_string(), + collection: $collection.to_string() + }) + }; +} + #[cfg(test)] mod tests { use crate::ErrorType; @@ -136,4 +146,10 @@ mod tests { let error = create_error!(LabelMe); assert!(matches!(error.error_type, ErrorType::LabelMe)); } + + #[test] + fn use_macro_to_construct_complex_error() { + let error = create_error!(LabelMe); + assert!(matches!(error.error_type, ErrorType::LabelMe)); + } }