Add cli parsing logic and logging to rotom_discord_bot
This commit is contained in:
@@ -4,8 +4,15 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.27", features = ["derive", "env"] }
|
||||
dotenvy = { version = "0.15.7", features = ["clap"] }
|
||||
env_logger = "0.11.6"
|
||||
log = "0.4.25"
|
||||
rotom_core = { path = "../rotom_core" }
|
||||
rotom_database = { path = "../rotom_database", default-features = false }
|
||||
secrecy = "0.10.3"
|
||||
thiserror = "2.0.11"
|
||||
tokio = { version = "1.43.0", features = ["full"] }
|
||||
|
||||
[features]
|
||||
default = ["mysql", "postgres", "sqlite"]
|
||||
|
||||
17
rotom_discord_bot/src/app/mod.rs
Normal file
17
rotom_discord_bot/src/app/mod.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use rotom_core::repository::RepositoryProvider;
|
||||
|
||||
use crate::cli::DiscordCredentials;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AppError {
|
||||
|
||||
}
|
||||
|
||||
pub async fn start<R>(_credentials: DiscordCredentials, _repository_provider: R) -> Result<(), AppError>
|
||||
where
|
||||
R: RepositoryProvider + Send + Sync + 'static,
|
||||
R::BackendError: Send + Sync,
|
||||
for<'a> R::Repository<'a>: Send + Sync,
|
||||
{
|
||||
todo!()
|
||||
}
|
||||
29
rotom_discord_bot/src/cli/command.rs
Normal file
29
rotom_discord_bot/src/cli/command.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use clap::Parser;
|
||||
|
||||
/// Subcommand of the CLI application.
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
pub enum Command {
|
||||
/// Start the main discord bot application.
|
||||
#[command(
|
||||
name = "start",
|
||||
about,
|
||||
long_about = None,
|
||||
)]
|
||||
Start(super::start::Start),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CommandError {
|
||||
#[error(transparent)]
|
||||
StartError(#[from] super::start::StartError),
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub async fn execute(self) -> Result<(), CommandError> {
|
||||
match self {
|
||||
Command::Start(start) => start.execute().await?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
148
rotom_discord_bot/src/cli/mod.rs
Normal file
148
rotom_discord_bot/src/cli/mod.rs
Normal file
@@ -0,0 +1,148 @@
|
||||
use std::fmt::Debug;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::Parser;
|
||||
use clap::ValueEnum;
|
||||
use command::Command;
|
||||
use secrecy::SecretString;
|
||||
|
||||
pub mod command;
|
||||
pub mod start;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CliError {
|
||||
#[error(transparent)]
|
||||
Dotenvy(#[from] dotenvy::Error),
|
||||
}
|
||||
|
||||
/// Parses command line arguments.
|
||||
pub fn parse() -> Result<Cli, CliError> {
|
||||
// First phase: If a dotenv file is specified, load values from it.
|
||||
// This allows values in the dotenv file to be used when parsing other arguments.
|
||||
let dotenv = Dotenv::parse();
|
||||
if let Some(dotenv_path) = &dotenv.path {
|
||||
dotenvy::from_path_override(dotenv_path)?;
|
||||
}
|
||||
|
||||
// Second phase: Parse CLI options using the `Cli` parser.
|
||||
let mut cli = Cli::parse();
|
||||
|
||||
// The `Cli` parser contains the `Dotenv` parser and it may parse different results in different phases.
|
||||
// It is replaced here to ensure the `Cli` instance reflects the original dotenv configuration.
|
||||
cli.dotenv = dotenv;
|
||||
|
||||
return Ok(cli);
|
||||
}
|
||||
|
||||
/// Main command line interface for the librarian application.
|
||||
///
|
||||
/// This struct combines the dotenv configuration with the subcommands
|
||||
/// that the CLI application supports.
|
||||
///
|
||||
/// The help and version flags and subcommands are explicitly enabled
|
||||
/// because `Dotenv` disables them and `Cli` requires them to be enabled.
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(
|
||||
name = "rotom",
|
||||
about,
|
||||
version,
|
||||
long_about = None,
|
||||
ignore_errors = false,
|
||||
disable_help_flag = false,
|
||||
disable_help_subcommand = false,
|
||||
disable_version_flag = false,
|
||||
)]
|
||||
pub struct Cli {
|
||||
/// Configuration for loading environment variables from a dotenv file.
|
||||
#[command(flatten)]
|
||||
pub dotenv: Dotenv,
|
||||
|
||||
/// The command to be executed as part of the CLI application.
|
||||
#[command(subcommand)]
|
||||
pub command: Command,
|
||||
}
|
||||
|
||||
/// Configuration for loading environment variables from a dotenv file.
|
||||
///
|
||||
/// This struct is used to specify the path to a dotenv file from which
|
||||
/// environment variables can be loaded. The default value is `.env`.
|
||||
///
|
||||
/// The help and version flags and subcommands are disabled because
|
||||
/// they are handled by `Cli` which is parsed after `Dotenv`.
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
#[command(
|
||||
ignore_errors = true,
|
||||
disable_help_flag = true,
|
||||
disable_help_subcommand = true,
|
||||
disable_version_flag = true
|
||||
)]
|
||||
pub struct Dotenv {
|
||||
/// The path to the dotenv file.
|
||||
#[arg(
|
||||
name = "path",
|
||||
short = None,
|
||||
long = "dotenv",
|
||||
env = "DOTENV",
|
||||
)]
|
||||
pub path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, ValueEnum)]
|
||||
pub enum DatabaseDialect {
|
||||
#[cfg(feature = "mysql")]
|
||||
Mysql,
|
||||
#[cfg(feature = "postgres")]
|
||||
Postgres,
|
||||
#[cfg(feature = "sqlite")]
|
||||
Sqlite,
|
||||
}
|
||||
|
||||
impl From<DatabaseDialect> for rotom_database::DatabaseDialect {
|
||||
fn from(value: DatabaseDialect) -> Self {
|
||||
use rotom_database::DatabaseDialect as Dialect;
|
||||
match value {
|
||||
#[cfg(feature = "mysql")]
|
||||
DatabaseDialect::Mysql => Dialect::Mysql,
|
||||
#[cfg(feature = "postgres")]
|
||||
DatabaseDialect::Postgres => Dialect::Postgres,
|
||||
#[cfg(feature = "sqlite")]
|
||||
DatabaseDialect::Sqlite => Dialect::Sqlite,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Credentials required to establish a database connection.
|
||||
#[derive(Clone, Debug, Parser)]
|
||||
pub struct DatabaseCredentials {
|
||||
/// The dialect of the database to connect to.
|
||||
#[arg(
|
||||
short = None,
|
||||
long = "database-dialect",
|
||||
env = "DATABASE_DIALECT",
|
||||
)]
|
||||
pub dialect: DatabaseDialect,
|
||||
|
||||
/// The URL of the database to connect to. This should include the
|
||||
/// necessary credentials (username and password) and the database
|
||||
/// name, following the format: `dialect://username:password@host:port/database`.
|
||||
#[arg(
|
||||
short = None,
|
||||
long = "database-url",
|
||||
env = "DATABASE_URL",
|
||||
hide_env_values(true),
|
||||
)]
|
||||
pub url: SecretString,
|
||||
}
|
||||
|
||||
/// Credentials required to authenticate a bot with Discord.
|
||||
#[derive(Clone, Debug, Parser)]
|
||||
pub struct DiscordCredentials {
|
||||
/// The token used to authenticate the bot with Discord.
|
||||
#[arg(
|
||||
short = None,
|
||||
long = "bot-token",
|
||||
env = "BOT_TOKEN",
|
||||
hide_env_values(true),
|
||||
)]
|
||||
pub bot_token: SecretString,
|
||||
}
|
||||
62
rotom_discord_bot/src/cli/start.rs
Normal file
62
rotom_discord_bot/src/cli/start.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use clap::Parser;
|
||||
use secrecy::ExposeSecret;
|
||||
|
||||
use super::DatabaseCredentials;
|
||||
use super::DiscordCredentials;
|
||||
|
||||
/// Start the main discord bot application.
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
pub struct Start {
|
||||
/// Credentials required to establish a database connection.
|
||||
#[command(flatten)]
|
||||
pub database: DatabaseCredentials,
|
||||
|
||||
/// Credentials required to authenticate a bot with Discord.
|
||||
#[command(flatten)]
|
||||
pub discord: DiscordCredentials,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum StartError {
|
||||
#[error(transparent)]
|
||||
RepositoryBackendError(#[from] rotom_database::BackendError),
|
||||
#[error(transparent)]
|
||||
AppError(#[from] crate::app::AppError),
|
||||
}
|
||||
|
||||
impl Start {
|
||||
pub async fn execute(self) -> Result<(), StartError> {
|
||||
log::debug!("{:#?}", self);
|
||||
|
||||
let database_url = self.database.url.expose_secret();
|
||||
|
||||
match self.database.dialect {
|
||||
#[cfg(feature = "mysql")]
|
||||
crate::cli::DatabaseDialect::Mysql => {
|
||||
log::info!("Running any pending database migrations.");
|
||||
rotom_database::mysql::run_pending_migrations(database_url)?;
|
||||
let repository_provider = rotom_database::mysql::repository_provider(database_url).await?;
|
||||
log::info!("Starting discord application.");
|
||||
crate::app::start(self.discord, repository_provider).await?;
|
||||
},
|
||||
#[cfg(feature = "postgres")]
|
||||
crate::cli::DatabaseDialect::Postgres => {
|
||||
log::info!("Running any pending database migrations.");
|
||||
rotom_database::postgres::run_pending_migrations(database_url)?;
|
||||
let repository_provider = rotom_database::postgres::repository_provider(database_url).await?;
|
||||
log::info!("Starting discord application.");
|
||||
crate::app::start(self.discord, repository_provider).await?;
|
||||
},
|
||||
#[cfg(feature = "sqlite")]
|
||||
crate::cli::DatabaseDialect::Sqlite => {
|
||||
log::info!("Running any pending database migrations.");
|
||||
rotom_database::sqlite::run_pending_migrations(database_url)?;
|
||||
let repository_provider = rotom_database::sqlite::repository_provider(database_url).await?;
|
||||
log::info!("Starting discord application.");
|
||||
crate::app::start(self.discord, repository_provider).await?;
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1 +1,20 @@
|
||||
fn main() {}
|
||||
mod app;
|
||||
mod cli;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
enum MainError {
|
||||
#[error(transparent)]
|
||||
CliError(#[from] cli::CliError),
|
||||
#[error(transparent)]
|
||||
CommandError(#[from] cli::command::CommandError)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), MainError> {
|
||||
let c = cli::parse()?;
|
||||
env_logger::init();
|
||||
|
||||
c.command.execute().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user