diff --git a/Cargo.lock b/Cargo.lock index 71af87e..6690d9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4353,11 +4353,13 @@ dependencies = [ "async-trait", "axum", "chrono", + "clap", "config", "database", "include_dir", "migration", "mime_guess", + "once_cell", "sea-orm", "serde", "serde_json", diff --git a/apps/api/Cargo.toml b/apps/api/Cargo.toml index 3d132ad..a3a1a00 100644 --- a/apps/api/Cargo.toml +++ b/apps/api/Cargo.toml @@ -21,3 +21,5 @@ sea-orm = { workspace = true } include_dir = { version = "0.7.4" } mime_guess = { version = "2.0.5" } utoipa = { version = "5.4.0", features = ["macros", "axum_extras", "chrono", "decimal", "uuid", "time", "openapi_extensions"] } +clap = { version = "4.5.53" } +once_cell = { version = "1.21.3" } diff --git a/apps/api/src/cmd.rs b/apps/api/src/cmd.rs new file mode 100644 index 0000000..ffb6eca --- /dev/null +++ b/apps/api/src/cmd.rs @@ -0,0 +1,45 @@ +use std::pin::Pin; +use std::{future::Future, process::exit}; + +use clap::{ArgMatches, Command}; + +pub mod generate_openapi; + +pub struct CliCommand { + pub command: Command, + pub action: fn(&clap::ArgMatches) -> Pin + Send>>, +} + +static CLI_COMMANDS: once_cell::sync::Lazy< + [CliCommand; 1 /* Update this count when adding new commands */], +> = + once_cell::sync::Lazy::new(|| { + [ + // Add new commands here + generate_openapi::get_cli_command(), + ] + }); + +pub fn get_command() -> Command { + let mut c = Command::new("cmd"); + + for cmd in CLI_COMMANDS.iter() { + c = c.subcommand(cmd.command.clone()); + } + + c +} + +pub fn execute(matches: &ArgMatches, help_msg: &str) -> Pin + Send>> { + if let Some((subcommand_name, subcommand_matches)) = matches.subcommand() { + for cmd in CLI_COMMANDS.iter() { + if cmd.command.get_name() == subcommand_name { + return (cmd.action)(subcommand_matches); + } + } + } + + eprintln!("Error: No valid subcommand provided."); + eprintln!("{}", help_msg); + exit(1); +} diff --git a/apps/api/src/cmd/generate_openapi.rs b/apps/api/src/cmd/generate_openapi.rs new file mode 100644 index 0000000..e227fed --- /dev/null +++ b/apps/api/src/cmd/generate_openapi.rs @@ -0,0 +1,38 @@ +use clap::{Arg, Command}; +use utoipa::OpenApi; + +use crate::{cmd::CliCommand, routes::ApiDoc}; + +pub fn get_cli_command() -> CliCommand { + CliCommand { + command: command(), + action, + } +} + +fn command() -> Command { + Command::new("generate:openapi") + .arg( + Arg::new("output_path") + .short('o') + .long("output-path") + .value_name("PATH") + .help("Path to output the generated OpenAPI documentation") + .required(true), + ) + .about("Generate OpenAPI documentation") +} + +fn action( + _matches: &clap::ArgMatches, +) -> std::pin::Pin + Send>> { + let output_path = _matches.get_one::("output_path"); + let output_path = output_path.unwrap().to_string(); + Box::pin(async move { + let doc = ApiDoc::openapi(); + let json = doc + .to_pretty_json() + .expect("Failed to serialize OpenAPI doc to JSON"); + std::fs::write(&output_path, json).expect("Failed to write OpenAPI doc to file"); + }) +} diff --git a/apps/api/src/main.rs b/apps/api/src/main.rs index dd1af30..f8a8ec9 100644 --- a/apps/api/src/main.rs +++ b/apps/api/src/main.rs @@ -1,3 +1,4 @@ +mod cmd; mod configs; mod errors; mod middlewares; @@ -21,15 +22,39 @@ use crate::{ #[tokio::main] async fn main() { - // Temporary subscriber for initial logging during configuration reading - let make_temporary_subscriber = || { - tracing_subscriber::fmt() - .with_max_level(tracing::Level::DEBUG) - .with_target(false) - .with_level(true) - .finish() - }; + // only run command line interface if arguments are provided + if std::env::args().len() > 1 { + process_commands().await; + return; + } + start_server().await; +} + +async fn process_commands() { + tracing::subscriber::with_default(make_temporary_subscriber(), async || { + use clap::error::ErrorKind; + + let mut command = cmd::get_command(); + let help_output = format!("{}", command.render_help()); + let matches = command + .try_get_matches() + .unwrap_or_else(|err| match err.kind() { + ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => { + err.print().expect("Error writing Error"); + std::process::exit(0); + } + _ => { + err.print().expect("Error writing Error"); + std::process::exit(1); + } + }); + cmd::execute(&matches, &help_output).await; + }) + .await +} + +async fn start_server() { let settings = tracing::subscriber::with_default(make_temporary_subscriber(), || -> ProgramSettings { debug!("Temporary subscriber installed."); @@ -86,6 +111,14 @@ async fn main() { .expect("Failed to run the server"); } +fn make_temporary_subscriber() -> tracing_subscriber::fmt::Subscriber { + tracing_subscriber::fmt() + .with_max_level(tracing::Level::DEBUG) + .with_target(false) + .with_level(true) + .finish() +} + fn get_global_tracing_subscriber_builder( settings: &LoggingSettings, ) -> tracing_subscriber::fmt::SubscriberBuilder<