gtoml/
lib.rs

1use regex::Regex;
2use toml::Value;
3
4/// Parses a TOML string and returns a `Value` type.
5///
6/// # Arguments
7/// * `toml_str` - A reference to a string containing TOML data.
8///
9/// # Returns
10/// * `Result<Value, toml::de::Error>` - Returns a `Value` if parsing is successful,
11///   otherwise returns a `toml::de::Error`.
12pub fn parse(toml_str: &str) -> Result<Value, toml::de::Error> {
13    toml::from_str(toml_str)
14}
15
16/// Retrieves a value from a TOML `Value` at a specified path.
17///
18/// # Arguments
19/// * `value` - A reference to the TOML `Value` to search in.
20/// * `path` - A reference to a string representing the path to the desired value.
21///
22/// # Returns
23/// * `Result<Option<Value>, String>` - Returns `Ok(Some(Value))` if the value is found,
24///   `Ok(None)` if the path does not exist, or an `Err` with an error message if there is an issue.
25pub fn get<'a>(value: &'a Value, path: &str) -> Result<Option<Value>, String> {
26    let mut current = value;
27    for part in path.split('.') {
28        current = match current {
29            // If the current value is a table
30            Value::Table(map) => {
31                if part.contains('*') || part.contains('?') {
32                    // Replace wildcard characters with regex patterns
33                    let pattern = part.replace('*', ".*").replace('?', ".");
34                    let regex = Regex::new(&pattern).map_err(|e| e.to_string())?;
35                    // Filter and collect values that match the regex pattern
36                    let results: Vec<_> = map
37                       .iter()
38                       .filter(|(key, _)| regex.is_match(key))
39                       .map(|(_, value)| value.clone())
40                       .collect();
41                    return Ok(Some(Value::Array(results)));
42                } else {
43                    // Get the value at the specified key
44                    map.get(part).ok_or_else(|| format!("Key not found: {}", part))?
45                }
46            }
47            // If the current value is an array
48            Value::Array(arr) => {
49                if part == "#" {
50                    // Return the length of the array
51                    return Ok(Some(Value::Integer(arr.len() as i64)));
52                } else if part.contains('*') || part.contains('?') {
53                    // Replace wildcard characters with regex patterns
54                    let pattern = part.replace('*', ".*").replace('?', ".");
55                    let regex = Regex::new(&pattern).map_err(|e| e.to_string())?;
56                    // Filter and collect values that match the regex pattern
57                    let results: Vec<_> = arr
58                       .iter()
59                       .filter(|item| regex.is_match(&item.to_string()))
60                       .cloned()
61                       .collect();
62                    return Ok(Some(Value::Array(results)));
63                } else {
64                    if let Ok(index) = part.parse::<usize>() {
65                        // Get the value at the specified index
66                        arr.get(index).ok_or_else(|| format!("Index out of bounds: {}", index))?
67                    } else {
68                        return Ok(None);
69                    }
70                }
71            }
72            // If the current value is neither a table nor an array
73            _ => return Ok(None),
74        };
75    }
76    Ok(Some(current.clone()))
77}
78
79/// Retrieves a value from a TOML string at a specified path.
80///
81/// # Arguments
82/// * `toml_str` - A reference to a string containing TOML data.
83/// * `path` - A reference to a string representing the path to the desired value.
84///
85/// # Returns
86/// * `Result<Option<Value>, String>` - Returns `Ok(Some(Value))` if the value is found,
87///   `Ok(None)` if the path does not exist, or an `Err` with an error message if there is an issue.
88pub fn get_from_str(toml_str: &str, path: &str) -> Result<Option<Value>, String> {
89    let value = parse(toml_str).map_err(|e| e.to_string())?;
90    get(&value, path)
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    /// Test the `parse` and `get` functions with a simple TOML string.
98    #[test]
99    fn test_parse_and_get() {
100        let toml_str = r#"
101            [owner]
102            name = "Tom Preston-Werner"
103            dob = 1979-05-27T07:32:00Z
104        "#;
105
106        let value = parse(toml_str).unwrap();
107        assert_eq!(
108            get(&value, "owner.name").unwrap(),
109            Some(Value::String("Tom Preston-Werner".to_string()))
110        );
111        assert_eq!(
112            get(&value, "owner.dob").unwrap().unwrap().to_string(),
113            "1979-05-27T07:32:00Z"
114        );
115    }
116
117    /// Test the `get_from_str` function with a simple TOML string.
118    #[test]
119    fn test_get_from_str() {
120        let toml_str = r#"
121            [owner]
122            name = "Tom Preston-Werner"
123            dob = 1979-05-27T07:32:00Z
124        "#;
125
126        assert_eq!(
127            get_from_str(toml_str, "owner.name").unwrap(),
128            Some(Value::String("Tom Preston-Werner".to_string()))
129        );
130        assert_eq!(
131            get_from_str(toml_str, "owner.dob")
132               .unwrap()
133               .unwrap()
134               .to_string(),
135            "1979-05-27T07:32:00Z"
136        );
137    }
138
139    /// Test the `get` function with an array in a TOML string.
140    #[test]
141    fn test_get_array() {
142        let toml_str = r#"
143            [[people]]
144            name = "Alice"
145            age = 30
146
147            [[people]]
148            name = "Bob"
149            age = 25
150        "#;
151
152        let value = parse(toml_str).unwrap();
153        assert_eq!(
154            get(&value, "people.0.name").unwrap(),
155            Some(Value::String("Alice".to_string()))
156        );
157        assert_eq!(
158            get(&value, "people.1.age").unwrap(),
159            Some(Value::Integer(25))
160        );
161    }
162
163    /// Test the `get` function with a nested array in a TOML string.
164    #[test]
165    fn test_nested_array() {
166        let toml_str = r#"
167            [[matrix]]
168            values = [1, 2, 3]
169
170            [[matrix]]
171            values = [4, 5, 6]
172        "#;
173
174        let value = parse(toml_str).unwrap();
175        assert_eq!(
176            get(&value, "matrix.0.values.1").unwrap(),
177            Some(Value::Integer(2))
178        );
179        assert_eq!(
180            get(&value, "matrix.1.values.2").unwrap(),
181            Some(Value::Integer(6))
182        );
183    }
184
185    /// Test the `get` function with a complex path in a TOML string.
186    #[test]
187    fn test_complex_path() {
188        let toml_str = r#"
189            [group]
190            [[group.people]]
191            name = "Alice"
192            age = 30
193
194            [[group.people]]
195            name = "Bob"
196            age = 25
197        "#;
198
199        let value = parse(toml_str).unwrap();
200        assert_eq!(
201            get(&value, "group.people.0.name").unwrap(),
202            Some(Value::String("Alice".to_string()))
203        );
204        assert_eq!(
205            get(&value, "group.people.1.age").unwrap(),
206            Some(Value::Integer(25))
207        );
208    }
209
210    /// Test the `get` function to get the length of an array in a TOML string.
211    #[test]
212    fn test_get_array_length() {
213        let toml_str = r#"
214            children = ["Sara", "Alex", "Jack"]
215        "#;
216
217        let value = parse(toml_str).unwrap();
218        assert_eq!(get(&value, "children.#").unwrap(), Some(Value::Integer(3)));
219    }
220
221    /// Test the `get` function with a wildcard in a TOML string.
222    #[test]
223    fn test_get_array_wildcard() {
224        let toml_str = r#"
225            children = ["Sara", "Alex", "Jack"]
226        "#;
227
228        let value = parse(toml_str).unwrap();
229        assert_eq!(
230            get(&value, "children.2").unwrap(),
231            Some(Value::String("Jack".to_string()))
232        );
233    }
234
235    /// Test the `get` function with a question mark in a TOML string.
236    #[test]
237    fn test_get_array_question_mark() {
238        let toml_str = r#"
239            children = ["Sara", "Alex", "Jack"]
240        "#;
241
242        let value = parse(toml_str).unwrap();
243        assert_eq!(
244            get(&value, "children.0").unwrap(),
245            Some(Value::String("Sara".to_string()))
246        );
247    }
248
249    /// Test the `get` function with a nested array to get specific values in a TOML string.
250    #[test]
251    fn test_get_nested_array() {
252        let toml_str = r#"
253            friends = [
254                { first = "James", last = "Murphy" },
255                { first = "Roger", last = "Craig" }
256            ]
257        "#;
258
259        let value = parse(toml_str).unwrap();
260        let result = get(&value, "friends").unwrap().unwrap();
261        if let Value::Array(arr) = result {
262            let first_names: Vec<Value> = arr
263               .iter()
264               .filter_map(|item| {
265                    if let Value::Table(map) = item {
266                        map.get("first").cloned()
267                    } else {
268                        None
269                    }
270                })
271               .collect();
272            assert_eq!(
273                first_names,
274                vec![
275                    Value::String("James".to_string()),
276                    Value::String("Roger".to_string())
277                ]
278            );
279        } else {
280            panic!("Expected an array");
281        }
282    }
283}