Add CLI command for generating OpenAPI documentation
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -4353,11 +4353,13 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"clap",
|
||||||
"config",
|
"config",
|
||||||
"database",
|
"database",
|
||||||
"include_dir",
|
"include_dir",
|
||||||
"migration",
|
"migration",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
|
"once_cell",
|
||||||
"sea-orm",
|
"sea-orm",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|||||||
@@ -21,3 +21,5 @@ sea-orm = { workspace = true }
|
|||||||
include_dir = { version = "0.7.4" }
|
include_dir = { version = "0.7.4" }
|
||||||
mime_guess = { version = "2.0.5" }
|
mime_guess = { version = "2.0.5" }
|
||||||
utoipa = { version = "5.4.0", features = ["macros", "axum_extras", "chrono", "decimal", "uuid", "time", "openapi_extensions"] }
|
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" }
|
||||||
|
|||||||
45
apps/api/src/cmd.rs
Normal file
45
apps/api/src/cmd.rs
Normal file
@@ -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<Box<dyn std::future::Future<Output = ()> + 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<Box<dyn Future<Output = ()> + 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);
|
||||||
|
}
|
||||||
38
apps/api/src/cmd/generate_openapi.rs
Normal file
38
apps/api/src/cmd/generate_openapi.rs
Normal file
@@ -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<Box<dyn std::future::Future<Output = ()> + Send>> {
|
||||||
|
let output_path = _matches.get_one::<String>("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");
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
mod cmd;
|
||||||
mod configs;
|
mod configs;
|
||||||
mod errors;
|
mod errors;
|
||||||
mod middlewares;
|
mod middlewares;
|
||||||
@@ -21,15 +22,39 @@ use crate::{
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
// Temporary subscriber for initial logging during configuration reading
|
// only run command line interface if arguments are provided
|
||||||
let make_temporary_subscriber = || {
|
if std::env::args().len() > 1 {
|
||||||
tracing_subscriber::fmt()
|
process_commands().await;
|
||||||
.with_max_level(tracing::Level::DEBUG)
|
return;
|
||||||
.with_target(false)
|
}
|
||||||
.with_level(true)
|
|
||||||
.finish()
|
|
||||||
};
|
|
||||||
|
|
||||||
|
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 =
|
let settings =
|
||||||
tracing::subscriber::with_default(make_temporary_subscriber(), || -> ProgramSettings {
|
tracing::subscriber::with_default(make_temporary_subscriber(), || -> ProgramSettings {
|
||||||
debug!("Temporary subscriber installed.");
|
debug!("Temporary subscriber installed.");
|
||||||
@@ -86,6 +111,14 @@ async fn main() {
|
|||||||
.expect("Failed to run the server");
|
.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(
|
fn get_global_tracing_subscriber_builder(
|
||||||
settings: &LoggingSettings,
|
settings: &LoggingSettings,
|
||||||
) -> tracing_subscriber::fmt::SubscriberBuilder<
|
) -> tracing_subscriber::fmt::SubscriberBuilder<
|
||||||
|
|||||||
Reference in New Issue
Block a user