Skip to main content

blvm_sdk/module/
cli_args.rs

1//! CLI argument parsing and type coercion for module commands.
2//!
3//! Parses `["--key", "value", "-n", "2"]` into a map and coerces values to handler types.
4
5use blvm_node::module::ipc::protocol::CliArgSpec;
6use blvm_node::module::traits::ModuleError;
7use std::collections::HashMap;
8
9/// Parse CLI args into a map keyed by param name.
10///
11/// Supports:
12/// - `--long_name value` and `-short value`
13/// - `--flag` (bool, no value) → param = "true"
14/// - Positional fallback: if no `-` prefix, treat as positional by arg order
15pub fn parse_args(
16    args: &[String],
17    arg_specs: &[CliArgSpec],
18) -> Result<HashMap<String, String>, ModuleError> {
19    let mut map = HashMap::new();
20
21    // Check if we have any -- or - prefixed args (named style)
22    let has_named = args.iter().any(|a| {
23        a.starts_with("--")
24            || (a.starts_with('-')
25                && a.len() > 1
26                && !a
27                    .chars()
28                    .nth(1)
29                    .map(|c| c.is_ascii_digit())
30                    .unwrap_or(false))
31    });
32
33    if has_named {
34        // Named parsing: --key value, -k value
35        let mut i = 0;
36        while i < args.len() {
37            let arg = &args[i];
38            if arg.starts_with("--") {
39                let key = arg.trim_start_matches('-');
40                if key.is_empty() {
41                    i += 1;
42                    continue;
43                }
44                let param_name = find_param_by_long(arg_specs, key);
45                i += 1;
46                if i < args.len() && !args[i].starts_with('-') {
47                    let value = args[i].clone();
48                    i += 1;
49                    map.insert(param_name.to_string(), value);
50                } else {
51                    // Flag without value (bool)
52                    map.insert(param_name.to_string(), "true".to_string());
53                }
54            } else if arg.starts_with('-') && arg.len() > 1 {
55                let short = &arg[1..];
56                let param_name = find_param_by_short(arg_specs, short);
57                i += 1;
58                if i < args.len() && !args[i].starts_with('-') {
59                    let value = args[i].clone();
60                    i += 1;
61                    map.insert(param_name.to_string(), value);
62                } else {
63                    map.insert(param_name.to_string(), "true".to_string());
64                }
65            } else {
66                i += 1;
67            }
68        }
69    } else {
70        // Positional: map by arg order
71        for (i, spec) in arg_specs.iter().enumerate() {
72            if let Some(v) = args.get(i) {
73                map.insert(spec.name.clone(), v.clone());
74            }
75        }
76    }
77
78    Ok(map)
79}
80
81fn find_param_by_long<'a>(specs: &'a [CliArgSpec], long: &'a str) -> &'a str {
82    for spec in specs {
83        let long_form = spec.long_name.as_deref().unwrap_or(&spec.name);
84        if long_form == long {
85            return &spec.name;
86        }
87    }
88    long
89}
90
91fn find_param_by_short<'a>(specs: &'a [CliArgSpec], short: &'a str) -> &'a str {
92    for spec in specs {
93        if let Some(s) = &spec.short_name {
94            if s == short {
95                return &spec.name;
96            }
97        }
98        if let Some(c) = short.chars().next() {
99            if spec.name.starts_with(c) {
100                return &spec.name;
101            }
102        }
103    }
104    short
105}
106
107/// Coerce a string value to the target type.
108///
109/// Used by the macro-generated dispatch. For custom types, implement FromStr.
110pub fn coerce_bool(s: &str) -> Result<bool, ModuleError> {
111    let s = s.to_lowercase();
112    if s == "true" || s == "1" || s == "yes" || s == "on" {
113        Ok(true)
114    } else if s == "false" || s == "0" || s == "no" || s == "off" {
115        Ok(false)
116    } else {
117        Err(ModuleError::Other(format!("invalid bool: {s}")))
118    }
119}