Add code documentation

This commit is contained in:
Victor Mignot 2023-04-16 23:52:43 +02:00
parent d260b43f9a
commit 4fe923622e
No known key found for this signature in database
GPG key ID: FFE4EF056FB5E0D0
4 changed files with 160 additions and 30 deletions

View file

@ -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::<Guild>(doc! {}).expect().unwrap();
///
/// db.update_one::<Guild>(result, doc! { "$set": { "tags": Vec::new() } });
/// ```
///
pub struct Client {
mongo_client: Option<MongoClient>,
database: Option<Database>,
}
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<String> = 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<String> =
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<T: YorokobotCollection>(&self) -> Collection<Document> {
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<T: YorokobotCollection>(&self) -> Collection<T> {
self.get_database()
.collection::<T>(&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<T: YorokobotCollection>(&self, id: &str) -> Result<Option<T>, ()> {
self.get_one::<T>(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<T: YorokobotCollection>(&self, filter: Document) -> Result<Option<T>, ()> {
match self
.get_typed_collection::<T>()
@ -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<T: YorokobotCollection>(
&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<T: YorokobotCollection>(&self, document: T) -> Result<(), ()> {
match self
.get_typed_collection::<T>()
@ -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<T: YorokobotCollection>(&self, documents: Vec<T>) -> 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<T: YorokobotCollection>(&self, document: Document) -> Result<u64, ()> {
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<T: YorokobotCollection>(&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<T: YorokobotCollection>(&self, document: Document) -> Result<(), ()> {
pub async fn delete_many<T: YorokobotCollection>(&self, filter: Document) -> Result<(), ()> {
match self
.get_typed_collection::<T>()
.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<T: YorokobotCollection>(
&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<T: YorokobotCollection>(
&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<T: YorokobotCollection>(
&self,
document: Document,
filter: Document,
update: Document,
) -> Result<(), ()> {
match self
.get_typed_collection::<T>()
.update_many(document, update, None)
.update_many(filter, update, None)
.await
{
Ok(_) => Ok(()),

View file

@ -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<String>,
}
/// Represent a Discord server/guild.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Guild {
#[serde(rename = "_id")]

View file

@ -1,3 +1,5 @@
//! Discord interaction module
pub mod event_handler;
mod message_builders;

View file

@ -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<BotCommandOption>;
/// Execute the command with the given `context` and using the given `database`.
async fn run(
&self,
context: Context,
database: Arc<DatabaseClient>,
) -> 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| {