use std::{
ffi::OsString,
fs::create_dir_all,
io::{self, ErrorKind},
path::PathBuf,
};
use anyhow::Context;
use cargo_metadata::MetadataCommand;
use clap::builder::{OsStringValueParser, PossibleValue, TypedValueParser};
use clap::Parser;
use clap_complete::Shell;
use shuttle_common::{models::project::IDLE_MINUTES, project::ProjectName};
use uuid::Uuid;
use crate::init::Framework;
#[derive(Parser)]
#[command(
version,
about,
arg(clap::Arg::new("dummy")
.value_parser([PossibleValue::new("shuttle")])
.required(false)
.hide(true))
)]
pub struct Args {
#[arg(long, env = "SHUTTLE_API")]
pub api_url: Option<String>,
#[command(flatten)]
pub project_args: ProjectArgs,
#[command(subcommand)]
pub cmd: Command,
}
#[derive(Parser, Debug)]
pub struct ProjectArgs {
#[arg(global = true, long, default_value = ".", value_parser = OsStringValueParser::new().try_map(parse_path))]
pub working_directory: PathBuf,
#[arg(global = true, long)]
pub name: Option<ProjectName>,
}
impl ProjectArgs {
pub fn workspace_path(&self) -> anyhow::Result<PathBuf> {
let path = MetadataCommand::new()
.current_dir(&self.working_directory)
.exec()
.context("failed to get cargo metadata")?
.workspace_root
.into();
Ok(path)
}
pub fn project_name(&self) -> anyhow::Result<ProjectName> {
let workspace_path = self.workspace_path()?;
let meta = MetadataCommand::new()
.current_dir(&workspace_path)
.exec()
.unwrap();
let package_name = if let Some(root_package) = meta.root_package() {
root_package.name.clone().parse()?
} else {
workspace_path
.file_name()
.context("failed to get project name from workspace path")?
.to_os_string()
.into_string()
.expect("workspace file name should be valid unicode")
.parse()?
};
Ok(package_name)
}
}
#[derive(Parser)]
pub enum Command {
Deploy(DeployArgs),
#[command(subcommand)]
Deployment(DeploymentCommand),
#[command(subcommand)]
Resource(ResourceCommand),
Init(InitArgs),
Generate {
#[arg(short, long, env, default_value_t = Shell::Bash)]
shell: Shell,
#[arg(short, long, env)]
output: Option<PathBuf>,
},
Status,
Logs {
id: Option<Uuid>,
#[arg(short, long)]
follow: bool,
},
Clean,
Stop,
Secrets,
Login(LoginArgs),
Logout,
Run(RunArgs),
Feedback,
#[command(subcommand)]
Project(ProjectCommand),
}
#[derive(Parser)]
pub enum DeploymentCommand {
List,
Status {
id: Uuid,
},
}
#[derive(Parser)]
pub enum ResourceCommand {
List,
}
#[derive(Parser)]
pub enum ProjectCommand {
Start {
#[arg(long, default_value_t = IDLE_MINUTES)]
idle_minutes: u64,
},
Status {
#[arg(short, long)]
follow: bool,
},
Stop,
Restart {
#[arg(long, default_value_t = IDLE_MINUTES)]
idle_minutes: u64,
},
List {
#[arg(long)]
filter: Option<String>,
},
}
#[derive(Parser, Clone, Debug)]
pub struct LoginArgs {
#[arg(long)]
pub api_key: Option<String>,
}
#[derive(Parser)]
pub struct DeployArgs {
#[arg(long)]
pub allow_dirty: bool,
#[arg(long)]
pub no_test: bool,
}
#[derive(Parser, Debug)]
pub struct RunArgs {
#[arg(long, env, default_value = "8000")]
pub port: u16,
#[arg(long)]
pub external: bool,
#[arg(long, short = 'r')]
pub release: bool,
}
#[derive(Parser, Debug)]
pub struct InitArgs {
#[arg(long="actix_web", conflicts_with_all = &["axum", "rocket", "tide", "tower", "poem", "serenity", "poise", "warp", "salvo", "thruster", "no_framework"])]
pub actix_web: bool,
#[arg(long, conflicts_with_all = &["actix_web","rocket", "tide", "tower", "poem", "serenity", "poise", "warp", "salvo", "thruster", "no_framework"])]
pub axum: bool,
#[arg(long, conflicts_with_all = &["actix_web","axum", "tide", "tower", "poem", "serenity", "poise", "warp", "salvo", "thruster", "no_framework"])]
pub rocket: bool,
#[arg(long, conflicts_with_all = &["actix_web","axum", "rocket", "tower", "poem", "serenity", "poise", "warp", "salvo", "thruster", "no_framework"])]
pub tide: bool,
#[arg(long, conflicts_with_all = &["actix_web","axum", "rocket", "tide", "poem", "serenity", "poise", "warp", "salvo", "thruster", "no_framework"])]
pub tower: bool,
#[arg(long, conflicts_with_all = &["actix_web","axum", "rocket", "tide", "tower", "serenity", "poise", "warp", "salvo", "thruster", "no_framework"])]
pub poem: bool,
#[arg(long, conflicts_with_all = &["actix_web","axum", "rocket", "tide", "tower", "poem", "warp", "serenity", "poise", "thruster", "no_framework"])]
pub salvo: bool,
#[arg(long, conflicts_with_all = &["actix_web","axum", "rocket", "tide", "tower", "poem", "warp", "poise", "salvo", "thruster", "no_framework"])]
pub serenity: bool,
#[clap(long, conflicts_with_all = &["actix_web","axum", "rocket", "tide", "tower", "poem", "warp", "serenity", "salvo", "thruster", "no_framework"])]
pub poise: bool,
#[arg(long, conflicts_with_all = &["actix_web","axum", "rocket", "tide", "tower", "poem", "serenity", "poise", "salvo", "thruster", "no_framework"])]
pub warp: bool,
#[arg(long, conflicts_with_all = &["actix_web","axum", "rocket", "tide", "tower", "poem", "warp", "salvo", "serenity", "poise", "no_framework"])]
pub thruster: bool,
#[arg(long, conflicts_with_all = &["actix_web","axum", "rocket", "tide", "tower", "poem", "warp", "salvo", "serenity", "poise", "thruster"])]
pub no_framework: bool,
#[arg(long)]
pub new: bool,
#[command(flatten)]
pub login_args: LoginArgs,
#[arg(default_value = ".", value_parser = OsStringValueParser::new().try_map(parse_init_path) )]
pub path: PathBuf,
}
impl InitArgs {
pub fn framework(&self) -> Option<Framework> {
if self.actix_web {
Some(Framework::ActixWeb)
} else if self.axum {
Some(Framework::Axum)
} else if self.rocket {
Some(Framework::Rocket)
} else if self.tide {
Some(Framework::Tide)
} else if self.tower {
Some(Framework::Tower)
} else if self.poem {
Some(Framework::Poem)
} else if self.salvo {
Some(Framework::Salvo)
} else if self.poise {
Some(Framework::Poise)
} else if self.serenity {
Some(Framework::Serenity)
} else if self.warp {
Some(Framework::Warp)
} else if self.thruster {
Some(Framework::Thruster)
} else if self.no_framework {
Some(Framework::None)
} else {
None
}
}
}
fn parse_path(path: OsString) -> Result<PathBuf, String> {
dunce::canonicalize(&path).map_err(|e| format!("could not turn {path:?} into a real path: {e}"))
}
pub(crate) fn parse_init_path(path: OsString) -> Result<PathBuf, io::Error> {
create_dir_all(&path)?;
parse_path(path.clone()).map_err(|e| {
io::Error::new(
ErrorKind::InvalidInput,
format!("could not turn {path:?} into a real path: {e}"),
)
})
}
#[cfg(test)]
mod tests {
use strum::IntoEnumIterator;
use crate::tests::path_from_workspace_root;
use super::*;
fn init_args_factory(framework: &str) -> InitArgs {
let mut init_args = InitArgs {
actix_web: false,
axum: false,
rocket: false,
tide: false,
tower: false,
poem: false,
salvo: false,
serenity: false,
poise: false,
warp: false,
thruster: false,
no_framework: false,
new: false,
login_args: LoginArgs { api_key: None },
path: PathBuf::new(),
};
match framework {
"actix-web" => init_args.actix_web = true,
"axum" => init_args.axum = true,
"rocket" => init_args.rocket = true,
"tide" => init_args.tide = true,
"tower" => init_args.tower = true,
"poem" => init_args.poem = true,
"salvo" => init_args.salvo = true,
"serenity" => init_args.serenity = true,
"poise" => init_args.poise = true,
"warp" => init_args.warp = true,
"thruster" => init_args.thruster = true,
"none" => init_args.no_framework = true,
_ => unreachable!(),
}
init_args
}
#[test]
fn test_init_args_framework() {
for framework in Framework::iter() {
let args = init_args_factory(&framework.to_string());
assert_eq!(args.framework(), Some(framework));
}
}
#[test]
fn workspace_path() {
let project_args = ProjectArgs {
working_directory: path_from_workspace_root("examples/axum/hello-world/src"),
name: None,
};
assert_eq!(
project_args.workspace_path().unwrap(),
path_from_workspace_root("examples/axum/hello-world/")
);
}
#[test]
fn project_name() {
let project_args = ProjectArgs {
working_directory: path_from_workspace_root("examples/axum/hello-world/src"),
name: None,
};
assert_eq!(
project_args.project_name().unwrap().to_string(),
"hello-world"
);
}
#[test]
fn project_name_in_workspace() {
let project_args = ProjectArgs {
working_directory: path_from_workspace_root(
"examples/rocket/workspace/hello-world/src",
),
name: None,
};
assert_eq!(
project_args.project_name().unwrap().to_string(),
"workspace"
);
}
}