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 std::collections::HashSet;
|
||||||
|
|
||||||
use futures::TryStreamExt;
|
use futures::TryStreamExt;
|
||||||
|
@ -11,14 +13,51 @@ use crate::environment::get_env_variable;
|
||||||
|
|
||||||
use super::models::{YorokobotCollection, COLLECTIONS_NAMES};
|
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 {
|
pub struct Client {
|
||||||
mongo_client: Option<MongoClient>,
|
mongo_client: Option<MongoClient>,
|
||||||
database: Option<Database>,
|
database: Option<Database>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
/// Create a new database client
|
/// Create a new Database client instance.
|
||||||
pub fn new() -> Client {
|
pub fn new() -> Client {
|
||||||
Client {
|
Client {
|
||||||
mongo_client: None,
|
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 {
|
match &self.database {
|
||||||
Some(db) => db,
|
Some(db) => db,
|
||||||
None => {
|
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) {
|
pub async fn connect(&mut self) {
|
||||||
self.mongo_client = match MongoClient::with_uri_str(get_env_variable("MONGODB_URI")).await {
|
self.mongo_client = match MongoClient::with_uri_str(get_env_variable("MONGODB_URI")).await {
|
||||||
Ok(c) => {
|
Ok(c) => {
|
||||||
|
@ -52,7 +98,7 @@ impl Client {
|
||||||
self.database = match &self.mongo_client {
|
self.database = match &self.mongo_client {
|
||||||
Some(c) => Some(c.database(get_env_variable("MONGODB_DATABASE").as_str())),
|
Some(c) => Some(c.database(get_env_variable("MONGODB_DATABASE").as_str())),
|
||||||
None => {
|
None => {
|
||||||
error!("Got an unexpected None from self.database");
|
error!("Got an unexpected None for inner mongodb client instance");
|
||||||
panic!();
|
panic!();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -60,19 +106,30 @@ impl Client {
|
||||||
self.check_init_error().await;
|
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) {
|
async fn check_init_error(&mut self) {
|
||||||
info!("Launching initial database checks");
|
info!("Launching initial database checks");
|
||||||
|
|
||||||
let database = self.get_database();
|
self.check_collections_presence().await;
|
||||||
|
|
||||||
Self::check_collections_presence(database).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");
|
trace!("Starting the collections presence check for the database");
|
||||||
|
|
||||||
let mut missing_collections: Vec<&str> = vec![];
|
let mut missing_collections: Vec<&str> = vec![];
|
||||||
let collections: HashSet<String> = match db.list_collection_names(None).await {
|
let collections: HashSet<String> =
|
||||||
|
match self.get_database().list_collection_names(None).await {
|
||||||
Ok(n) => n.into_iter().collect(),
|
Ok(n) => n.into_iter().collect(),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to get the collections for the database: {e:#?}");
|
error!("Failed to get the collections for the database: {e:#?}");
|
||||||
|
@ -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)]
|
#[allow(dead_code)]
|
||||||
fn get_collection<T: YorokobotCollection>(&self) -> Collection<Document> {
|
fn get_collection<T: YorokobotCollection>(&self) -> Collection<Document> {
|
||||||
self.get_database().collection(&T::get_collection_name())
|
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> {
|
fn get_typed_collection<T: YorokobotCollection>(&self) -> Collection<T> {
|
||||||
self.get_database()
|
self.get_database()
|
||||||
.collection::<T>(&T::get_collection_name())
|
.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>, ()> {
|
pub async fn get_by_id<T: YorokobotCollection>(&self, id: &str) -> Result<Option<T>, ()> {
|
||||||
self.get_one::<T>(doc! {"_id": id}).await
|
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>, ()> {
|
pub async fn get_one<T: YorokobotCollection>(&self, filter: Document) -> Result<Option<T>, ()> {
|
||||||
match self
|
match self
|
||||||
.get_typed_collection::<T>()
|
.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)]
|
#[allow(dead_code)]
|
||||||
pub async fn get_all<T: YorokobotCollection>(
|
pub async fn get_all<T: YorokobotCollection>(
|
||||||
&self,
|
&self,
|
||||||
|
@ -150,7 +230,8 @@ impl Client {
|
||||||
Ok(matching_docs)
|
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<(), ()> {
|
pub async fn insert_one<T: YorokobotCollection>(&self, document: T) -> Result<(), ()> {
|
||||||
match self
|
match self
|
||||||
.get_typed_collection::<T>()
|
.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)]
|
#[allow(dead_code)]
|
||||||
pub async fn insert_many<T: YorokobotCollection>(&self, documents: Vec<T>) -> Result<(), ()> {
|
pub async fn insert_many<T: YorokobotCollection>(&self, documents: Vec<T>) -> Result<(), ()> {
|
||||||
match self
|
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)]
|
#[allow(dead_code)]
|
||||||
pub async fn delete_one<T: YorokobotCollection>(&self, document: Document) -> Result<u64, ()> {
|
pub async fn delete_one<T: YorokobotCollection>(&self, document: Document) -> Result<u64, ()> {
|
||||||
match self
|
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)]
|
#[allow(dead_code)]
|
||||||
pub async fn delete_by_id<T: YorokobotCollection>(&self, id: &str) -> Result<(), ()> {
|
pub async fn delete_by_id<T: YorokobotCollection>(&self, id: &str) -> Result<(), ()> {
|
||||||
match self
|
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)]
|
#[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
|
match self
|
||||||
.get_typed_collection::<T>()
|
.get_typed_collection::<T>()
|
||||||
.delete_many(document, None)
|
.delete_many(filter, None)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => Ok(()),
|
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>(
|
pub async fn update_one<T: YorokobotCollection>(
|
||||||
&self,
|
&self,
|
||||||
object: T,
|
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)]
|
#[allow(dead_code)]
|
||||||
pub async fn update_by_id<T: YorokobotCollection>(
|
pub async fn update_by_id<T: YorokobotCollection>(
|
||||||
&self,
|
&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)]
|
#[allow(dead_code)]
|
||||||
pub async fn update_many<T: YorokobotCollection>(
|
pub async fn update_many<T: YorokobotCollection>(
|
||||||
&self,
|
&self,
|
||||||
document: Document,
|
filter: Document,
|
||||||
update: Document,
|
update: Document,
|
||||||
) -> Result<(), ()> {
|
) -> Result<(), ()> {
|
||||||
match self
|
match self
|
||||||
.get_typed_collection::<T>()
|
.get_typed_collection::<T>()
|
||||||
.update_many(document, update, None)
|
.update_many(filter, update, None)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => Ok(()),
|
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};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Name of the collections of the database.
|
||||||
pub const COLLECTIONS_NAMES: [&str; 1] = ["guilds"];
|
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 {
|
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;
|
fn get_collection_name() -> String;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tags
|
/// Represent a Yorokobot tag.
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct Tag {
|
pub struct Tag {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
@ -16,6 +19,7 @@ pub struct Tag {
|
||||||
pub subscribers: Vec<String>,
|
pub subscribers: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represent a Discord server/guild.
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct Guild {
|
pub struct Guild {
|
||||||
#[serde(rename = "_id")]
|
#[serde(rename = "_id")]
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
//! Discord interaction module
|
||||||
|
|
||||||
pub mod event_handler;
|
pub mod event_handler;
|
||||||
mod message_builders;
|
mod message_builders;
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
//! Commons elements that are used in while executing the bot commands.
|
||||||
|
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
@ -14,17 +16,34 @@ use serenity::{
|
||||||
|
|
||||||
use crate::database::Client as DatabaseClient;
|
use crate::database::Client as DatabaseClient;
|
||||||
|
|
||||||
|
/// The kind of errors that can be returned while executing a command.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum CommandExecutionError {
|
pub enum CommandExecutionError {
|
||||||
|
/// An error that is returned when the `Serenity` crate failed to get the argument from the
|
||||||
|
/// Discord API payload.
|
||||||
ArgumentExtractionError(String),
|
ArgumentExtractionError(String),
|
||||||
|
|
||||||
|
/// An error that is returned when the cast from the argument value to a Rust type failed.
|
||||||
ArgumentDeserializationError(String),
|
ArgumentDeserializationError(String),
|
||||||
|
|
||||||
|
/// The kind of error that is returned when a Database query failed.
|
||||||
DatabaseQueryError(String),
|
DatabaseQueryError(String),
|
||||||
|
|
||||||
|
/// Error returned when we fail to extract the context of a Discord commands.
|
||||||
ContextRetrievalError(String),
|
ContextRetrievalError(String),
|
||||||
|
|
||||||
|
/// Error returned when sending a command to the discord API failed.
|
||||||
DiscordAPICallError(String),
|
DiscordAPICallError(String),
|
||||||
|
|
||||||
|
/// Error returned when there was an issue while using a selector embed in the command
|
||||||
|
/// response.
|
||||||
SelectorError(String),
|
SelectorError(String),
|
||||||
|
|
||||||
|
/// Error returned when the given command is unknown to Yorokobot.
|
||||||
UnknownCommand(String),
|
UnknownCommand(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An option that can/have to be passed to a Discord command.
|
||||||
pub struct BotCommandOption {
|
pub struct BotCommandOption {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
|
@ -32,18 +51,29 @@ pub struct BotCommandOption {
|
||||||
pub required: bool,
|
pub required: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A trait that represent a Yorokobot command.
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait BotCommand {
|
pub trait BotCommand {
|
||||||
|
/// Create a new command instance.
|
||||||
fn new(interaction: ApplicationCommandInteraction) -> Self;
|
fn new(interaction: ApplicationCommandInteraction) -> Self;
|
||||||
|
|
||||||
|
/// Return the name of the command.
|
||||||
fn name() -> String;
|
fn name() -> String;
|
||||||
|
|
||||||
|
/// Return the command description.
|
||||||
fn description() -> String;
|
fn description() -> String;
|
||||||
|
|
||||||
|
/// Return the list of the argument that the command takes.
|
||||||
fn options_list() -> Vec<BotCommandOption>;
|
fn options_list() -> Vec<BotCommandOption>;
|
||||||
|
|
||||||
|
/// Execute the command with the given `context` and using the given `database`.
|
||||||
async fn run(
|
async fn run(
|
||||||
&self,
|
&self,
|
||||||
context: Context,
|
context: Context,
|
||||||
database: Arc<DatabaseClient>,
|
database: Arc<DatabaseClient>,
|
||||||
) -> Result<(), CommandExecutionError>;
|
) -> Result<(), CommandExecutionError>;
|
||||||
|
|
||||||
|
/// Extract and deserialize the `index`th value from the command `options`.
|
||||||
fn extract_option_value(
|
fn extract_option_value(
|
||||||
options: &[CommandDataOption],
|
options: &[CommandDataOption],
|
||||||
index: usize,
|
index: usize,
|
||||||
|
@ -52,6 +82,7 @@ pub trait BotCommand {
|
||||||
serialized_opt.resolved.clone()
|
serialized_opt.resolved.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Register the command to the Discord API.
|
||||||
async fn register(context: &Context) {
|
async fn register(context: &Context) {
|
||||||
info!("Starting the {} command registration", Self::name());
|
info!("Starting the {} command registration", Self::name());
|
||||||
match Command::create_global_application_command(context, |command| {
|
match Command::create_global_application_command(context, |command| {
|
||||||
|
|
Loading…
Reference in a new issue