From 4fe923622e74683cd4e283c86ab580692e9e82d9 Mon Sep 17 00:00:00 2001 From: Victor Mignot Date: Sun, 16 Apr 2023 23:52:43 +0200 Subject: [PATCH] Add code documentation --- src/database/client.rs | 149 ++++++++++++++++++++++++++------ src/database/models.rs | 8 +- src/discord.rs | 2 + src/discord/commands/commons.rs | 31 +++++++ 4 files changed, 160 insertions(+), 30 deletions(-) diff --git a/src/database/client.rs b/src/database/client.rs index c9906c2..478d059 100644 --- a/src/database/client.rs +++ b/src/database/client.rs @@ -1,3 +1,5 @@ +//! The MongoDB crate wrapper used by Yorokobot to query the database. + use std::collections::HashSet; use futures::TryStreamExt; @@ -11,14 +13,51 @@ use crate::environment::get_env_variable; use super::models::{YorokobotCollection, COLLECTIONS_NAMES}; -/// Database client +/// Yorokobot's database client. +/// +/// Actually, it's just a wrapper around the [mongodb] crate. +/// It does basically nothing until you call the [`connect`] method. +/// +/// To use it, the MONGODB_URI and/or MONGODB_DATABASE environment variable +/// have to be set. +/// +/// [`connect`]: Client::connect +/// +/// # Collections +/// +/// To call the public method of this wrapper, you have to declare a Collection struct, +/// implement the [YorokobotCollection] traits, and add the collection name to [COLLECTIONS_NAMES]. +/// See the [`Guild`] struct. +/// +/// [`Guild`]: super::models::Guild +/// +/// # Update methods +/// +/// In each update method of the Client struct, the given update has to be nested in the `$set` +/// parameter, as this argument is passed as it is in the request to the database. +/// +///# Example +/// +/// ``` +/// use mongodb::doc; +/// +/// let mut db_client = Client::new(); +/// +/// db_client.connect(); +/// +/// // Do your stuff with the database. +/// let result = db_client.get_one::(doc! {}).expect().unwrap(); +/// +/// db.update_one::(result, doc! { "$set": { "tags": Vec::new() } }); +/// ``` +/// pub struct Client { mongo_client: Option, database: Option, } impl Client { - /// Create a new database client + /// Create a new Database client instance. pub fn new() -> Client { Client { mongo_client: None, @@ -26,7 +65,8 @@ impl Client { } } - pub fn get_database(&self) -> &Database { + /// Get a reference to the inner mongodb client. + fn get_database(&self) -> &Database { match &self.database { Some(db) => db, None => { @@ -36,7 +76,13 @@ impl Client { } } - /// Connect the client + /// Connect the client. + /// + /// This method will panic if: + /// - The MONGODB_URI environment variable is not set. + /// - If the database to use is not specified in the connection string given with MONGODB_URI + /// and if MONGODB_DATABASE is not set. + /// - If the given credentials are invalid. pub async fn connect(&mut self) { self.mongo_client = match MongoClient::with_uri_str(get_env_variable("MONGODB_URI")).await { Ok(c) => { @@ -52,7 +98,7 @@ impl Client { self.database = match &self.mongo_client { Some(c) => Some(c.database(get_env_variable("MONGODB_DATABASE").as_str())), None => { - error!("Got an unexpected None from self.database"); + error!("Got an unexpected None for inner mongodb client instance"); panic!(); } }; @@ -60,25 +106,36 @@ impl Client { self.check_init_error().await; } + /// Initials check that are run before right after connecting to the database. + /// + /// It currently runs the following tests: + /// - Check that all the needed Mongo collections exists within the database. async fn check_init_error(&mut self) { info!("Launching initial database checks"); - let database = self.get_database(); - - Self::check_collections_presence(database).await; + self.check_collections_presence().await; } - async fn check_collections_presence(db: &Database) { + /// Check the collections presence within the database. + /// + /// This function is called by [`check_init_error`]. + /// This method panic if: + /// - It fails to fetch the database collections. + /// - A collection is missing. + /// + /// [`check_init_error`]: Self::check_init_error + async fn check_collections_presence(&self) { trace!("Starting the collections presence check for the database"); let mut missing_collections: Vec<&str> = vec![]; - let collections: HashSet = match db.list_collection_names(None).await { - Ok(n) => n.into_iter().collect(), - Err(e) => { - error!("Failed to get the collections for the database: {e:#?}"); - panic!(); - } - }; + let collections: HashSet = + match self.get_database().list_collection_names(None).await { + Ok(n) => n.into_iter().collect(), + Err(e) => { + error!("Failed to get the collections for the database: {e:#?}"); + panic!(); + } + }; for col in COLLECTIONS_NAMES { if !collections.contains(col) { @@ -95,20 +152,41 @@ impl Client { } } + /// Get access to the collection matching the `T` type. + /// + /// Unlike the [`get_typed_collection`] method, this one allows to deals directly with mongodb + /// [Document] type, without using [serde] (de)serialization. + /// + /// [`get_typed_collection`]: Self::get_typed_collection #[allow(dead_code)] fn get_collection(&self) -> Collection { self.get_database().collection(&T::get_collection_name()) } + /// Get access to the collection matching the `T` type, but dealing directly with the matching + /// struct. + /// + /// This internly use [serde]'s [`Serialize`] and [`Deserialize`] traits. + /// + /// [`Serialize`]: serde::Serialize + /// [`Deserialize`]: serde::Deserialize fn get_typed_collection(&self) -> Collection { self.get_database() .collection::(&T::get_collection_name()) } + /// Return the document in the `T` type matching the `id` ObjectId. + /// + /// Return Ok(None) if the collection does not contains element matching this id and Err(()) + /// if the query failed. pub async fn get_by_id(&self, id: &str) -> Result, ()> { self.get_one::(doc! {"_id": id}).await } + /// Return the first element of the `T` type matching the given `filter`. + /// + /// Return Ok(None) if the collection does not contains matching element matching this filter + /// and Err(()) if the query failed. pub async fn get_one(&self, filter: Document) -> Result, ()> { match self .get_typed_collection::() @@ -120,6 +198,8 @@ impl Client { } } + /// Return all the element of the type `T`. + /// If `filter` is provided, return only the ones matching the given filter. #[allow(dead_code)] pub async fn get_all( &self, @@ -150,7 +230,8 @@ impl Client { Ok(matching_docs) } - // TODO: Set true error handling + /// Insert the provided `document` in the database. + /// Return Err(()) if the query failed. pub async fn insert_one(&self, document: T) -> Result<(), ()> { match self .get_typed_collection::() @@ -162,7 +243,8 @@ impl Client { } } - // TODO: Set true error handling + /// Insert all the provided `documents` in the database. + /// Return Err(()) if the query failed. #[allow(dead_code)] pub async fn insert_many(&self, documents: Vec) -> Result<(), ()> { match self @@ -175,7 +257,8 @@ impl Client { } } - // TODO: Set true error handling + /// Delete the first document matching the given `document`. + /// Return Err(()) if the query failed. #[allow(dead_code)] pub async fn delete_one(&self, document: Document) -> Result { match self @@ -188,7 +271,8 @@ impl Client { } } - // TODO: Set true error handling + /// Delete the document matching whose ObjectId match the given `id`. + /// Return Err(()) if the query failed. #[allow(dead_code)] pub async fn delete_by_id(&self, id: &str) -> Result<(), ()> { match self @@ -201,12 +285,13 @@ impl Client { } } - // TODO: Set true error handling + /// Delete the documents matching the given filter. + /// Return Err(()) if the query failed. #[allow(dead_code)] - pub async fn delete_many(&self, document: Document) -> Result<(), ()> { + pub async fn delete_many(&self, filter: Document) -> Result<(), ()> { match self .get_typed_collection::() - .delete_many(document, None) + .delete_many(filter, None) .await { Ok(_) => Ok(()), @@ -214,7 +299,10 @@ impl Client { } } - //TODO: Set true error handling + /// Update the given `object` with the `update` modifications. + /// Currently, we should pass the `"$set"` argument to the `update` + /// document which is passed as it is to MongoDB. + /// Return Err(()) if the query failed. pub async fn update_one( &self, object: T, @@ -235,7 +323,10 @@ impl Client { } } - //TODO: Set true error handling + /// Update the object whose ObjectId match the given `id` with the given `update`. The `update` + /// parameter need to be nested within the MongoDB `"$set"` parameter, as it is passed as it is + /// to the database instance. + /// Return Err(()) if the query failed. #[allow(dead_code)] pub async fn update_by_id( &self, @@ -252,16 +343,18 @@ impl Client { } } - //TODO: Set true error handling + /// Update all the objects matching the given `filter` with the given `update`. + /// The update parameter need to be nested within the MongoDB `"$set"` parameter, as it is + /// passed as it is in the request. #[allow(dead_code)] pub async fn update_many( &self, - document: Document, + filter: Document, update: Document, ) -> Result<(), ()> { match self .get_typed_collection::() - .update_many(document, update, None) + .update_many(filter, update, None) .await { Ok(_) => Ok(()), diff --git a/src/database/models.rs b/src/database/models.rs index 987af91..cf30974 100644 --- a/src/database/models.rs +++ b/src/database/models.rs @@ -1,14 +1,17 @@ -//! Bot data models +//! The data models used by Yorokobot and that link to the MongoDB collections. use serde::{Deserialize, Serialize}; +/// Name of the collections of the database. pub const COLLECTIONS_NAMES: [&str; 1] = ["guilds"]; +/// The trait that should be implemented by the collections model of Yorokobot. pub trait YorokobotCollection: for<'de> Deserialize<'de> + Serialize + Unpin + Send + Sync { + /// Return the collection name linked to the model struct. fn get_collection_name() -> String; } -/// Tags +/// Represent a Yorokobot tag. #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Tag { pub name: String, @@ -16,6 +19,7 @@ pub struct Tag { pub subscribers: Vec, } +/// Represent a Discord server/guild. #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Guild { #[serde(rename = "_id")] diff --git a/src/discord.rs b/src/discord.rs index fd7c559..cd20cbc 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -1,3 +1,5 @@ +//! Discord interaction module + pub mod event_handler; mod message_builders; diff --git a/src/discord/commands/commons.rs b/src/discord/commands/commons.rs index c294130..c6a2aa2 100644 --- a/src/discord/commands/commons.rs +++ b/src/discord/commands/commons.rs @@ -1,3 +1,5 @@ +//! Commons elements that are used in while executing the bot commands. + use log::{info, warn}; use std::sync::Arc; @@ -14,17 +16,34 @@ use serenity::{ use crate::database::Client as DatabaseClient; +/// The kind of errors that can be returned while executing a command. #[derive(Debug)] pub enum CommandExecutionError { + /// An error that is returned when the `Serenity` crate failed to get the argument from the + /// Discord API payload. ArgumentExtractionError(String), + + /// An error that is returned when the cast from the argument value to a Rust type failed. ArgumentDeserializationError(String), + + /// The kind of error that is returned when a Database query failed. DatabaseQueryError(String), + + /// Error returned when we fail to extract the context of a Discord commands. ContextRetrievalError(String), + + /// Error returned when sending a command to the discord API failed. DiscordAPICallError(String), + + /// Error returned when there was an issue while using a selector embed in the command + /// response. SelectorError(String), + + /// Error returned when the given command is unknown to Yorokobot. UnknownCommand(String), } +/// An option that can/have to be passed to a Discord command. pub struct BotCommandOption { pub name: String, pub description: String, @@ -32,18 +51,29 @@ pub struct BotCommandOption { pub required: bool, } +/// A trait that represent a Yorokobot command. #[async_trait] pub trait BotCommand { + /// Create a new command instance. fn new(interaction: ApplicationCommandInteraction) -> Self; + + /// Return the name of the command. fn name() -> String; + + /// Return the command description. fn description() -> String; + + /// Return the list of the argument that the command takes. fn options_list() -> Vec; + + /// Execute the command with the given `context` and using the given `database`. async fn run( &self, context: Context, database: Arc, ) -> Result<(), CommandExecutionError>; + /// Extract and deserialize the `index`th value from the command `options`. fn extract_option_value( options: &[CommandDataOption], index: usize, @@ -52,6 +82,7 @@ pub trait BotCommand { serialized_opt.resolved.clone() } + /// Register the command to the Discord API. async fn register(context: &Context) { info!("Starting the {} command registration", Self::name()); match Command::create_global_application_command(context, |command| {