toml_path/
lib.rs

1use eyre::bail;
2use eyre::Result;
3use log::debug;
4use std::fs;
5use std::path::Path;
6use std::str::FromStr;
7use toml::{Table, Value};
8use winnow::ascii::alphanumeric1;
9use winnow::ascii::dec_int;
10use winnow::ascii::space0;
11use winnow::combinator::delimited;
12use winnow::combinator::repeat;
13use winnow::combinator::separated;
14use winnow::combinator::separated_pair;
15use winnow::combinator::seq;
16use winnow::prelude::*;
17use winnow::token::take_while;
18
19mod toml_path;
20pub use toml_path::{Index, Op, TomlPath};
21
22pub fn traverse(value: &Value, path: &[Op]) -> Result<Value> {
23    let current_op = &path[0];
24    let num_ops = path.len();
25    match value {
26        Value::String(string) => {
27            if num_ops > 1 {
28                bail!(
29                    "Hit the end of toml tree (string: '{}') but path has more parts left: {:?}",
30                    string,
31                    path[1..].to_vec()
32                );
33            }
34            return Ok(value.clone());
35        }
36        Value::Integer(int) => {
37            if num_ops > 1 {
38                bail!(
39                    "Hit the end of toml tree (integer: {}) but path has more parts left: {:?}",
40                    int,
41                    path[1..].to_vec()
42                );
43            }
44            return Ok(value.clone());
45        }
46        Value::Float(float) => {
47            if num_ops > 1 {
48                bail!(
49                    "Hit the end of toml tree (float: {}) but path has more parts left: {:?}",
50                    float,
51                    path[1..].to_vec()
52                );
53            }
54            return Ok(value.clone());
55        }
56        Value::Boolean(bool) => {
57            if num_ops > 1 {
58                bail!(
59                    "Hit the end of toml tree (bool: {}) but path has more parts left: {:?}",
60                    bool,
61                    path[1..].to_vec()
62                );
63            }
64            return Ok(value.clone());
65        }
66        Value::Datetime(date) => {
67            if num_ops > 1 {
68                bail!(
69                    "Hit the end of toml tree (datetime: '{}') but path has more parts left: {:?}",
70                    date,
71                    path[1..].to_vec()
72                );
73            }
74            return Ok(value.clone());
75        }
76        Value::Array(array) => match current_op {
77            Op::Dot => {
78                if num_ops == 1 {
79                    return Ok(value.clone());
80                }
81                return traverse(&value, &path[1..]);
82            }
83            Op::Name(name) => {
84                bail!("Cannot index array with string ({:?})", name);
85            }
86            Op::BracketIndex(indexes) => {
87                let num_items = array.len();
88                let mut filtered_values: Vec<Value> = Vec::new();
89                for index in indexes {
90                    match index {
91                        Index::Number(i_signed) => {
92                            let i_unsigned = if *i_signed < 1 {
93                                (num_items as isize + i_signed) as usize
94                            } else {
95                                *i_signed as usize
96                            };
97                            let Some(item) = array.get(i_unsigned) else {
98                                bail!("No item at index {} in array ({:?})", i_unsigned, array);
99                            };
100                            filtered_values.push(item.clone());
101                        }
102                        Index::Range(range) => {
103                            for i in range.gen_range_indexes(num_items)? {
104                                let Some(item): Option<&Value> = array.get(i) else {
105                                    bail!(
106                                        "No item at index {} (from range {:?}) in array ({:?})",
107                                        i,
108                                        range,
109                                        array
110                                    );
111                                };
112                                filtered_values.push(item.clone());
113                            }
114                        }
115                    }
116                }
117                let subset = Value::Array(filtered_values);
118                if num_ops == 1 {
119                    return Ok(subset);
120                }
121                return traverse(&subset, &path[1..]);
122            }
123            Op::BracketName(names) => {
124                bail!("Cannot index array with strings ({:?})", names);
125            }
126        },
127        Value::Table(table) => match current_op {
128            Op::Dot => {
129                if num_ops == 1 {
130                    return Ok(value.clone());
131                }
132                return traverse(&value, &path[1..]);
133            }
134            Op::Name(name) => {
135                let Some(section) = table.get(name) else {
136                    bail!("Could not find key '{:?}' in table ({:?})", name, table);
137                };
138                if num_ops == 1 {
139                    return Ok(section.clone());
140                }
141                return traverse(section, &path[1..]);
142            }
143            Op::BracketIndex(indexes) => {
144                bail!("Cannot index table with indexes ({:?})", indexes)
145            }
146            Op::BracketName(names) => {
147                let mut filtered_values: Vec<Value> = Vec::new();
148
149                for name in names {
150                    let Some(section) = table.get(name) else {
151                        bail!(
152                            "Could not find key '{:?}' (from keys ({:?}) in table ({:?})",
153                            name,
154                            names,
155                            table
156                        );
157                    };
158                    filtered_values.push(section.clone());
159                }
160
161                let subset = Value::Array(filtered_values);
162                if num_ops == 1 {
163                    return Ok(subset);
164                }
165                return traverse(&subset, &path[1..]);
166            }
167        },
168    }
169}
170
171/// Get value(s) specified by a tomlpath from a toml
172pub fn get(toml: &Value, path: &TomlPath) -> Result<String> {
173    let value = traverse(&toml, &path.parts())?;
174    debug!("type: {}", value.type_str());
175    let s = match value {
176        Value::Table(ref t) => toml::to_string(&value)?,
177        _ => {
178            let mut s = String::new();
179            serde::Serialize::serialize(&value, toml::ser::ValueSerializer::new(&mut s))?;
180            s
181        }
182    };
183    Ok(s)
184}
185
186/// Convienence wrapper for 'get' to get a value directly from a file.
187pub fn get_from_file(file: &Path, path: &str) -> Result<String> {
188    let file = file.canonicalize()?;
189    debug!("Reading file: {}", file.display());
190    let contents = fs::read_to_string(file)?;
191    let toml: Value = toml::from_str(&contents)?;
192    let toml_path = TomlPath::from_str(path)?;
193    let result = get(&toml, &toml_path)?;
194    Ok(result)
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use pretty_assertions::assert_eq;
201
202    /*
203    #[test]
204    fn test_() {
205        todo!()
206    }
207    */
208}