use std::collections::BTreeMap;
cfg_if::cfg_if! {
if #[cfg(feature = "with-db")] {
use sea_orm_migration::MigratorTrait;
use crate::doctor;
use crate::boot::{run_db};
use crate::db;
use std::process::exit;
} else {}
}
use clap::{Parser, Subcommand};
use crate::{
app::{AppContext, Hooks},
boot::{
create_app, create_context, list_endpoints, run_task, start, RunDbCommand, ServeParams,
StartMode,
},
environment::{resolve_from_env, Environment, DEFAULT_ENVIRONMENT},
gen::{self, Component},
logger,
};
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
#[command(propagate_version = true)]
struct Playground {
#[arg(short, long, global = true, help = &format!("Specify the environment [default: {}]", DEFAULT_ENVIRONMENT))]
environment: Option<String>,
}
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
#[command(propagate_version = true)]
struct Cli {
#[command(subcommand)]
command: Commands,
#[arg(short, long, global = true, help = &format!("Specify the environment [default: {}]", DEFAULT_ENVIRONMENT))]
environment: Option<String>,
}
#[derive(Subcommand)]
enum Commands {
Start {
#[arg(short, long, action)]
worker: bool,
#[arg(short, long, action)]
server_and_worker: bool,
#[arg(short, long, action)]
binding: Option<String>,
#[arg(short, long, action)]
port: Option<i32>,
},
#[cfg(feature = "with-db")]
Db {
#[command(subcommand)]
command: DbCommands,
},
Routes {},
Task {
name: Option<String>,
#[clap(value_parser = parse_key_val::<String,String>)]
params: Vec<(String, String)>,
},
Generate {
#[command(subcommand)]
component: ComponentArg,
},
#[cfg(feature = "with-db")]
Doctor {},
Version {},
}
#[derive(Subcommand)]
enum ComponentArg {
#[cfg(feature = "with-db")]
Model {
name: String,
#[arg(short, long, action)]
link: bool,
#[arg(short, long, action)]
migration_only: bool,
#[clap(value_parser = parse_key_val::<String,String>)]
fields: Vec<(String, String)>,
},
#[cfg(feature = "with-db")]
Migration {
name: String,
},
#[cfg(feature = "with-db")]
Scaffold {
name: String,
#[clap(value_parser = parse_key_val::<String,String>)]
fields: Vec<(String, String)>,
#[clap(short, long, value_enum, default_value_t = gen::ScaffoldKind::Api)]
kind: gen::ScaffoldKind,
},
Controller {
name: String,
},
Task {
name: String,
},
Worker {
name: String,
},
Mailer {
name: String,
},
Deployment {},
}
impl From<ComponentArg> for Component {
fn from(value: ComponentArg) -> Self {
match value {
#[cfg(feature = "with-db")]
ComponentArg::Model {
name,
link,
migration_only,
fields,
} => Self::Model {
name,
link,
migration_only,
fields,
},
#[cfg(feature = "with-db")]
ComponentArg::Migration { name } => Self::Migration { name },
#[cfg(feature = "with-db")]
ComponentArg::Scaffold { name, fields, kind } => Self::Scaffold { name, fields, kind },
ComponentArg::Controller { name } => Self::Controller { name },
ComponentArg::Task { name } => Self::Task { name },
ComponentArg::Worker { name } => Self::Worker { name },
ComponentArg::Mailer { name } => Self::Mailer { name },
ComponentArg::Deployment {} => Self::Deployment {},
}
}
}
#[derive(Subcommand)]
enum DbCommands {
Create,
Migrate,
Reset,
Status,
Entities,
Truncate,
}
impl From<DbCommands> for RunDbCommand {
fn from(value: DbCommands) -> Self {
match value {
DbCommands::Migrate => Self::Migrate,
DbCommands::Reset => Self::Reset,
DbCommands::Status => Self::Status,
DbCommands::Entities => Self::Entities,
DbCommands::Truncate => Self::Truncate,
DbCommands::Create => {
unreachable!("Create db should't handled in the global db commands")
}
}
}
}
fn parse_key_val<T, U>(
s: &str,
) -> std::result::Result<(T, U), Box<dyn std::error::Error + Send + Sync>>
where
T: std::str::FromStr,
T::Err: std::error::Error + Send + Sync + 'static,
U: std::str::FromStr,
U::Err: std::error::Error + Send + Sync + 'static,
{
let pos = s
.find(':')
.ok_or_else(|| format!("invalid KEY=value: no `:` found in `{s}`"))?;
Ok((s[..pos].parse()?, s[pos + 1..].parse()?))
}
#[cfg(feature = "with-db")]
pub async fn playground<H: Hooks>() -> crate::Result<AppContext> {
let cli = Playground::parse();
let environment: Environment = cli.environment.unwrap_or_else(resolve_from_env).into();
let app_context = create_context::<H>(&environment).await?;
Ok(app_context)
}
#[cfg(feature = "with-db")]
pub async fn main<H: Hooks, M: MigratorTrait>() -> eyre::Result<()> {
let cli: Cli = Cli::parse();
let environment: Environment = cli.environment.unwrap_or_else(resolve_from_env).into();
let config = environment.load()?;
if !H::init_logger(&config, &environment)? {
logger::init::<H>(&config.logger);
}
let task_span = create_root_span(&environment);
let _guard = task_span.enter();
match cli.command {
Commands::Start {
worker,
server_and_worker,
binding,
port,
} => {
let start_mode = if worker {
StartMode::WorkerOnly
} else if server_and_worker {
StartMode::ServerAndWorker
} else {
StartMode::ServerOnly
};
let boot_result = create_app::<H, M>(start_mode, &environment).await?;
let serve_params = ServeParams {
port: port.map_or(boot_result.app_context.config.server.port, |p| p),
binding: binding.map_or(
boot_result.app_context.config.server.binding.to_string(),
|b| b,
),
};
start::<H>(boot_result, serve_params).await?;
}
#[cfg(feature = "with-db")]
Commands::Db { command } => {
if matches!(command, DbCommands::Create) {
db::create(&environment.load()?.database.uri).await?;
} else {
let app_context = create_context::<H>(&environment).await?;
run_db::<H, M>(&app_context, command.into()).await?;
}
}
Commands::Routes {} => {
let app_context = create_context::<H>(&environment).await?;
show_list_endpoints::<H>(&app_context);
}
Commands::Task { name, params } => {
let mut hash = BTreeMap::new();
for (k, v) in params {
hash.insert(k, v);
}
let app_context = create_context::<H>(&environment).await?;
run_task::<H>(&app_context, name.as_ref(), &hash).await?;
}
Commands::Generate { component } => {
gen::generate::<H>(component.into(), &config)?;
}
Commands::Doctor {} => {
let mut should_exit = false;
for (_, check) in doctor::run_all(&config).await {
if !should_exit && !check.valid() {
should_exit = true;
}
println!("{check}");
}
if should_exit {
exit(1);
}
}
Commands::Version {} => {
println!("{}", H::app_version(),);
}
}
Ok(())
}
#[cfg(not(feature = "with-db"))]
pub async fn main<H: Hooks>() -> eyre::Result<()> {
let cli = Cli::parse();
let environment: Environment = cli.environment.unwrap_or_else(resolve_from_env).into();
let config = environment.load()?;
if !H::init_logger(&config, &environment)? {
logger::init::<H>(&config.logger);
}
let task_span = create_root_span(&environment);
let _guard = task_span.enter();
match cli.command {
Commands::Start {
worker,
server_and_worker,
binding,
port,
} => {
let start_mode = if worker {
StartMode::WorkerOnly
} else if server_and_worker {
StartMode::ServerAndWorker
} else {
StartMode::ServerOnly
};
let boot_result = create_app::<H>(start_mode, &environment).await?;
let serve_params = ServeParams {
port: port.map_or(boot_result.app_context.config.server.port, |p| p),
binding: binding.map_or(
boot_result.app_context.config.server.binding.to_string(),
|b| b,
),
};
start::<H>(boot_result, serve_params).await?;
}
Commands::Routes {} => {
let app_context = create_context::<H>(&environment).await?;
show_list_endpoints::<H>(&app_context)
}
Commands::Task { name, params } => {
let mut hash = BTreeMap::new();
for (k, v) in params {
hash.insert(k, v);
}
let app_context = create_context::<H>(&environment).await?;
run_task::<H>(&app_context, name.as_ref(), &hash).await?;
}
Commands::Generate { component } => {
gen::generate::<H>(component.into(), &config)?;
}
Commands::Version {} => {
println!("{}", H::app_version(),);
}
}
Ok(())
}
fn show_list_endpoints<H: Hooks>(ctx: &AppContext) {
let mut routes = list_endpoints::<H>(ctx);
routes.sort_by(|a, b| a.uri.cmp(&b.uri));
for router in routes {
println!("{}", router.to_string());
}
}
fn create_root_span(environment: &Environment) -> tracing::Span {
tracing::span!(tracing::Level::DEBUG, "app", environment = %environment)
}