use std::ffi::OsString;
use std::path::PathBuf;
pub(crate) const USAGE: &str = "Usage: shimpz <check|test> [options]";
pub(crate) const HELP: &str = "\
Fast local tooling for Shimpz Assistants.
Usage:
shimpz check [--project <path>]
shimpz test <power> [--input <json> | --input-file <path>] [--project <path>]
shimpz --help
shimpz --version
";
#[derive(Debug, Eq, PartialEq)]
pub(crate) enum Action {
Help,
Version,
Run(Command),
}
#[derive(Debug, Eq, PartialEq)]
pub(crate) enum Command {
Check {
project: PathBuf,
},
Test {
project: PathBuf,
power: String,
input: Input,
},
}
#[derive(Debug, Eq, PartialEq)]
pub(crate) enum Input {
Inline(String),
File(PathBuf),
Stdin,
}
pub(crate) fn parse(arguments: impl IntoIterator<Item = OsString>) -> Result<Action, String> {
let values = arguments
.into_iter()
.map(unicode)
.collect::<Result<Vec<_>, _>>()?;
let Some((command, rest)) = values.split_first() else {
return Ok(Action::Help);
};
match command.as_str() {
"-h" | "--help" | "help" => Ok(Action::Help),
"-V" | "--version" | "version" => Ok(Action::Version),
"check" => parse_check(rest),
"test" => parse_test(rest),
_ => Err("unknown command".into()),
}
}
fn parse_check(arguments: &[String]) -> Result<Action, String> {
if arguments == ["--help"] || arguments == ["-h"] {
return Ok(Action::Help);
}
let project = project_option(arguments)?;
Ok(Action::Run(Command::Check { project }))
}
fn parse_test(arguments: &[String]) -> Result<Action, String> {
if arguments == ["--help"] || arguments == ["-h"] {
return Ok(Action::Help);
}
let Some(power) = arguments.first().filter(|value| !value.starts_with('-')) else {
return Err("test requires a Power id".into());
};
if !valid_power_id(power) {
return Err("Power id is invalid".into());
}
let mut project = PathBuf::from(".");
let mut project_seen = false;
let mut input = None;
let mut index = 1;
while index < arguments.len() {
let option = &arguments[index];
let value = arguments
.get(index + 1)
.ok_or_else(|| format!("{option} requires a value"))?;
match option.as_str() {
"--project" if !project_seen => {
project = PathBuf::from(value);
project_seen = true;
}
"--input" if input.is_none() => {
input = Some(if value == "-" {
Input::Stdin
} else {
Input::Inline(value.clone())
});
}
"--input-file" if input.is_none() => input = Some(Input::File(PathBuf::from(value))),
"--project" | "--input" | "--input-file" => {
return Err(format!("{option} was repeated"));
}
_ => return Err(format!("unknown option {option}")),
}
index += 2;
}
Ok(Action::Run(Command::Test {
project,
power: power.clone(),
input: input.unwrap_or_else(|| Input::Inline("{}".into())),
}))
}
fn project_option(arguments: &[String]) -> Result<PathBuf, String> {
match arguments {
[] => Ok(PathBuf::from(".")),
[option, value] if option == "--project" => Ok(PathBuf::from(value)),
[option] if option == "--project" => Err("--project requires a value".into()),
_ => Err("check accepts only --project <path>".into()),
}
}
fn unicode(value: OsString) -> Result<String, String> {
value
.into_string()
.map_err(|_| "arguments must be valid UTF-8".into())
}
fn valid_power_id(value: &str) -> bool {
!value.is_empty()
&& value.len() <= 64
&& value.starts_with(|character: char| character.is_ascii_lowercase())
&& value
.bytes()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
&& !value.ends_with('-')
}
#[cfg(test)]
mod tests {
use super::*;
fn strings(values: &[&str]) -> Vec<OsString> {
values.iter().map(OsString::from).collect()
}
#[test]
fn parses_check_with_the_current_project() {
assert_eq!(
parse(strings(&["check"])),
Ok(Action::Run(Command::Check {
project: PathBuf::from(".")
}))
);
}
#[test]
fn parses_a_file_backed_power_test() {
assert_eq!(
parse(strings(&[
"test",
"create-dns",
"--input-file",
"request.json",
"--project",
"assistant"
])),
Ok(Action::Run(Command::Test {
project: PathBuf::from("assistant"),
power: "create-dns".into(),
input: Input::File(PathBuf::from("request.json")),
}))
);
}
#[test]
fn rejects_secrets_and_unknown_options() {
assert_eq!(
parse(strings(&["test", "create-dns", "--account", "token"])),
Err("unknown option --account".into())
);
}
#[test]
fn rejects_invalid_power_ids() {
assert_eq!(
parse(strings(&["test", "CreateDns"])),
Err("Power id is invalid".into())
);
}
}