pub mod commands;
pub(crate) mod parser;
pub use parser::{Database, DbAction, MakeKind, Stack, Subcommand, parse};
#[cfg(all(feature = "database", feature = "jobs"))]
pub use parser::QueueAction;
use std::ffi::OsString;
use std::process::ExitCode;
#[must_use]
pub fn run(args: impl IntoIterator<Item = OsString>) -> ExitCode {
let args: Vec<OsString> = args.into_iter().collect();
match parse(&args) {
Ok(cmd) => match execute(cmd) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("arc: {error}");
ExitCode::FAILURE
}
},
Err(error) => {
let failed = error.use_stderr();
let _ = error.print();
if failed {
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
}
}
}
}
fn execute(cmd: Subcommand) -> Result<(), CliError> {
match cmd {
Subcommand::New {
name,
dest,
stack,
database,
} => commands::new::run(&name, dest, stack, database).map_err(CliError::from),
Subcommand::Version => {
commands::version::run();
Ok(())
}
Subcommand::Serve { bind, port } => {
commands::serve::run(bind.as_deref(), port).map_err(CliError::from)
}
Subcommand::Migrate { dsn } => {
commands::migrate::run(dsn.as_deref()).map_err(CliError::from)
}
Subcommand::Schedule { dsn } => {
commands::schedule::run(dsn.as_deref()).map_err(CliError::from)
}
Subcommand::Make { kind, name } => {
let generated = commands::make::run(kind, &name).map_err(CliError::from)?;
generated.report();
Ok(())
}
#[cfg(feature = "auth")]
Subcommand::KeyGenerate { show } => {
commands::key_generate::run(show).map_err(CliError::from)
}
Subcommand::StorageLink => commands::storage_link::run().map_err(CliError::from),
Subcommand::Db { action, dsn, force } => {
commands::db::run(action, dsn.as_deref(), force).map_err(CliError::from)
}
#[cfg(all(feature = "database", feature = "jobs"))]
Subcommand::Queue { action, dsn } => {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|error| CliError::Runtime(error.to_string()))?;
runtime
.block_on(commands::queue::run(&action, dsn.as_deref()))
.map_err(CliError::from)
}
#[cfg(feature = "database")]
Subcommand::Doctor => commands::doctor::run().map_err(CliError::from),
Subcommand::Dev { port, host, open } => {
let options = commands::dev::options(port, host.as_deref(), open)
.map_err(|error| CliError::Command(error.to_string()))?;
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.map_err(|error| CliError::Runtime(error.to_string()))?;
runtime
.block_on(commands::dev::run(options))
.map_err(CliError::from)
}
#[cfg(feature = "uag")]
Subcommand::Routes { json } => commands::routes::run(json).map_err(CliError::from),
#[cfg(feature = "uag")]
Subcommand::Typegen => commands::typegen::run().map_err(CliError::from),
#[cfg(feature = "uag")]
Subcommand::Build => commands::build::run().map_err(CliError::from),
}
}
#[derive(Debug)]
pub enum CliError {
Command(String),
Runtime(String),
}
impl std::fmt::Display for CliError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Command(message) => formatter.write_str(message),
Self::Runtime(message) => write!(formatter, "failed to build runtime: {message}"),
}
}
}
impl std::error::Error for CliError {}
macro_rules! command_error {
($($(#[$gate:meta])* $path:path),* $(,)?) => {
$(
$(#[$gate])*
impl From<$path> for CliError {
fn from(error: $path) -> Self {
Self::Command(error.to_string())
}
}
)*
};
}
command_error! {
commands::dev::DevError,
commands::new::NewError,
commands::serve::ServeError,
commands::migrate::MigrateError,
commands::schedule::ScheduleError,
commands::make::MakeError,
commands::storage_link::StorageLinkError,
commands::db::DbError,
#[cfg(feature = "uag")]
commands::routes::RoutesError,
#[cfg(feature = "uag")]
commands::typegen::TypegenError,
#[cfg(feature = "uag")]
commands::build::BuildError,
#[cfg(feature = "auth")]
commands::key_generate::KeyGenerateError,
#[cfg(all(feature = "database", feature = "jobs"))]
commands::queue::QueueError,
#[cfg(feature = "database")]
commands::doctor::DoctorError,
}
#[cfg(test)]
mod tests {
use super::*;
fn argv(args: &[&str]) -> Vec<OsString> {
std::iter::once("arc")
.chain(args.iter().copied())
.map(OsString::from)
.collect()
}
#[test]
fn help_exits_successfully_because_it_is_not_a_failure() {
assert_eq!(run(argv(&["--help"])), ExitCode::SUCCESS);
}
#[test]
fn an_unknown_subcommand_exits_with_a_failure_code() {
assert_eq!(run(argv(&["migrat"])), ExitCode::FAILURE);
}
#[test]
fn a_bare_invocation_shows_help_and_fails() {
assert_eq!(run(argv(&[])), ExitCode::FAILURE);
}
#[cfg(feature = "uag")]
#[test]
fn typegen_outside_a_project_exits_with_a_failure_code() {
let previous = std::env::current_dir().expect("a working directory");
let empty = std::env::temp_dir().join(format!("arcature-cli-{}", std::process::id()));
std::fs::create_dir_all(&empty).expect("temp dir");
std::env::set_current_dir(&empty).expect("chdir");
let code = run(argv(&["typegen"]));
std::env::set_current_dir(previous).expect("chdir back");
assert_eq!(code, ExitCode::FAILURE);
}
#[test]
fn a_destructive_db_command_without_force_exits_with_a_failure_code() {
assert_eq!(run(argv(&["db:fresh"])), ExitCode::FAILURE);
}
}