1
0
Fork 0
mirror of https://codeberg.org/icewind/haze.git synced 2026-06-03 09:04:12 +02:00
haze/src/args.rs

694 lines
21 KiB
Rust

use crate::cloud::CloudOptions;
use crate::config::{HazeConfig, Preset};
use crate::service::{Service, ServiceTrait};
use miette::{IntoDiagnostic, Report, Result};
use std::fmt::{Debug, Display};
use std::rc::Rc;
use std::str::FromStr;
use strum::{
Display, EnumIter, EnumMessage, EnumProperty, EnumString, IntoEnumIterator, IntoStaticStr,
};
pub trait SubCommand: Display + EnumMessage + EnumProperty + Debug {
fn parent(&self) -> &'static str {
""
}
fn allows_filter(&self) -> bool {
false
}
fn sub_commands(&self) -> Option<Box<dyn Iterator<Item = Box<dyn SubCommand>>>> {
None
}
}
#[derive(Debug, Clone)]
pub enum HazeArgs {
List {
filter: Option<String>,
},
Start {
options: CloudOptions,
},
Stop {
filter: Option<String>,
},
Test {
options: CloudOptions,
args: Vec<String>,
},
Exec {
filter: Option<String>,
service: Option<ExecService>,
command: Vec<String>,
root: bool,
},
Occ {
filter: Option<String>,
command: Vec<String>,
},
Db {
filter: Option<String>,
root: bool,
command: Vec<String>,
index: Option<String>,
},
Clean,
Logs {
filter: Option<String>,
follow: bool,
service: Option<LogService>,
count: Option<usize>,
},
Open {
filter: Option<String>,
},
Fmt {
path: String,
},
Integration {
options: CloudOptions,
args: Vec<String>,
},
Shell {
options: CloudOptions,
command: Vec<String>,
},
Pin {
filter: Option<String>,
},
Unpin {
filter: Option<String>,
},
Proxy,
Git {
operation: GitOperation,
},
Env {
filter: Option<String>,
command: String,
args: Vec<String>,
},
Update,
Help {
command: Option<Rc<dyn SubCommand>>,
},
Version,
Edit {
filter: Option<String>,
path: String,
},
Reload {
filter: Option<String>,
},
}
#[derive(
Debug,
Clone,
Eq,
PartialEq,
Display,
EnumProperty,
EnumString,
IntoStaticStr,
EnumMessage,
EnumIter,
)]
#[strum(serialize_all = "lowercase")]
pub enum GitOperation {
/// Pull the active branch in all apps
Pull,
/// Checkout a branch in all apps
///
/// "main" and "master" can be used interchangeably.
#[strum(props(
Args = "[branch] branch to checkout, defaults to the branch matching the current server version [-v] verbose"
))]
Checkout {
branch: Option<String>,
verbose: bool,
},
}
impl SubCommand for GitOperation {
fn parent(&self) -> &'static str {
" git"
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum LogService {
Service(Service),
Database,
}
impl LogService {
pub fn from_type(presets: &[Preset], ty: &str) -> Option<Self> {
if ty == "db" {
return Some(LogService::Database);
}
Some(LogService::Service(
Service::from_type(presets, ty)?.into_iter().next()?,
))
}
pub fn container_name(&self, cloud_id: &str) -> Option<String> {
match self {
LogService::Database => Some(format!("{}-db", cloud_id)),
LogService::Service(service) => service.container_name(cloud_id),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum ExecService {
Db,
}
impl HazeArgs {
pub fn parse<I, S>(config: &HazeConfig, mut args: I) -> Result<HazeArgs>
where
S: AsRef<str> + Into<String> + Display,
I: Iterator<Item = S>,
{
let _bin = args.next();
let command_or_filter = match args.next() {
Some(s) => s,
None => return Ok(HazeArgs::List { filter: None }),
};
let (cmd, filter) = match HazeCommand::from_str(command_or_filter.as_ref()) {
Ok(cmd) => (cmd, None),
Err(_) => {
let cmd = match args.next() {
Some(cmd) => HazeCommand::from_str(cmd.as_ref()).into_diagnostic()?,
None => {
return Ok(HazeArgs::List {
filter: Some(command_or_filter.into()),
});
}
};
if !cmd.allows_filter() {
return Err(Report::msg(format!(
"{} doesn't allow specifying a filter",
cmd
)));
}
(cmd, Some(command_or_filter.into()))
}
};
match cmd {
HazeCommand::List => Ok(HazeArgs::List {
filter: filter.or_else(|| args.next().map(S::into)),
}),
HazeCommand::Start => {
let mut args = args.peekable();
let options = CloudOptions::parse(config, &mut args)?;
if let Some(leftover) = args.next() {
return Err(Report::msg(format!("unrecognized option {}", leftover)));
}
Ok(HazeArgs::Start { options })
}
HazeCommand::Stop => Ok(HazeArgs::Stop { filter }),
HazeCommand::Test => {
let mut args = args.peekable();
let options = CloudOptions::parse(config, &mut args)?;
let args = args.map(S::into).collect();
Ok(HazeArgs::Test { options, args })
}
HazeCommand::Integration => {
let mut args = args.peekable();
let options = CloudOptions::parse(config, &mut args)?;
let args = args.map(S::into).collect();
Ok(HazeArgs::Integration { options, args })
}
HazeCommand::Exec => {
let mut args = args.peekable();
let service = match args.peek() {
Some(arg) if arg.as_ref() == "db" => {
args.next();
Some(ExecService::Db)
}
_ => None,
};
let root = match args.peek() {
Some(arg) if arg.as_ref() == "root" => {
args.next();
true
}
_ => false,
};
let command = args.map(S::into).collect();
Ok(HazeArgs::Exec {
filter,
service,
command,
root,
})
}
HazeCommand::Occ => Ok(HazeArgs::Occ {
filter,
command: args.map(S::into).collect(),
}),
HazeCommand::Db => {
let mut args = args.peekable();
let root = if let Some(first) = args.peek() {
let root = first.as_ref() == "root";
if root {
let _ = args.next();
}
root
} else {
false
};
let index = if let Some(next) = args.peek() {
let all = next.as_ref() == "all";
let is_number = next.as_ref().chars().all(char::is_numeric);
if all || is_number {
args.next().map(Into::into)
} else {
None
}
} else {
None
};
let command = args.map(S::into).collect();
Ok(HazeArgs::Db {
filter,
root,
command,
index,
})
}
HazeCommand::Clean => Ok(HazeArgs::Clean),
HazeCommand::Logs => {
let mut args = args.peekable();
let follow = args.next_if(|arg| arg.as_ref() == "-f").is_some();
let service = args
.next_if(|arg| LogService::from_type(&config.preset, arg.as_ref()).is_some())
.and_then(|arg| LogService::from_type(&config.preset, arg.as_ref()));
Ok(HazeArgs::Logs {
filter,
follow,
service,
count: args
.next()
.map(|arg| arg.as_ref().parse())
.transpose()
.into_diagnostic()?,
})
}
HazeCommand::Open => Ok(HazeArgs::Open { filter }),
HazeCommand::Fmt => {
let path = args
.next()
.map(S::into)
.ok_or_else(|| Report::msg("No path provided"))?;
Ok(HazeArgs::Fmt { path })
}
HazeCommand::Shell => {
let mut args = args.peekable();
let options = CloudOptions::parse(config, &mut args)?;
let command = args.map(S::into).collect();
Ok(HazeArgs::Shell { options, command })
}
HazeCommand::Pin => Ok(HazeArgs::Pin { filter }),
HazeCommand::Unpin => Ok(HazeArgs::Unpin { filter }),
HazeCommand::Proxy => Ok(HazeArgs::Proxy),
HazeCommand::Checkout => {
let branch = args.next().map(S::into);
Ok(HazeArgs::Git {
operation: GitOperation::Checkout {
branch,
verbose: false,
},
})
}
HazeCommand::Git => {
let mut args = args.peekable();
let operation = args
.next()
.ok_or_else(|| Report::msg("No git operation provided"))?;
match operation.as_ref() {
"checkout" => {
let verbose = args.next_if(|arg| arg.as_ref() == "-v").is_some();
let branch = args.next().map(S::into);
let verbose = verbose | args.next_if(|arg| arg.as_ref() == "-v").is_some();
Ok(HazeArgs::Git {
operation: GitOperation::Checkout { branch, verbose },
})
}
"pull" => Ok(HazeArgs::Git {
operation: GitOperation::Pull,
}),
operation => Err(Report::msg(format!("Unknown git operation {operation}"))),
}
}
HazeCommand::Env => {
let mut args = args.map(S::into);
let command = args
.next()
.ok_or_else(|| Report::msg("No command provided"))?;
Ok(HazeArgs::Env {
filter,
command,
args: args.collect(),
})
}
HazeCommand::Update => Ok(HazeArgs::Update),
HazeCommand::Help => {
let command = args
.next()
.map(|command| match command.as_ref() {
"git" => match args.next() {
Some(op) => GitOperation::from_str(op.as_ref())
.map(|op| Rc::new(op) as Rc<dyn SubCommand>)
.map_err(|_| Report::msg("Unknown command")),
None => Ok(Rc::new(HazeCommand::Git) as Rc<dyn SubCommand>),
},
command => HazeCommand::from_str(command)
.map(|command| Rc::new(command) as Rc<dyn SubCommand>)
.map_err(|_| Report::msg("Unknown command")),
})
.transpose()?;
Ok(HazeArgs::Help { command })
}
HazeCommand::Version => Ok(HazeArgs::Version),
HazeCommand::Edit => Ok(HazeArgs::Edit {
filter,
path: args
.next()
.ok_or_else(|| Report::msg("No path provided"))?
.into(),
}),
HazeCommand::Reload => Ok(HazeArgs::Reload { filter }),
}
}
}
#[derive(
Debug,
Clone,
Copy,
Eq,
PartialEq,
Display,
IntoStaticStr,
EnumIter,
EnumString,
EnumMessage,
EnumProperty,
)]
#[strum(serialize_all = "lowercase")]
pub enum HazeCommand {
/// List all instances
List,
/// Start a new instance
#[strum(props(InstanceArgs = true))]
Start,
/// Stop an instance
Stop,
/// Run tests in a new instance
#[strum(props(
InstanceArgs = true,
Args = "[phpunit arguments] arguments to pass to phpunit"
))]
Test,
/// Run a command in an instance
#[strum(props(
Args = "[service] run command on a service container instead [command] command to run"
))]
Exec,
/// Run an occ command in an instance
#[strum(props(Args = "[occ arguments] arguments to pass to occ"))]
Occ,
/// Connect to the database of an instance
#[strum(props(
Args = "[root] connect to the database as root [db index] database instance to use for sharded setup [sql] sql command to run"
))]
Db,
/// Remove all non-pinned instances
Clean,
/// View the logs from an instance or service
#[strum(props(
Args = "[service] service to show logs from [follow] show logs lines as they appear [count] number of lines to show"
))]
Logs,
/// Open an instance in the browser
Open,
/// Run code formatting from a new instance
#[strum(props(Args = "[path] path to format"))]
Fmt,
/// Run integration tests in a new instance
#[strum(props(InstanceArgs = true, Args = "[args] arguments to pass to behat"))]
Integration,
/// Start a shell in an empirical instance
#[strum(props(InstanceArgs = true, Args = "[command] command to run"))]
Shell,
/// Pin an instance
Pin,
/// Unpin an instance
Unpin,
/// Start the proxy
Proxy,
/// Perform git operations on all apps
Git,
/// Deprecated, use `haze git checkout` instead
#[strum(props(Args = "[branch] branch to checkout"))]
Checkout,
/// Run command with notify_push environment variables
Env,
#[strum(props(Args = "[command] command to run with environment variables"))]
/// Update docker images
Update,
/// Show help text
#[strum(serialize = "--help", to_string = "help")]
Help,
/// Show version number
#[strum(serialize = "--version", to_string = "version")]
Version,
/// Edit a file in the instance with $EDITOR on the host
#[strum(props(Args = "[path] file to edit"))]
Edit,
/// Reload the php configuration in the instance
#[strum(props(
Details = "note: you can overwrite <yellow>php.ini</yellow> settings with <literal>haze</literal> <arg>[filter]</arg> <literal>edit /config/php.ini</literal>"
))]
Reload,
}
impl SubCommand for HazeCommand {
fn allows_filter(&self) -> bool {
matches!(
self,
HazeCommand::List
| HazeCommand::Stop
| HazeCommand::Exec
| HazeCommand::Occ
| HazeCommand::Db
| HazeCommand::Logs
| HazeCommand::Open
| HazeCommand::Pin
| HazeCommand::Unpin
| HazeCommand::Env
| HazeCommand::Edit
| HazeCommand::Reload
)
}
fn sub_commands(&self) -> Option<Box<dyn Iterator<Item = Box<dyn SubCommand>>>> {
match self {
HazeCommand::Git => Some(Box::new(GitOperation::iter().map(|op| Box::new(op) as _))),
_ => None,
}
}
}
#[test]
fn test_arg_parse() {
let config = HazeConfig {
sources_root: Default::default(),
app_directories: Default::default(),
work_dir: Default::default(),
auto_setup: Default::default(),
volume: vec![],
blackfire: None,
proxy: Default::default(),
preset: vec![],
};
fn assert_eq<A: Debug, B: Debug>(a: A, b: B) {
assert_eq!(format!("{a:?}"), format!("{b:?}"));
}
assert_eq(
HazeArgs::parse(&config, vec!["haze"].into_iter()).unwrap(),
HazeArgs::List { filter: None },
);
assert_eq(
HazeArgs::parse(&config, vec!["haze", "test"].into_iter()).unwrap(),
HazeArgs::Test {
options: Default::default(),
args: vec![],
},
);
assert_eq(
HazeArgs::parse(&config, vec!["haze", "asdasd"].into_iter()).unwrap(),
HazeArgs::List {
filter: Some("asdasd".to_string()),
},
);
assert_eq(
HazeArgs::parse(&config, vec!["haze", "asdasd", "db"].into_iter()).unwrap(),
HazeArgs::Db {
filter: Some("asdasd".to_string()),
root: false,
command: Vec::new(),
index: None,
},
);
assert_eq(
HazeArgs::parse(&config, vec!["haze", "asdasd", "db", "root"].into_iter()).unwrap(),
HazeArgs::Db {
filter: Some("asdasd".to_string()),
root: true,
command: Vec::new(),
index: None,
},
);
assert_eq(
HazeArgs::parse(
&config,
vec!["haze", "asdasd", "db", "select", "1"].into_iter(),
)
.unwrap(),
HazeArgs::Db {
filter: Some("asdasd".to_string()),
root: false,
command: vec!["select".to_string(), "1".to_string()],
index: None,
},
);
assert_eq(
HazeArgs::parse(
&config,
vec!["haze", "asdasd", "db", "root", "select 1"].into_iter(),
)
.unwrap(),
HazeArgs::Db {
filter: Some("asdasd".to_string()),
root: true,
command: vec!["select 1".to_string()],
index: None,
},
);
assert_eq(
HazeArgs::parse(
&config,
vec!["haze", "asdasd", "db", "root", "1", "select 1"].into_iter(),
)
.unwrap(),
HazeArgs::Db {
filter: Some("asdasd".to_string()),
root: true,
command: vec!["select 1".to_string()],
index: Some("1".into()),
},
);
assert_eq(
HazeArgs::parse(
&config,
vec!["haze", "asdasd", "db", "all", "select 1"].into_iter(),
)
.unwrap(),
HazeArgs::Db {
filter: Some("asdasd".to_string()),
root: false,
command: vec!["select 1".to_string()],
index: Some("all".into()),
},
);
assert_eq(
HazeArgs::parse(&config, vec!["haze", "exec", "foo", "bar"].into_iter()).unwrap(),
HazeArgs::Exec {
filter: None,
service: None,
command: vec!["foo".to_string(), "bar".to_string()],
root: false,
},
);
assert_eq(
HazeArgs::parse(
&config,
vec!["haze", "exec", "root", "foo", "bar"].into_iter(),
)
.unwrap(),
HazeArgs::Exec {
filter: None,
service: None,
command: vec!["foo".to_string(), "bar".to_string()],
root: true,
},
);
assert_eq(
HazeArgs::parse(
&config,
vec!["haze", "asdasd", "exec", "foo", "bar"].into_iter(),
)
.unwrap(),
HazeArgs::Exec {
filter: Some("asdasd".to_string()),
service: None,
command: vec!["foo".to_string(), "bar".to_string()],
root: false,
},
);
assert_eq(
HazeArgs::parse(
&config,
vec!["haze", "asdasd", "exec", "db", "foo", "bar"].into_iter(),
)
.unwrap(),
HazeArgs::Exec {
filter: Some("asdasd".to_string()),
service: Some(ExecService::Db),
command: vec!["foo".to_string(), "bar".to_string()],
root: false,
},
);
assert_eq(
HazeArgs::parse(&config, vec!["haze", "test", "foo", "bar"].into_iter()).unwrap(),
HazeArgs::Test {
options: Default::default(),
args: vec!["foo".into(), "bar".into()],
},
);
assert_eq(
HazeArgs::parse(&config, vec!["haze", "logs", "-f", "smb"].into_iter()).unwrap(),
HazeArgs::Logs {
filter: None,
follow: true,
service: Some(LogService::from_type(&[], "smb").unwrap()),
count: None,
},
);
assert_eq(
HazeArgs::parse(
&config,
vec!["haze", "asdasd", "logs", "smb", "123"].into_iter(),
)
.unwrap(),
HazeArgs::Logs {
filter: Some("asdasd".to_string()),
follow: false,
service: Some(LogService::from_type(&[], "smb").unwrap()),
count: Some(123),
},
);
}