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
171pub 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
186pub 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 }