1#![allow(clippy::module_name_repetitions)]
10
11use std::time::Duration;
12
13pub const DEFAULT_DOMAIN: u32 = 0;
15pub const DEFAULT_DURATION_SECS: u64 = 5;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum Command {
21 Probe(ProbeArgs),
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct ProbeArgs {
28 pub domain: u32,
30 pub duration: Duration,
32 pub format: ProbeFormat,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum ProbeFormat {
39 Text,
41 Json,
43}
44
45impl Default for ProbeArgs {
46 fn default() -> Self {
47 Self {
48 domain: DEFAULT_DOMAIN,
49 duration: Duration::from_secs(DEFAULT_DURATION_SECS),
50 format: ProbeFormat::Text,
51 }
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum ParseError {
58 Missing,
60 Unknown(String),
62 MissingArg(&'static str),
64 BadValue {
66 flag: &'static str,
68 got: String,
70 },
71}
72
73impl std::fmt::Display for ParseError {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 match self {
76 Self::Missing => write!(f, "no sub-command given"),
77 Self::Unknown(s) => write!(f, "unknown sub-command: {s}"),
78 Self::MissingArg(a) => write!(f, "missing required arg: {a}"),
79 Self::BadValue { flag, got } => write!(f, "bad value for --{flag}: {got}"),
80 }
81 }
82}
83
84impl std::error::Error for ParseError {}
85
86pub fn parse_args(args: &[String]) -> Result<Command, ParseError> {
92 let first = args.first().ok_or(ParseError::Missing)?;
93 let (sub, rest) = if first.starts_with('-') {
94 ("probe", args)
95 } else {
96 (first.as_str(), &args[1..])
97 };
98 match sub {
99 "probe" => parse_probe(rest).map(Command::Probe),
100 other => Err(ParseError::Unknown(other.to_string())),
101 }
102}
103
104fn parse_probe(rest: &[String]) -> Result<ProbeArgs, ParseError> {
105 let mut out = ProbeArgs::default();
106 let mut i = 0;
107 while i < rest.len() {
108 match rest[i].as_str() {
109 "--domain" | "-d" => {
110 i += 1;
111 let v = rest.get(i).ok_or(ParseError::MissingArg("domain"))?;
112 out.domain = v.parse().map_err(|_| ParseError::BadValue {
113 flag: "domain",
114 got: v.clone(),
115 })?;
116 }
117 "--duration" => {
118 i += 1;
119 let v = rest.get(i).ok_or(ParseError::MissingArg("duration"))?;
120 out.duration =
121 zerodds_cli_common::parse_duration(v).map_err(|_| ParseError::BadValue {
122 flag: "duration",
123 got: v.clone(),
124 })?;
125 }
126 "--format" | "-f" => {
127 i += 1;
128 let v = rest.get(i).ok_or(ParseError::MissingArg("format"))?;
129 out.format = match v.as_str() {
130 "text" => ProbeFormat::Text,
131 "json" => ProbeFormat::Json,
132 other => {
133 return Err(ParseError::BadValue {
134 flag: "format",
135 got: other.to_string(),
136 });
137 }
138 };
139 }
140 other => return Err(ParseError::Unknown(other.to_string())),
141 }
142 i += 1;
143 }
144 Ok(out)
145}
146
147#[cfg(test)]
148#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
149mod tests {
150 use super::*;
151
152 fn s(args: &[&str]) -> Vec<String> {
153 args.iter().map(|s| (*s).to_string()).collect()
154 }
155
156 #[test]
157 fn parse_probe_default() {
158 let cmd = parse_args(&s(&["probe"])).unwrap();
159 assert_eq!(cmd, Command::Probe(ProbeArgs::default()));
160 }
161
162 #[test]
163 fn parse_probe_implicit() {
164 let cmd = parse_args(&s(&["-d", "5"])).unwrap();
165 let Command::Probe(p) = cmd;
166 assert_eq!(p.domain, 5);
167 }
168
169 #[test]
170 fn parse_probe_full() {
171 let cmd = parse_args(&s(&[
172 "probe",
173 "-d",
174 "5",
175 "--duration",
176 "10s",
177 "--format",
178 "json",
179 ]))
180 .unwrap();
181 let Command::Probe(p) = cmd;
182 assert_eq!(p.domain, 5);
183 assert_eq!(p.duration, Duration::from_secs(10));
184 assert_eq!(p.format, ProbeFormat::Json);
185 }
186
187 #[test]
188 fn parse_no_args_rejected() {
189 assert!(matches!(parse_args(&[]), Err(ParseError::Missing)));
190 }
191
192 #[test]
193 fn parse_unknown_subcommand_rejected() {
194 let err = parse_args(&s(&["wat"])).unwrap_err();
195 assert!(matches!(err, ParseError::Unknown(_)));
196 }
197
198 #[test]
199 fn parse_bad_format_rejected() {
200 assert!(matches!(
201 parse_args(&s(&["probe", "--format", "xml"])),
202 Err(ParseError::BadValue { .. })
203 ));
204 }
205}