#![allow(clippy::module_name_repetitions)]
use std::time::Duration;
pub const DEFAULT_DOMAIN: u32 = 0;
pub const DEFAULT_DURATION_SECS: u64 = 5;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Command {
Probe(ProbeArgs),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProbeArgs {
pub domain: u32,
pub duration: Duration,
pub format: ProbeFormat,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProbeFormat {
Text,
Json,
}
impl Default for ProbeArgs {
fn default() -> Self {
Self {
domain: DEFAULT_DOMAIN,
duration: Duration::from_secs(DEFAULT_DURATION_SECS),
format: ProbeFormat::Text,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseError {
Missing,
Unknown(String),
MissingArg(&'static str),
BadValue {
flag: &'static str,
got: String,
},
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Missing => write!(f, "no sub-command given"),
Self::Unknown(s) => write!(f, "unknown sub-command: {s}"),
Self::MissingArg(a) => write!(f, "missing required arg: {a}"),
Self::BadValue { flag, got } => write!(f, "bad value for --{flag}: {got}"),
}
}
}
impl std::error::Error for ParseError {}
pub fn parse_args(args: &[String]) -> Result<Command, ParseError> {
let first = args.first().ok_or(ParseError::Missing)?;
let (sub, rest) = if first.starts_with('-') {
("probe", args)
} else {
(first.as_str(), &args[1..])
};
match sub {
"probe" => parse_probe(rest).map(Command::Probe),
other => Err(ParseError::Unknown(other.to_string())),
}
}
fn parse_probe(rest: &[String]) -> Result<ProbeArgs, ParseError> {
let mut out = ProbeArgs::default();
let mut i = 0;
while i < rest.len() {
match rest[i].as_str() {
"--domain" | "-d" => {
i += 1;
let v = rest.get(i).ok_or(ParseError::MissingArg("domain"))?;
out.domain = v.parse().map_err(|_| ParseError::BadValue {
flag: "domain",
got: v.clone(),
})?;
}
"--duration" => {
i += 1;
let v = rest.get(i).ok_or(ParseError::MissingArg("duration"))?;
out.duration =
zerodds_cli_common::parse_duration(v).map_err(|_| ParseError::BadValue {
flag: "duration",
got: v.clone(),
})?;
}
"--format" | "-f" => {
i += 1;
let v = rest.get(i).ok_or(ParseError::MissingArg("format"))?;
out.format = match v.as_str() {
"text" => ProbeFormat::Text,
"json" => ProbeFormat::Json,
other => {
return Err(ParseError::BadValue {
flag: "format",
got: other.to_string(),
});
}
};
}
other => return Err(ParseError::Unknown(other.to_string())),
}
i += 1;
}
Ok(out)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
fn s(args: &[&str]) -> Vec<String> {
args.iter().map(|s| (*s).to_string()).collect()
}
#[test]
fn parse_probe_default() {
let cmd = parse_args(&s(&["probe"])).unwrap();
assert_eq!(cmd, Command::Probe(ProbeArgs::default()));
}
#[test]
fn parse_probe_implicit() {
let cmd = parse_args(&s(&["-d", "5"])).unwrap();
let Command::Probe(p) = cmd;
assert_eq!(p.domain, 5);
}
#[test]
fn parse_probe_full() {
let cmd = parse_args(&s(&[
"probe",
"-d",
"5",
"--duration",
"10s",
"--format",
"json",
]))
.unwrap();
let Command::Probe(p) = cmd;
assert_eq!(p.domain, 5);
assert_eq!(p.duration, Duration::from_secs(10));
assert_eq!(p.format, ProbeFormat::Json);
}
#[test]
fn parse_no_args_rejected() {
assert!(matches!(parse_args(&[]), Err(ParseError::Missing)));
}
#[test]
fn parse_unknown_subcommand_rejected() {
let err = parse_args(&s(&["wat"])).unwrap_err();
assert!(matches!(err, ParseError::Unknown(_)));
}
#[test]
fn parse_bad_format_rejected() {
assert!(matches!(
parse_args(&s(&["probe", "--format", "xml"])),
Err(ParseError::BadValue { .. })
));
}
}