Skip to main content

mabi_cli/
validation.rs

1//! Reusable CLI argument validators.
2//!
3//! Provides `value_parser` compatible functions for clap argument validation.
4//! Each validator returns `Result<T, String>` as required by clap.
5
6use mabi_core::tags::{parse_tag_string, Tags};
7
8/// Validates that a port number is within the usable range (1–65535).
9///
10/// Port 0 is rejected because it causes OS-assigned ephemeral port binding,
11/// which is not meaningful for a simulator that clients need to connect to.
12pub fn parse_port(s: &str) -> Result<u16, String> {
13    let port: u16 = s
14        .parse()
15        .map_err(|_| format!("'{s}' is not a valid port number"))?;
16    if port == 0 {
17        return Err("port must be between 1 and 65535 (port 0 is not allowed)".to_string());
18    }
19    Ok(port)
20}
21
22/// Validates that a count value is at least 1.
23///
24/// Zero-count resources (devices, objects, nodes, groups) produce a server
25/// with nothing to simulate, which is almost certainly a user mistake.
26pub fn parse_nonzero_count(s: &str) -> Result<usize, String> {
27    let n: usize = s
28        .parse()
29        .map_err(|_| format!("'{s}' is not a valid number"))?;
30    if n == 0 {
31        return Err("value must be at least 1".to_string());
32    }
33    Ok(n)
34}
35
36/// Tag entry parsed from CLI argument.
37///
38/// Represents either a key-value tag or a label (key only).
39#[derive(Debug, Clone)]
40pub struct TagEntry {
41    pub key: String,
42    pub value: Option<String>,
43}
44
45/// Parses a tag argument in the format "key=value" or "label".
46///
47/// - "location=building-a" -> TagEntry { key: "location", value: Some("building-a") }
48/// - "critical" -> TagEntry { key: "critical", value: None } (label)
49pub fn parse_tag(s: &str) -> Result<TagEntry, String> {
50    let (key, value) = parse_tag_string(s)?;
51    Ok(TagEntry { key, value })
52}
53
54/// Converts a slice of TagEntry into a Tags object.
55pub fn tags_from_entries(entries: &[TagEntry]) -> Tags {
56    let mut tags = Tags::new();
57    for entry in entries {
58        match &entry.value {
59            Some(v) => tags.insert(&entry.key, v),
60            None => tags.add_label(&entry.key),
61        }
62    }
63    tags
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn test_parse_port_valid() {
72        assert_eq!(parse_port("1").unwrap(), 1);
73        assert_eq!(parse_port("3671").unwrap(), 3671);
74        assert_eq!(parse_port("65535").unwrap(), 65535);
75    }
76
77    #[test]
78    fn test_parse_port_zero_rejected() {
79        assert!(parse_port("0").is_err());
80    }
81
82    #[test]
83    fn test_parse_port_invalid_string() {
84        assert!(parse_port("abc").is_err());
85        assert!(parse_port("-1").is_err());
86        assert!(parse_port("99999").is_err());
87    }
88
89    #[test]
90    fn test_parse_nonzero_count_valid() {
91        assert_eq!(parse_nonzero_count("1").unwrap(), 1);
92        assert_eq!(parse_nonzero_count("50000").unwrap(), 50000);
93    }
94
95    #[test]
96    fn test_parse_nonzero_count_zero_rejected() {
97        assert!(parse_nonzero_count("0").is_err());
98    }
99
100    #[test]
101    fn test_parse_nonzero_count_invalid() {
102        assert!(parse_nonzero_count("abc").is_err());
103        assert!(parse_nonzero_count("-1").is_err());
104    }
105
106    #[test]
107    fn test_parse_tag_key_value() {
108        let entry = parse_tag("location=building-a").unwrap();
109        assert_eq!(entry.key, "location");
110        assert_eq!(entry.value, Some("building-a".to_string()));
111    }
112
113    #[test]
114    fn test_parse_tag_label() {
115        let entry = parse_tag("critical").unwrap();
116        assert_eq!(entry.key, "critical");
117        assert_eq!(entry.value, None);
118    }
119
120    #[test]
121    fn test_parse_tag_empty_value() {
122        let entry = parse_tag("key=").unwrap();
123        assert_eq!(entry.key, "key");
124        assert_eq!(entry.value, Some("".to_string()));
125    }
126
127    #[test]
128    fn test_parse_tag_invalid() {
129        assert!(parse_tag("").is_err());
130        assert!(parse_tag("=value").is_err());
131    }
132
133    #[test]
134    fn test_tags_from_entries() {
135        let entries = vec![
136            TagEntry { key: "location".to_string(), value: Some("building-a".to_string()) },
137            TagEntry { key: "floor".to_string(), value: Some("3".to_string()) },
138            TagEntry { key: "critical".to_string(), value: None },
139        ];
140
141        let tags = tags_from_entries(&entries);
142        assert_eq!(tags.get("location"), Some("building-a"));
143        assert_eq!(tags.get("floor"), Some("3"));
144        assert!(tags.has_label("critical"));
145    }
146}