use nonempty::NonEmpty;
use nutype::nutype;
use crate::name::NameError;
use crate::name::validation::validate_short;
#[nutype(
validate(with = validate_short, error = NameError),
derive(
Debug,
Clone,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Display,
FromStr,
AsRef,
),
)]
pub struct NonEmptyAsciiName(String);
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
pub enum ShellType {
#[default]
Sh,
Bash,
Other(NonEmptyAsciiName),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum TaskAction {
Command(NonEmpty<String>),
Shell {
script: String,
shell: ShellType,
},
}
#[cfg(test)]
mod tests {
use nonempty::NonEmpty;
use crate::action::{NonEmptyAsciiName, ShellType, TaskAction};
use crate::name::NameError;
#[test]
fn shell_type_default_is_sh() {
assert_eq!(ShellType::default(), ShellType::Sh);
}
#[test]
fn shell_type_other_carries_validated_name() {
let zsh = NonEmptyAsciiName::try_new("zsh").unwrap();
let st = ShellType::Other(zsh);
assert!(matches!(st, ShellType::Other(_)));
}
#[test]
fn shell_types_are_distinct() {
assert_ne!(ShellType::Sh, ShellType::Bash);
let pwsh = NonEmptyAsciiName::try_new("pwsh").unwrap();
assert_ne!(ShellType::Sh, ShellType::Other(pwsh));
}
#[test]
fn ascii_name_accepts_typical_shell_names() {
for s in ["sh", "bash", "zsh", "fish", "pwsh", "dash", "ksh"] {
assert!(
NonEmptyAsciiName::try_new(s).is_ok(),
"rejected typical shell name {s:?}"
);
}
}
#[test]
fn ascii_name_rejects_empty() {
assert!(matches!(
NonEmptyAsciiName::try_new(""),
Err(NameError::Empty)
));
}
#[test]
fn ascii_name_rejects_path_separator() {
assert!(matches!(
NonEmptyAsciiName::try_new("/usr/bin/zsh"),
Err(NameError::InvalidLeadingChar { c: '/' })
));
}
#[test]
fn ascii_name_rejects_shell_metacharacter() {
assert!(matches!(
NonEmptyAsciiName::try_new("z|h"),
Err(NameError::InvalidChar { c: '|' })
));
}
#[test]
fn ascii_name_rejects_space() {
assert!(matches!(
NonEmptyAsciiName::try_new("zsh extra"),
Err(NameError::InvalidChar { c: ' ' })
));
}
#[test]
fn ascii_name_accepts_at_32_byte_cap() {
let exactly_32 = "a".repeat(32);
assert!(NonEmptyAsciiName::try_new(&exactly_32).is_ok());
}
#[test]
fn ascii_name_rejects_above_32_bytes() {
let too_long = "a".repeat(33);
assert!(matches!(
NonEmptyAsciiName::try_new(&too_long),
Err(NameError::TooLong { length: 33 })
));
}
#[test]
fn ascii_name_diverges_from_identifier_at_33_bytes() {
let s = "a".repeat(33);
assert!(crate::name::ProjectName::try_new(&s).is_ok());
assert!(NonEmptyAsciiName::try_new(&s).is_err());
}
#[test]
fn command_accepts_non_empty_argv() {
let argv = NonEmpty::from_vec(vec!["echo".to_owned(), "hi".to_owned()]).unwrap();
let action = TaskAction::Command(argv);
match action {
TaskAction::Command(argv) => {
assert_eq!(argv.len(), 2);
assert_eq!(argv.first(), "echo");
}
TaskAction::Shell { .. } => unreachable!(),
}
}
#[test]
fn command_empty_argv_unrepresentable() {
assert!(NonEmpty::from_vec(Vec::<String>::new()).is_none());
}
#[test]
fn shell_action_with_default_shell() {
let action = TaskAction::Shell {
script: "echo hi".to_owned(),
shell: ShellType::default(),
};
match action {
TaskAction::Shell { script, shell } => {
assert_eq!(script, "echo hi");
assert_eq!(shell, ShellType::Sh);
}
TaskAction::Command(_) => unreachable!(),
}
}
#[test]
fn shell_action_with_named_other_shell() {
let pwsh = NonEmptyAsciiName::try_new("pwsh").unwrap();
let action = TaskAction::Shell {
script: "Write-Host hi".to_owned(),
shell: ShellType::Other(pwsh),
};
assert!(matches!(action, TaskAction::Shell { .. }));
}
}