1#![allow(clippy::module_name_repetitions)]
10
11use std::time::Duration;
12
13pub const DEFAULT_SRC_DOMAIN: u32 = 0;
15pub const DEFAULT_DST_DOMAIN: u32 = 1;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum Command {
21 Bridge(BridgeArgs),
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct BridgeArgs {
28 pub src_domain: u32,
30 pub dst_domain: u32,
32 pub src_topic: String,
34 pub dst_topic: String,
36 pub duration: Option<Duration>,
38 pub bidirectional: bool,
40}
41
42impl Default for BridgeArgs {
43 fn default() -> Self {
44 Self {
45 src_domain: DEFAULT_SRC_DOMAIN,
46 dst_domain: DEFAULT_DST_DOMAIN,
47 src_topic: String::new(),
48 dst_topic: String::new(),
49 duration: None,
50 bidirectional: false,
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 ("bridge", args)
95 } else {
96 (first.as_str(), &args[1..])
97 };
98 match sub {
99 "bridge" => parse_bridge(rest).map(Command::Bridge),
100 other => Err(ParseError::Unknown(other.to_string())),
101 }
102}
103
104fn parse_bridge(rest: &[String]) -> Result<BridgeArgs, ParseError> {
105 let mut out = BridgeArgs::default();
106 let mut i = 0;
107 while i < rest.len() {
108 match rest[i].as_str() {
109 "--src-domain" => {
110 i += 1;
111 let v = rest.get(i).ok_or(ParseError::MissingArg("src-domain"))?;
112 out.src_domain = v.parse().map_err(|_| ParseError::BadValue {
113 flag: "src-domain",
114 got: v.clone(),
115 })?;
116 }
117 "--dst-domain" => {
118 i += 1;
119 let v = rest.get(i).ok_or(ParseError::MissingArg("dst-domain"))?;
120 out.dst_domain = v.parse().map_err(|_| ParseError::BadValue {
121 flag: "dst-domain",
122 got: v.clone(),
123 })?;
124 }
125 "--topic" | "-t" => {
126 i += 1;
127 let topic = rest.get(i).ok_or(ParseError::MissingArg("topic"))?.clone();
128 out.src_topic = topic.clone();
129 if out.dst_topic.is_empty() {
130 out.dst_topic = topic;
131 }
132 }
133 "--src-topic" => {
134 i += 1;
135 out.src_topic = rest
136 .get(i)
137 .ok_or(ParseError::MissingArg("src-topic"))?
138 .clone();
139 }
140 "--dst-topic" => {
141 i += 1;
142 out.dst_topic = rest
143 .get(i)
144 .ok_or(ParseError::MissingArg("dst-topic"))?
145 .clone();
146 }
147 "--duration" => {
148 i += 1;
149 let v = rest.get(i).ok_or(ParseError::MissingArg("duration"))?;
150 out.duration = Some(zerodds_cli_common::parse_duration(v).map_err(|_| {
151 ParseError::BadValue {
152 flag: "duration",
153 got: v.clone(),
154 }
155 })?);
156 }
157 "--bidirectional" => {
158 out.bidirectional = true;
159 }
160 other => return Err(ParseError::Unknown(other.to_string())),
161 }
162 i += 1;
163 }
164 Ok(out)
165}
166
167#[cfg(test)]
168#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
169mod tests {
170 use super::*;
171
172 fn s(args: &[&str]) -> Vec<String> {
173 args.iter().map(|s| (*s).to_string()).collect()
174 }
175
176 #[test]
177 fn parse_bridge_minimal() {
178 let cmd = parse_args(&s(&["bridge", "-t", "Foo"])).unwrap();
179 let Command::Bridge(b) = cmd;
180 assert_eq!(b.src_topic, "Foo");
181 assert_eq!(b.dst_topic, "Foo");
182 assert_eq!(b.src_domain, 0);
183 assert_eq!(b.dst_domain, 1);
184 }
185
186 #[test]
187 fn parse_bridge_implicit() {
188 let cmd = parse_args(&s(&["-t", "Bar"])).unwrap();
189 let Command::Bridge(b) = cmd;
190 assert_eq!(b.src_topic, "Bar");
191 }
192
193 #[test]
194 fn parse_bridge_full() {
195 let cmd = parse_args(&s(&[
196 "bridge",
197 "--src-domain",
198 "0",
199 "--dst-domain",
200 "5",
201 "--src-topic",
202 "X",
203 "--dst-topic",
204 "Y",
205 "--duration",
206 "10s",
207 "--bidirectional",
208 ]))
209 .unwrap();
210 let Command::Bridge(b) = cmd;
211 assert_eq!(b.src_domain, 0);
212 assert_eq!(b.dst_domain, 5);
213 assert_eq!(b.src_topic, "X");
214 assert_eq!(b.dst_topic, "Y");
215 assert_eq!(b.duration, Some(Duration::from_secs(10)));
216 assert!(b.bidirectional);
217 }
218
219 #[test]
220 fn parse_bridge_topic_propagates_to_dst() {
221 let cmd = parse_args(&s(&["bridge", "-t", "T1"])).unwrap();
222 let Command::Bridge(b) = cmd;
223 assert_eq!(b.src_topic, b.dst_topic);
224 }
225
226 #[test]
227 fn parse_no_args_rejected() {
228 assert!(matches!(parse_args(&[]), Err(ParseError::Missing)));
229 }
230
231 #[test]
232 fn parse_unknown_subcommand_rejected() {
233 let err = parse_args(&s(&["wat"])).unwrap_err();
234 assert!(matches!(err, ParseError::Unknown(_)));
235 }
236
237 #[test]
238 fn parse_bad_domain_rejected() {
239 assert!(matches!(
240 parse_args(&s(&["bridge", "--src-domain", "abc"])),
241 Err(ParseError::BadValue { .. })
242 ));
243 }
244}