feature/openapi #7

Merged
GW_MC merged 10 commits from feature/openapi into master 2025-12-05 20:50:37 +08:00
5 changed files with 128 additions and 8 deletions
Showing only changes of commit d2b842d933 - Show all commits

2
Cargo.lock generated
View File

@@ -4353,11 +4353,13 @@ dependencies = [
"async-trait",
"axum",
"chrono",
"clap",
"config",
"database",
"include_dir",
"migration",
"mime_guess",
"once_cell",
"sea-orm",
"serde",
"serde_json",

View File

@@ -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" }

45
apps/api/src/cmd.rs Normal file
View 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);
}

View 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");
})
}

View File

@@ -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<