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 std::path::PathBuf;
use clap::{ArgAction, Parser, Subcommand};
use duct::cmd;
use loco_gen::{Component, ScaffoldKind};
use crate::{
app::{AppContext, Hooks},
boot::{
create_app, create_context, list_endpoints, list_middlewares, run_scheduler, run_task,
start, RunDbCommand, ServeParams, StartMode,
},
config::Config,
environment::{resolve_from_env, Environment, DEFAULT_ENVIRONMENT},
logger, task, Error,
};
#[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 {
#[clap(alias("s"))]
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>,
#[arg(short, long, action = ArgAction::SetTrue)]
no_banner: bool,
},
#[cfg(feature = "with-db")]
Db {
#[command(subcommand)]
command: DbCommands,
},
Routes {},
Middleware {
#[arg(short, long, action)]
config: bool,
},
#[clap(alias("t"))]
Task {
name: Option<String>,
#[clap(value_parser = parse_key_val::<String,String>)]
params: Vec<(String, String)>,
},
Scheduler {
#[arg(short, long, action)]
name: Option<String>,
#[arg(short, long, action)]
tag: Option<String>,
#[clap(value_parser)]
#[arg(short, long, action)]
config: Option<PathBuf>,
#[arg(short, long, action)]
list: bool,
},
#[clap(alias("g"))]
Generate {
#[command(subcommand)]
component: ComponentArg,
},
#[cfg(feature = "with-db")]
Doctor {
#[arg(short, long, action)]
config: bool,
},
Version {},
#[clap(alias("w"))]
Watch {
#[arg(short, long, action)]
worker: bool,
#[arg(short, long, action)]
server_and_worker: bool,
},
}
#[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, group = "scaffold_kind_group")]
kind: Option<ScaffoldKind>,
#[clap(long, group = "scaffold_kind_group")]
htmx: bool,
#[clap(long, group = "scaffold_kind_group")]
html: bool,
#[clap(long, group = "scaffold_kind_group")]
api: bool,
},
Controller {
name: String,
actions: Vec<String>,
#[clap(short, long, value_enum, group = "scaffold_kind_group")]
kind: Option<ScaffoldKind>,
#[clap(long, group = "scaffold_kind_group")]
htmx: bool,
#[clap(long, group = "scaffold_kind_group")]
html: bool,
#[clap(long, group = "scaffold_kind_group")]
api: bool,
},
Task {
name: String,
},
Scheduler {},
Worker {
name: String,
},
Mailer {
name: String,
},
Deployment {},
}
impl ComponentArg {
fn into_gen_component(self, config: &Config) -> crate::Result<Component> {
match self {
#[cfg(feature = "with-db")]
Self::Model {
name,
link,
migration_only,
fields,
} => Ok(Component::Model {
name,
link,
migration_only,
fields,
}),
#[cfg(feature = "with-db")]
Self::Migration { name } => Ok(Component::Migration { name }),
#[cfg(feature = "with-db")]
Self::Scaffold {
name,
fields,
kind,
htmx,
html,
api,
} => {
let kind = if let Some(kind) = kind {
kind
} else if htmx {
ScaffoldKind::Htmx
} else if html {
ScaffoldKind::Html
} else if api {
ScaffoldKind::Api
} else {
return Err(crate::Error::string(
"Error: One of `kind`, `htmx`, `html`, or `api` must be specified.",
));
};
Ok(Component::Scaffold { name, fields, kind })
}
Self::Controller {
name,
actions,
kind,
htmx,
html,
api,
} => {
let kind = if let Some(kind) = kind {
kind
} else if htmx {
ScaffoldKind::Htmx
} else if html {
ScaffoldKind::Html
} else if api {
ScaffoldKind::Api
} else {
return Err(crate::Error::string(
"Error: One of `kind`, `htmx`, `html`, or `api` must be specified.",
));
};
Ok(Component::Controller {
name,
actions,
kind,
})
}
Self::Task { name } => Ok(Component::Task { name }),
Self::Scheduler {} => Ok(Component::Scheduler {}),
Self::Worker { name } => Ok(Component::Worker { name }),
Self::Mailer { name } => Ok(Component::Mailer { name }),
Self::Deployment {} => {
let copy_asset_folder = &config
.server
.middlewares
.static_assets
.clone()
.map(|a| a.folder.path);
let fallback_file = &config
.server
.middlewares
.static_assets
.clone()
.map(|a| a.fallback);
Ok(Component::Deployment {
asset_folder: copy_asset_folder.clone(),
fallback_file: fallback_file.clone(),
host: config.server.host.clone(),
port: config.server.port,
})
}
}
}
}
#[derive(Subcommand)]
enum DbCommands {
Create,
Migrate,
Down {
#[arg(default_value_t = 1)]
steps: u32,
},
Reset,
Status,
Entities,
Truncate,
}
impl From<DbCommands> for RunDbCommand {
fn from(value: DbCommands) -> Self {
match value {
DbCommands::Migrate => Self::Migrate,
DbCommands::Down { steps } => Self::Down(steps),
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")]
#[allow(clippy::too_many_lines)]
#[allow(clippy::cognitive_complexity)]
pub async fn main<H: Hooks, M: MigratorTrait>() -> crate::Result<()> {
use colored::Colorize;
use loco_gen::AppInfo;
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,
no_banner,
} => {
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
.unwrap_or_else(|| boot_result.app_context.config.server.binding.to_string()),
};
start::<H>(boot_result, serve_params, no_banner).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::Middleware { config } => {
let app_context = create_context::<H>(&environment).await?;
let middlewares = list_middlewares::<H>(&app_context);
for middleware in middlewares.iter().filter(|m| m.enabled) {
println!(
"{:<22} {}",
middleware.id.bold(),
if config {
middleware.detail.as_str()
} else {
""
}
);
}
println!("\n");
for middleware in middlewares.iter().filter(|m| !m.enabled) {
println!("{:<22} (disabled)", middleware.id.bold().dimmed(),);
}
}
Commands::Task { name, params } => {
let vars = task::Vars::from_cli_args(params);
let app_context = create_context::<H>(&environment).await?;
run_task::<H>(&app_context, name.as_ref(), &vars).await?;
}
Commands::Scheduler {
name,
config,
tag,
list,
} => {
let app_context = create_context::<H>(&environment).await?;
run_scheduler::<H>(&app_context, config.as_ref(), name, tag, list).await?;
}
Commands::Generate { component } => {
loco_gen::generate(
component.into_gen_component(&config)?,
&AppInfo {
app_name: H::app_name().to_string(),
},
)?;
}
Commands::Doctor { config: config_arg } => {
if config_arg {
println!("{}", &config);
println!("Environment: {}", &environment);
} else {
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(),);
}
Commands::Watch {
worker,
server_and_worker,
} => {
let mut subcmd = vec!["cargo", "loco", "start"];
if worker {
subcmd.push("--worker");
} else if server_and_worker {
subcmd.push("--server-and-worker");
}
cmd("cargo-watch", &["-s", &subcmd.join(" ")])
.run()
.map_err(|err| {
Error::Message(format!(
"failed to start with `cargo-watch`. Did you `cargo install \
cargo-watch`?. error details: `{err}`",
))
})?;
}
}
Ok(())
}
#[cfg(not(feature = "with-db"))]
pub async fn main<H: Hooks>() -> crate::Result<()> {
use colored::Colorize;
use loco_gen::AppInfo;
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,
no_banner,
} => {
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, no_banner).await?;
}
Commands::Routes {} => {
let app_context = create_context::<H>(&environment).await?;
show_list_endpoints::<H>(&app_context)
}
Commands::Middleware { config } => {
let app_context = create_context::<H>(&environment).await?;
let middlewares = list_middlewares::<H>(&app_context);
for middleware in middlewares.iter().filter(|m| m.enabled) {
println!(
"{:<22} {}",
middleware.id.bold(),
if config {
middleware.detail.as_str()
} else {
""
}
);
}
println!("\n");
for middleware in middlewares.iter().filter(|m| !m.enabled) {
println!("{:<22} (disabled)", middleware.id.bold().dimmed(),);
}
}
Commands::Task { name, params } => {
let vars = task::Vars::from_cli_args(params);
let app_context = create_context::<H>(&environment).await?;
run_task::<H>(&app_context, name.as_ref(), &vars).await?;
}
Commands::Scheduler {
name,
config,
tag,
list,
} => {
let app_context = create_context::<H>(&environment).await?;
run_scheduler::<H>(&app_context, config.as_ref(), name, tag, list).await?;
}
Commands::Generate { component } => {
loco_gen::generate(
component.into_gen_component(&config)?,
&AppInfo {
app_name: H::app_name().to_string(),
},
)?;
}
Commands::Version {} => {
println!("{}", H::app_version(),);
}
Commands::Watch {
worker,
server_and_worker,
} => {
let mut subcmd = vec!["cargo", "loco", "start"];
if worker {
subcmd.push("--worker");
} else if server_and_worker {
subcmd.push("--server-and-worker");
}
cmd("cargo-watch", &["-s", &subcmd.join(" ")])
.run()
.map_err(|err| {
Error::Message(format!(
"failed to start with `cargo-watch`. Did you `cargo install \
cargo-watch`?. error details: `{err}`",
))
})?;
}
}
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}");
}
}
fn create_root_span(environment: &Environment) -> tracing::Span {
tracing::span!(tracing::Level::DEBUG, "app", environment = %environment)
}