use std::{ffi::OsString, path::PathBuf};
use crate::error::{AppError, Result};
pub(crate) const USAGE: &str = "Usage: git-file-history <file_path>";
const MISSING_FILE_PATH: &str = "missing file path";
const TOO_MANY_ARGUMENTS: &str = "too many arguments";
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum CliAction {
Run(PathBuf),
Help,
Version,
}
#[must_use = "the parsed CLI action or error must be handled"]
pub(crate) fn parse_args<I>(mut args: I) -> Result<CliAction>
where
I: Iterator<Item = OsString>,
{
let _program = args.next();
let Some(file_path) = args.next() else {
return Err(AppError::message(format!("{MISSING_FILE_PATH}\n{USAGE}")));
};
let has_extra_arg = args.next().is_some();
if file_path == "--help" || file_path == "-h" {
return if has_extra_arg {
Err(AppError::message(format!("{TOO_MANY_ARGUMENTS}\n{USAGE}")))
} else {
Ok(CliAction::Help)
};
}
if file_path == "--version" || file_path == "-V" {
return if has_extra_arg {
Err(AppError::message(format!("{TOO_MANY_ARGUMENTS}\n{USAGE}")))
} else {
Ok(CliAction::Version)
};
}
if has_extra_arg {
return Err(AppError::message(format!("{TOO_MANY_ARGUMENTS}\n{USAGE}")));
}
Ok(CliAction::Run(PathBuf::from(file_path)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_exactly_one_file_path() {
let args = ["git-file-history", "src/main.rs"].map(OsString::from);
let action = parse_args(args.into_iter()).expect("args should parse");
assert_eq!(action, CliAction::Run(PathBuf::from("src/main.rs")));
}
#[test]
fn rejects_missing_file_path() {
let args = ["git-file-history"].map(OsString::from);
let error = parse_args(args.into_iter()).expect_err("args should fail");
assert_eq!(error.to_string(), format!("{MISSING_FILE_PATH}\n{USAGE}"));
}
#[test]
fn rejects_extra_args() {
let args = ["git-file-history", "a", "b"].map(OsString::from);
let error = parse_args(args.into_iter()).expect_err("args should fail");
assert_eq!(error.to_string(), format!("{TOO_MANY_ARGUMENTS}\n{USAGE}"));
}
#[test]
fn accepts_help_flags() {
for flag in ["--help", "-h"] {
let args = ["git-file-history", flag].map(OsString::from);
assert!(matches!(parse_args(args.into_iter()), Ok(CliAction::Help)));
}
}
#[test]
fn rejects_help_flags_with_extra_args() {
let args = ["git-file-history", "--help", "extra"].map(OsString::from);
let error = parse_args(args.into_iter()).expect_err("args should fail");
assert_eq!(error.to_string(), format!("{TOO_MANY_ARGUMENTS}\n{USAGE}"));
}
#[test]
fn accepts_version_flags() {
for flag in ["--version", "-V"] {
let args = ["git-file-history", flag].map(OsString::from);
assert!(matches!(
parse_args(args.into_iter()),
Ok(CliAction::Version)
));
}
}
#[test]
fn rejects_version_flags_with_extra_args() {
let args = ["git-file-history", "--version", "extra"].map(OsString::from);
let error = parse_args(args.into_iter()).expect_err("args should fail");
assert_eq!(error.to_string(), format!("{TOO_MANY_ARGUMENTS}\n{USAGE}"));
}
}