use std::mem::discriminant;
use std::num::NonZeroUsize;
use anyhow::{Context, Result, bail};
use crate::issue::IssueUrl;
use crate::notification::NotificationAsk;
use crate::run::Goal;
pub enum Command {
Help,
Version,
Update,
Setup,
EmailTest(Option<String>),
Run(RunArgs),
}
pub const SPEC_BRANCH: &str = "--spec-branch";
pub struct RunArgs {
pub issue: IssueUrl,
pub goal: Option<Goal>,
pub email: Option<NotificationAsk>,
pub parallel: Option<NonZeroUsize>,
pub spec_branch: Option<String>,
}
pub fn parse(args: &[String]) -> Result<Command> {
match args.first().map(String::as_str) {
Some("help" | "--help" | "-h") => return Ok(Command::Help),
Some("version" | "--version" | "-V") => return Ok(Command::Version),
Some("update") => return Ok(Command::Update),
Some("setup") => {
return match &args[1..] {
[] => Ok(Command::Setup),
[extra, ..] => bail!("unexpected argument after setup: {extra}"),
};
}
Some("email-test") => {
return match &args[1..] {
[] => Ok(Command::EmailTest(None)),
[address] => Ok(Command::EmailTest(Some(address.clone()))),
[_, extra, ..] => bail!("unexpected argument after the address: {extra}"),
};
}
_ => {}
}
let mut issue = None;
let mut goal = None;
let mut email = None;
let mut parallel = None;
let mut spec_branch = None;
let mut args = args.iter().peekable();
while let Some(arg) = args.next() {
match arg.as_str() {
"merge" | "--merge" => ask_once(&mut goal, Goal::Merged, arg, MERGE_FLAGS)?,
"no-merge" | "--no-merge" => {
ask_once(&mut goal, Goal::ReadyForReview, arg, MERGE_FLAGS)?
}
"email" | "--email" => {
let to = args.next_if(|next| is_address(next)).cloned();
ask_once(&mut email, NotificationAsk::Send(to), arg, EMAIL_FLAGS)?
}
"no-email" | "--no-email" => {
ask_once(&mut email, NotificationAsk::Skip, arg, EMAIL_FLAGS)?
}
"parallel" | "--parallel" => {
if parallel.is_some() {
bail!("repeated argument: {arg}");
}
let n = args.next();
let Some(n) = n.and_then(|n| n.parse::<NonZeroUsize>().ok()) else {
let given = n.map(|n| format!(", not {n}")).unwrap_or_default();
bail!("{arg} must be followed by a whole number from 1 up{given}");
};
parallel = Some(n);
}
SPEC_BRANCH => {
if spec_branch.is_some() {
bail!("repeated argument: {arg}");
}
spec_branch = Some(args.next().context("missing Spec branch")?.clone());
}
_ => {
if issue.is_some() {
bail!("unexpected argument after the Issue URL: {arg}");
}
issue = Some(IssueUrl::parse(arg)?);
}
}
}
let Some(issue) = issue else {
bail!("missing Issue URL");
};
Ok(Command::Run(RunArgs {
issue,
goal,
email,
parallel,
spec_branch,
}))
}
const MERGE_FLAGS: &str = "merge and no-merge";
const EMAIL_FLAGS: &str = "email and no-email";
fn ask_once<T>(given: &mut Option<T>, asked: T, arg: &str, flags: &str) -> Result<()> {
match given {
None => *given = Some(asked),
Some(given) if discriminant(given) == discriminant(&asked) => {
bail!("repeated argument: {arg}")
}
Some(_) => bail!("{flags} can't be used together"),
}
Ok(())
}
fn is_address(arg: &str) -> bool {
arg.contains('@') && !arg.starts_with("https://")
}