vpp_sim/
cli.rs

1use std::env;
2use std::path::PathBuf;
3
4pub struct CliOptions {
5    pub scenario: Option<PathBuf>,
6    pub preset: Option<String>,
7    pub telemetry_out: Option<PathBuf>,
8    pub api_bind: Option<String>,
9}
10
11pub fn parse_args() -> Result<CliOptions, String> {
12    let args: Vec<String> = env::args().skip(1).collect();
13    parse_args_from(args)
14}
15
16fn parse_args_from(args: Vec<String>) -> Result<CliOptions, String> {
17    if args.len() == 1 && (args[0] == "--help" || args[0] == "-h") {
18        print_usage();
19        std::process::exit(0);
20    }
21    parse_options(&args)
22}
23
24fn parse_options(args: &[String]) -> Result<CliOptions, String> {
25    let mut i = 0usize;
26    let mut scenario = None;
27    let mut preset = None;
28    let mut telemetry_out = None;
29    let mut api_bind = None;
30
31    while i < args.len() {
32        match args[i].as_str() {
33            "--scenario" => {
34                i += 1;
35                let path = args.get(i).ok_or_else(|| {
36                    "missing value for --scenario (expected a .toml scenario file path)".to_string()
37                })?;
38                if scenario.replace(PathBuf::from(path)).is_some() {
39                    return Err("--scenario provided more than once".to_string());
40                }
41            }
42            "--preset" => {
43                i += 1;
44                let name = args.get(i).ok_or_else(|| {
45                    "missing value for --preset (expected a preset name)".to_string()
46                })?;
47                if preset.replace(name.clone()).is_some() {
48                    return Err("--preset provided more than once".to_string());
49                }
50            }
51            "--telemetry-out" => {
52                i += 1;
53                let path = args.next_or_err(
54                    i,
55                    "missing value for --telemetry-out (expected a file path)",
56                )?;
57                if telemetry_out.replace(PathBuf::from(path)).is_some() {
58                    return Err("--telemetry-out provided more than once".to_string());
59                }
60            }
61            "--api-bind" => {
62                i += 1;
63                let addr =
64                    args.next_or_err(i, "missing value for --api-bind (expected host:port)")?;
65                if api_bind.replace(addr.to_string()).is_some() {
66                    return Err("--api-bind provided more than once".to_string());
67                }
68            }
69            "--help" | "-h" => {
70                print_usage();
71                std::process::exit(0);
72            }
73            other => return Err(format!("unknown argument: {other}")),
74        }
75        i += 1;
76    }
77
78    if scenario.is_some() && preset.is_some() {
79        return Err(
80            "arguments `--scenario` and `--preset` are mutually exclusive; choose one source"
81                .to_string(),
82        );
83    }
84
85    if scenario.is_none() && preset.is_none() {
86        preset = Some("demo".to_string());
87    }
88
89    Ok(CliOptions {
90        scenario,
91        preset,
92        telemetry_out,
93        api_bind,
94    })
95}
96
97trait SliceArgExt {
98    fn next_or_err(&self, index: usize, err: &str) -> Result<&str, String>;
99}
100
101impl SliceArgExt for [String] {
102    fn next_or_err(&self, index: usize, err: &str) -> Result<&str, String> {
103        self.get(index)
104            .map(String::as_str)
105            .ok_or_else(|| err.to_string())
106    }
107}
108
109pub fn print_usage() {
110    eprintln!("Usage:");
111    eprintln!(
112        "  cargo run --release -- [--scenario <path> | --preset <name>] [--telemetry-out <path>] [--api-bind <host:port>]"
113    );
114}
115
116#[cfg(test)]
117mod tests {
118    use super::parse_args_from;
119
120    #[test]
121    fn supports_scenario_cli() {
122        let opts = parse_args_from(vec!["--scenario".to_string(), "scenario.json".to_string()])
123            .expect("parse should succeed");
124        assert_eq!(
125            opts.scenario.as_deref().and_then(|p| p.to_str()),
126            Some("scenario.json")
127        );
128        assert!(opts.preset.is_none());
129    }
130
131    #[test]
132    fn supports_preset_cli() {
133        let opts = parse_args_from(vec!["--preset".to_string(), "demo".to_string()])
134            .expect("parse should succeed");
135        assert_eq!(opts.preset.as_deref(), Some("demo"));
136        assert!(opts.scenario.is_none());
137    }
138
139    #[test]
140    fn supports_api_bind_cli() {
141        let opts = parse_args_from(vec![
142            "--preset".to_string(),
143            "demo".to_string(),
144            "--api-bind".to_string(),
145            "127.0.0.1:8080".to_string(),
146        ])
147        .expect("parse should succeed");
148        assert_eq!(opts.api_bind.as_deref(), Some("127.0.0.1:8080"));
149    }
150}