zerodds_websocket_bridge/daemon/
cli.rs1use std::string::String;
17use std::vec::Vec;
18
19#[derive(Debug, Clone, Default)]
21pub struct CliArgs {
22 pub config: Option<String>,
24 pub listen: Option<String>,
26 pub domain: Option<i32>,
28 pub topics: Vec<String>,
30 pub tls_cert: Option<String>,
32 pub tls_key: Option<String>,
34 pub auth_token: Option<String>,
36 pub log_level: Option<String>,
38 pub metrics: Option<String>,
40 pub version: bool,
42 pub help: bool,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
48pub enum CliError {
49 UnknownFlag(String),
51 MissingValue(String),
53 InvalidValue {
55 flag: String,
57 value: String,
59 },
60}
61
62impl core::fmt::Display for CliError {
63 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
64 match self {
65 Self::UnknownFlag(name) => write!(f, "unknown flag: {name}"),
66 Self::MissingValue(name) => write!(f, "flag {name} requires a value"),
67 Self::InvalidValue { flag, value } => {
68 write!(f, "flag {flag} got invalid value: {value}")
69 }
70 }
71 }
72}
73
74impl std::error::Error for CliError {}
75
76pub const HELP_TEXT: &str = "\
78zerodds-ws-bridged 1.0 — DDS↔WebSocket-Bridge-Daemon
79
80USAGE:
81 zerodds-ws-bridged [OPTIONS]
82
83OPTIONS:
84 --config <FILE> Path zur Config-File (YAML)
85 --listen <ADDR> Bind-Address (Default 0.0.0.0:8080)
86 --domain <ID> DDS-Domain-ID (Default 0)
87 --topic <NAME> Single-Topic-Override (mehrfach)
88 --tls-cert <FILE> TLS-Cert (PEM); aktiviert wss:// — L5-stub
89 --tls-key <FILE> TLS-Key (PEM) — L5-stub
90 --auth-token <SECRET> Bearer-Token-Auth — L5-stub
91 --log-level <LEVEL> trace/debug/info/warn/error (Default info)
92 --metrics <ADDR> Prometheus-Scrape-Endpoint — L5-stub
93 --version Versions-Info
94 --help Hilfe
95
96EXIT-CODES:
97 0 normaler Shutdown (SIGTERM/SIGINT)
98 1 Config-Fehler
99 2 Bind-Fehler (Port belegt)
100 3 DDS-Discovery-Fehler
101 4 TLS-Fehler
102
103Spec: docs/specs/zerodds-ws-bridge-1.0.md
104";
105
106pub const VERSION_TEXT: &str = "zerodds-ws-bridged 1.0";
108
109pub fn parse(args: &[String]) -> Result<CliArgs, CliError> {
114 let mut out = CliArgs::default();
115 let mut i = 0;
116 while i < args.len() {
117 let raw = &args[i];
118 let (flag, inline) = match raw.split_once('=') {
120 Some((k, v)) => (k.to_string(), Some(v.to_string())),
121 None => (raw.clone(), None),
122 };
123 let take_value = |i: &mut usize, flag: &str| -> Result<String, CliError> {
124 if let Some(v) = inline.clone() {
125 Ok(v)
126 } else {
127 *i += 1;
128 args.get(*i)
129 .cloned()
130 .ok_or_else(|| CliError::MissingValue(flag.to_string()))
131 }
132 };
133 match flag.as_str() {
134 "--help" | "-h" => out.help = true,
135 "--version" | "-V" => out.version = true,
136 "--config" => out.config = Some(take_value(&mut i, "--config")?),
137 "--listen" => out.listen = Some(take_value(&mut i, "--listen")?),
138 "--domain" => {
139 let v = take_value(&mut i, "--domain")?;
140 out.domain = Some(v.parse().map_err(|_| CliError::InvalidValue {
141 flag: "--domain".to_string(),
142 value: v,
143 })?);
144 }
145 "--topic" => {
146 out.topics.push(take_value(&mut i, "--topic")?);
147 }
148 "--tls-cert" => out.tls_cert = Some(take_value(&mut i, "--tls-cert")?),
149 "--tls-key" => out.tls_key = Some(take_value(&mut i, "--tls-key")?),
150 "--auth-token" => out.auth_token = Some(take_value(&mut i, "--auth-token")?),
151 "--log-level" => out.log_level = Some(take_value(&mut i, "--log-level")?),
152 "--metrics" => out.metrics = Some(take_value(&mut i, "--metrics")?),
153 other => return Err(CliError::UnknownFlag(other.to_string())),
154 }
155 i += 1;
156 }
157 Ok(out)
158}
159
160#[cfg(test)]
161#[allow(clippy::expect_used, clippy::unwrap_used)]
162mod tests {
163 use super::*;
164
165 fn args(parts: &[&str]) -> Vec<String> {
166 parts.iter().map(|s| (*s).to_string()).collect()
167 }
168
169 #[test]
170 fn empty_args_yield_default() {
171 let p = parse(&[]).unwrap();
172 assert!(p.config.is_none());
173 assert!(!p.help);
174 }
175
176 #[test]
177 fn help_and_version_no_arg() {
178 assert!(parse(&args(&["--help"])).unwrap().help);
179 assert!(parse(&args(&["--version"])).unwrap().version);
180 }
181
182 #[test]
183 fn space_separated_value() {
184 let p = parse(&args(&["--config", "/tmp/c.yaml"])).unwrap();
185 assert_eq!(p.config.as_deref(), Some("/tmp/c.yaml"));
186 }
187
188 #[test]
189 fn equals_form_value() {
190 let p = parse(&args(&["--listen=0.0.0.0:9000"])).unwrap();
191 assert_eq!(p.listen.as_deref(), Some("0.0.0.0:9000"));
192 }
193
194 #[test]
195 fn domain_parses_int() {
196 assert_eq!(parse(&args(&["--domain", "7"])).unwrap().domain, Some(7));
197 }
198
199 #[test]
200 fn domain_rejects_non_int() {
201 let err = parse(&args(&["--domain", "abc"])).unwrap_err();
202 assert!(matches!(err, CliError::InvalidValue { .. }));
203 }
204
205 #[test]
206 fn topic_repeats_collected() {
207 let p = parse(&args(&["--topic", "A", "--topic", "B"])).unwrap();
208 assert_eq!(p.topics, vec!["A".to_string(), "B".to_string()]);
209 }
210
211 #[test]
212 fn unknown_flag_rejected() {
213 let err = parse(&args(&["--nope"])).unwrap_err();
214 assert!(matches!(err, CliError::UnknownFlag(_)));
215 }
216
217 #[test]
218 fn missing_value_rejected() {
219 let err = parse(&args(&["--config"])).unwrap_err();
220 assert!(matches!(err, CliError::MissingValue(_)));
221 }
222}