Add code documentation
This commit is contained in:
parent
d260b43f9a
commit
4fe923622e
|
@ -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(()),
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//! Discord interaction module
|
||||
|
||||
pub mod event_handler;
|
||||
mod message_builders;
|
||||
|
||||
|
|
|
@ -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| {
|
||||
|
|
Loading…
Reference in a new issue