use std::collections::BTreeMap;
use clap::{Parser, Subcommand};
#[cfg(feature = "with-db")]
use sea_orm_migration::MigratorTrait;
#[cfg(feature = "with-db")]
use crate::boot::run_db;
use crate::{
app::{AppContext, Hooks},
boot::{create_app, create_context, list_endpoints, run_task, start, RunDbCommand, StartMode},
environment::resolve_from_env,
gen::{self, Component},
Result,
};
const DEFAULT_ENVIRONMENT: &str = "development";
#[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,
},
#[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,
},
}
#[derive(Subcommand)]
enum ComponentArg {
#[cfg(feature = "with-db")]
Model {
name: String,
#[clap(value_parser = parse_key_val::<String,String>)]
fields: Vec<(String, String)>,
},
#[cfg(feature = "with-db")]
Scaffold {
name: String,
#[clap(value_parser = parse_key_val::<String,String>)]
fields: Vec<(String, String)>,
},
Controller {
name: String,
},
Task {
name: String,
},
Worker {
name: String,
},
Mailer {
name: String,
},
}
impl From<ComponentArg> for Component {
fn from(value: ComponentArg) -> Self {
match value {
#[cfg(feature = "with-db")]
ComponentArg::Model { name, fields } => Self::Model { name, fields },
#[cfg(feature = "with-db")]
ComponentArg::Scaffold { name, fields } => Self::Scaffold { name, fields },
ComponentArg::Controller { name } => Self::Controller { name },
ComponentArg::Task { name } => Self::Task { name },
ComponentArg::Worker { name } => Self::Worker { name },
ComponentArg::Mailer { name } => Self::Mailer { name },
}
}
}
#[derive(Subcommand)]
enum DbCommands {
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,
}
}
}
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>() -> Result<AppContext> {
let cli = Playground::parse();
let environment = cli
.environment
.or_else(resolve_from_env)
.unwrap_or_else(|| DEFAULT_ENVIRONMENT.to_string());
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::parse();
let environment = cli
.environment
.or_else(resolve_from_env)
.unwrap_or_else(|| DEFAULT_ENVIRONMENT.to_string());
match cli.command {
Commands::Start {
worker,
server_and_worker,
} => {
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?;
start(boot_result).await?;
}
#[cfg(feature = "with-db")]
Commands::Db { command } => {
let app_context = create_context::<H>(&environment).await?;
run_db::<H, M>(&app_context, command.into()).await?;
}
Commands::Routes {} => show_list_endpoints::<H>(),
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(component.into())?;
}
}
Ok(())
}
#[cfg(not(feature = "with-db"))]
pub async fn main<H: Hooks>() -> eyre::Result<()> {
let cli = Cli::parse();
let environment = cli
.environment
.or_else(resolve_from_env)
.unwrap_or_else(|| DEFAULT_ENVIRONMENT.to_string());
match cli.command {
Commands::Start {
worker,
server_and_worker,
} => {
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?;
start(boot_result).await?;
}
Commands::Routes {} => show_list_endpoints::<H>(),
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(component.into())?;
}
}
Ok(())
}
fn show_list_endpoints<H: Hooks>() {
let mut routes = list_endpoints::<H>();
routes.sort_by(|a, b| a.uri.cmp(&b.uri));
for router in routes {
println!("{}", router.to_string());
}
}