Skip to main content

openstranded_common_helpers/
lib.rs

1//! # openstranded-common-helpers
2//!
3//! Shared helper functions for parsing `.inf` file fields across
4//! all `openstranded-common-*` domain type crates.
5//!
6//! These functions were previously duplicated in every common-* crate.
7//! They operate on the `HashMap<String, Vec<String>>` structure produced
8//! by the `inf2ron` parser.
9//!
10//! ## Functions
11//!
12//! | Function | Returns | Description |
13//! |----------|---------|-------------|
14//! | [`first_str`] | `Option<&str>` | Get first value of a field as `&str` |
15//! | [`first_string`] | `Option<String>` | Get first value as owned `String` |
16//! | [`first_u32`] | `Option<u32>` | Get first value parsed as `u32` |
17//! | [`first_f32`] | `Option<f32>` | Get first value parsed as `f32` |
18//! | [`parse_color`] | `Option<[u8; 3]>` | Parse `"R,G,B"` colour string |
19//! | [`parse_optional_u32`] | `Option<u32>` | Parse a field that may be `"none"` or a number |
20
21use std::collections::HashMap;
22
23/// Parse a colour string `"R,G,B"` into `[R, G, B]`.
24///
25/// Each component must be a valid integer between 0 and 255.
26/// Returns `None` if the string does not have exactly 3 comma-separated values.
27///
28/// # Examples
29///
30/// ```
31/// use openstranded_common_helpers::parse_color;
32///
33/// assert_eq!(parse_color("255,0,128"), Some([255, 0, 128]));
34/// assert_eq!(parse_color("invalid"), None);
35/// ```
36#[must_use]
37pub fn parse_color(s: &str) -> Option<[u8; 3]> {
38    let parts: Vec<&str> = s.split(',').collect();
39    if parts.len() != 3 {
40        return None;
41    }
42    Some([
43        parts[0].trim().parse().ok()?,
44        parts[1].trim().parse().ok()?,
45        parts[2].trim().parse().ok()?,
46    ])
47}
48
49/// Get the first value of a field as `&str`, if present.
50///
51/// # Examples
52///
53/// ```
54/// use std::collections::HashMap;
55/// use openstranded_common_helpers::first_str;
56///
57/// let mut fields = HashMap::new();
58/// fields.insert("name".into(), vec!["Wood".into()]);
59/// assert_eq!(first_str(&fields, "name"), Some("Wood"));
60/// assert_eq!(first_str(&fields, "missing"), None);
61/// ```
62#[must_use]
63pub fn first_str<'a>(fields: &'a HashMap<String, Vec<String>>, key: &str) -> Option<&'a str> {
64    fields.get(key)?.first().map(String::as_str)
65}
66
67/// Get the first value of a field parsed as `u32`.
68///
69/// # Examples
70///
71/// ```
72/// use std::collections::HashMap;
73/// use openstranded_common_helpers::first_u32;
74///
75/// let mut fields = HashMap::new();
76/// fields.insert("id".into(), vec!["42".into()]);
77/// assert_eq!(first_u32(&fields, "id"), Some(42));
78/// assert_eq!(first_u32(&fields, "missing"), None);
79/// ```
80#[must_use]
81pub fn first_u32(fields: &HashMap<String, Vec<String>>, key: &str) -> Option<u32> {
82    first_str(fields, key)?.parse().ok()
83}
84
85/// Get the first value of a field parsed as `f32`.
86///
87/// # Examples
88///
89/// ```
90/// use std::collections::HashMap;
91/// use openstranded_common_helpers::first_f32;
92///
93/// let mut fields = HashMap::new();
94/// fields.insert("weight".into(), vec!["3.5".into()]);
95/// let val = first_f32(&fields, "weight");
96/// assert!((val.unwrap() - 3.5).abs() < 1e-6);
97/// ```
98#[must_use]
99pub fn first_f32(fields: &HashMap<String, Vec<String>>, key: &str) -> Option<f32> {
100    first_str(fields, key)?.parse().ok()
101}
102
103/// Get the first value of a field as an owned `String`.
104///
105/// # Examples
106///
107/// ```
108/// use std::collections::HashMap;
109/// use openstranded_common_helpers::first_string;
110///
111/// let mut fields = HashMap::new();
112/// fields.insert("name".into(), vec!["Wood".into()]);
113/// assert_eq!(first_string(&fields, "name"), Some("Wood".into()));
114/// ```
115#[must_use]
116pub fn first_string(fields: &HashMap<String, Vec<String>>, key: &str) -> Option<String> {
117    first_str(fields, key).map(ToOwned::to_owned)
118}
119
120/// Parse a field that may be `"none"` (returns `None`) or a number.
121///
122/// Used for optional numeric fields where a sentinel string indicates absence.
123///
124/// # Examples
125///
126/// ```
127/// use openstranded_common_helpers::parse_optional_u32;
128///
129/// assert_eq!(parse_optional_u32("42"), Some(Some(42)));
130/// assert_eq!(parse_optional_u32("none"), Some(None));
131/// assert_eq!(parse_optional_u32("invalid"), None);
132/// ```
133#[must_use]
134pub fn parse_optional_u32(s: &str) -> Option<Option<u32>> {
135    if s.eq_ignore_ascii_case("none") {
136        return Some(None);
137    }
138    s.parse::<u32>().ok().map(Some)
139}
140
141// ── Tests ──────────────────────────────────────────────────────────
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn test_parse_color_rgb() {
149        assert_eq!(parse_color("255,0,128"), Some([255, 0, 128]));
150    }
151
152    #[test]
153    fn test_parse_color_whitespace() {
154        assert_eq!(parse_color(" 10 , 20 , 30 "), Some([10, 20, 30]));
155    }
156
157    #[test]
158    fn test_parse_color_too_few() {
159        assert_eq!(parse_color("255,0"), None);
160    }
161
162    #[test]
163    fn test_parse_color_too_many() {
164        assert_eq!(parse_color("1,2,3,4"), None);
165    }
166
167    #[test]
168    fn test_parse_color_invalid() {
169        assert_eq!(parse_color("abc,def,ghi"), None);
170    }
171
172    #[test]
173    fn test_parse_color_empty() {
174        assert_eq!(parse_color(""), None);
175    }
176
177    #[test]
178    fn test_first_str_found() {
179        let mut fields = HashMap::new();
180        fields.insert("name".into(), vec!["Wood".into()]);
181        assert_eq!(first_str(&fields, "name"), Some("Wood"));
182    }
183
184    #[test]
185    fn test_first_str_not_found() {
186        let fields = HashMap::new();
187        assert_eq!(first_str(&fields, "missing"), None);
188    }
189
190    #[test]
191    fn test_first_str_multiple_values() {
192        let mut fields = HashMap::new();
193        fields.insert("key".into(), vec!["first".into(), "second".into()]);
194        assert_eq!(first_str(&fields, "key"), Some("first"));
195    }
196
197    #[test]
198    fn test_first_u32_ok() {
199        let mut fields = HashMap::new();
200        fields.insert("id".into(), vec!["42".into()]);
201        assert_eq!(first_u32(&fields, "id"), Some(42));
202    }
203
204    #[test]
205    fn test_first_u32_invalid() {
206        let mut fields = HashMap::new();
207        fields.insert("id".into(), vec!["not_a_number".into()]);
208        assert_eq!(first_u32(&fields, "id"), None);
209    }
210
211    #[test]
212    fn test_first_u32_not_found() {
213        let fields = HashMap::new();
214        assert_eq!(first_u32(&fields, "missing"), None);
215    }
216
217    #[test]
218    fn test_first_f32_ok() {
219        let mut fields = HashMap::new();
220        fields.insert("weight".into(), vec!["3.5".into()]);
221        let val = first_f32(&fields, "weight").unwrap();
222        assert!((val - 3.5).abs() < 1e-6);
223    }
224
225    #[test]
226    fn test_first_f32_integer_string() {
227        let mut fields = HashMap::new();
228        fields.insert("val".into(), vec!["7".into()]);
229        let val = first_f32(&fields, "val").unwrap();
230        assert!((val - 7.0).abs() < 1e-6);
231    }
232
233    #[test]
234    fn test_first_f32_not_found() {
235        let fields = HashMap::new();
236        assert_eq!(first_f32(&fields, "missing"), None);
237    }
238
239    #[test]
240    fn test_first_string_ok() {
241        let mut fields = HashMap::new();
242        fields.insert("name".into(), vec!["Wood".into()]);
243        assert_eq!(first_string(&fields, "name"), Some("Wood".into()));
244    }
245
246    #[test]
247    fn test_first_string_not_found() {
248        let fields = HashMap::new();
249        assert_eq!(first_string(&fields, "missing"), None);
250    }
251
252    #[test]
253    fn test_parse_optional_u32_number() {
254        assert_eq!(parse_optional_u32("42"), Some(Some(42)));
255    }
256
257    #[test]
258    fn test_parse_optional_u32_none() {
259        assert_eq!(parse_optional_u32("none"), Some(None));
260    }
261
262    #[test]
263    fn test_parse_optional_u32_none_uppercase() {
264        assert_eq!(parse_optional_u32("NONE"), Some(None));
265    }
266
267    #[test]
268    fn test_parse_optional_u32_invalid() {
269        assert_eq!(parse_optional_u32("abc"), None);
270    }
271}