1use std::fmt;
8use std::str::FromStr;
9
10use crate::{ConfigObject, ConfigValue};
11
12#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
14pub enum PathError {
15 #[error("expected KEY.PATH=VALUE")]
17 MissingEquals,
18 #[error("empty path")]
20 EmptyPath,
21 #[error("empty segment in path `{path}`")]
23 EmptySegment {
24 path: String,
26 },
27 #[error("malformed index in path `{path}`")]
29 BadIndex {
30 path: String,
32 },
33 #[error("`{path}` contains an array index; merge paths take keys only")]
35 IndexInKeyPath {
36 path: String,
38 },
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
43pub enum Seg {
44 Key(String),
46 Index(usize),
48}
49
50pub fn render_path(path: &[Seg]) -> String {
52 let mut out = String::new();
53 for seg in path {
54 match seg {
55 Seg::Key(k) => {
56 if !out.is_empty() {
57 out.push('.');
58 }
59 out.push_str(k);
60 }
61 Seg::Index(i) => out.push_str(&format!("[{i}]")),
62 }
63 }
64 out
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
71pub struct RefPath {
72 path: Vec<Seg>,
73}
74
75impl RefPath {
76 pub fn from_keys(keys: Vec<String>) -> Result<Self, PathError> {
78 if keys.is_empty() {
79 return Err(PathError::EmptyPath);
80 }
81 if keys.iter().any(|k| k.is_empty()) {
82 return Err(PathError::EmptySegment {
83 path: keys.join("."),
84 });
85 }
86 Ok(Self {
87 path: keys.into_iter().map(Seg::Key).collect(),
88 })
89 }
90
91 pub fn segs(&self) -> &[Seg] {
93 &self.path
94 }
95
96 pub fn into_segs(self) -> Vec<Seg> {
98 self.path
99 }
100
101 pub fn try_into_keys(self) -> Result<Vec<String>, PathError> {
103 if self.path.iter().any(|seg| matches!(seg, Seg::Index(_))) {
104 return Err(PathError::IndexInKeyPath {
105 path: render_path(&self.path),
106 });
107 }
108 Ok(self
109 .path
110 .into_iter()
111 .map(|seg| match seg {
112 Seg::Key(key) => key,
113 Seg::Index(_) => unreachable!("index steps were rejected above"),
114 })
115 .collect())
116 }
117}
118
119impl FromStr for RefPath {
120 type Err = PathError;
121
122 fn from_str(body: &str) -> Result<Self, Self::Err> {
124 if body.is_empty() {
125 return Err(PathError::EmptyPath);
126 }
127 let bad_index = || PathError::BadIndex {
128 path: body.to_string(),
129 };
130 let empty_segment = || PathError::EmptySegment {
131 path: body.to_string(),
132 };
133
134 let bytes = body.as_bytes();
135 let mut path = Vec::new();
136 let mut cursor = 0;
137
138 let (key, next) = take_key(body, cursor);
140 if key.is_empty() {
141 return Err(if bytes[cursor] == b']' {
142 bad_index()
143 } else {
144 empty_segment()
145 });
146 }
147 path.push(Seg::Key(key));
148 cursor = next;
149
150 while cursor < body.len() {
151 match bytes[cursor] {
152 b'.' => {
153 let (key, next) = take_key(body, cursor + 1);
154 if key.is_empty() {
155 return Err(empty_segment());
156 }
157 path.push(Seg::Key(key));
158 cursor = next;
159 }
160 b'[' => {
161 let digits_start = cursor + 1;
162 let mut end = digits_start;
163 while end < body.len() && bytes[end].is_ascii_digit() {
164 end += 1;
165 }
166 if end == digits_start || bytes.get(end) != Some(&b']') {
167 return Err(bad_index());
168 }
169 let index: usize = body[digits_start..end].parse().map_err(|_| bad_index())?;
170 path.push(Seg::Index(index));
171 cursor = end + 1;
172 }
173 _ => return Err(bad_index()),
175 }
176 }
177
178 Ok(Self { path })
179 }
180}
181
182fn take_key(body: &str, from: usize) -> (String, usize) {
184 let mut end = from;
185 while end < body.len() && !matches!(body.as_bytes()[end], b'.' | b'[' | b']') {
186 end += 1;
187 }
188 (body[from..end].to_string(), end)
189}
190
191impl fmt::Display for RefPath {
192 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193 f.write_str(&render_path(&self.path))
194 }
195}
196
197pub(crate) fn render_keys(path: &[String]) -> String {
199 if path.is_empty() {
200 "<root>".to_string()
201 } else {
202 path.join(".")
203 }
204}
205
206pub(crate) fn lookup<'a, V: ConfigValue>(root: &'a V, path: &[Seg]) -> Option<&'a V> {
208 let mut value = root;
209 for seg in path {
210 value = match seg {
211 Seg::Key(k) => value.as_object()?.get(k)?,
212 Seg::Index(i) => value.as_array()?.get(*i)?,
213 };
214 }
215 Some(value)
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221 use serde_json::Value;
222 type Map = serde_json::Map<String, Value>;
223
224 #[test]
225 fn render_path_mixes_keys_and_indices() {
226 assert_eq!(render_path(&[]), "");
227 assert_eq!(
228 render_path(&[Seg::Key("a".into()), Seg::Key("b".into())]),
229 "a.b"
230 );
231 assert_eq!(
232 render_path(&[Seg::Key("tags".into()), Seg::Index(0)]),
233 "tags[0]"
234 );
235 }
236
237 fn seg(key: &str) -> Seg {
238 Seg::Key(key.into())
239 }
240
241 #[test]
242 fn ref_path_parses_mixed_steps() {
243 let cases: &[(&str, &[Seg])] = &[
244 ("a", &[seg("a")]),
245 ("a.b", &[seg("a"), seg("b")]),
246 ("a[0]", &[seg("a"), Seg::Index(0)]),
247 ("a.b[2].c", &[seg("a"), seg("b"), Seg::Index(2), seg("c")]),
248 ("a[0][1]", &[seg("a"), Seg::Index(0), Seg::Index(1)]),
249 ("a:b[3]", &[seg("a:b"), Seg::Index(3)]),
251 ];
252 for (body, want) in cases {
253 let parsed: RefPath = body.parse().unwrap();
254 assert_eq!(parsed.segs(), *want, "{body}");
255 }
256 }
257
258 #[test]
259 fn ref_path_rejects_malformed_bodies() {
260 let cases: &[(&str, PathError)] = &[
261 (".a", empty_segment(".a")),
262 ("a..b", empty_segment("a..b")),
263 ("a[0]..b", empty_segment("a[0]..b")),
264 ("[0].a", empty_segment("[0].a")),
265 ("a[]", bad_index("a[]")),
266 ("a[x]", bad_index("a[x]")),
267 ("a[1", bad_index("a[1")),
268 ("a[-1]", bad_index("a[-1]")),
269 ("a]", bad_index("a]")),
270 (
271 "a[99999999999999999999999999]",
272 bad_index("a[99999999999999999999999999]"),
273 ),
274 ];
275 for (body, want) in cases {
276 assert_eq!(&body.parse::<RefPath>().unwrap_err(), want, "{body}");
277 }
278 assert_eq!("".parse::<RefPath>().unwrap_err(), PathError::EmptyPath);
279 }
280
281 fn empty_segment(path: &str) -> PathError {
282 PathError::EmptySegment { path: path.into() }
283 }
284
285 fn bad_index(path: &str) -> PathError {
286 PathError::BadIndex { path: path.into() }
287 }
288
289 #[test]
290 fn ref_path_displays_as_rendered_witness() {
291 let parsed: RefPath = "a.b[2].c".parse().unwrap();
292 assert_eq!(parsed.to_string(), "a.b[2].c");
293 assert_eq!("a.b".parse::<RefPath>().unwrap().to_string(), "a.b");
294 }
295
296 #[test]
297 fn from_keys_rejects_empty_path_and_empty_segments() {
298 assert_eq!(
299 RefPath::from_keys(vec![]).unwrap_err(),
300 PathError::EmptyPath
301 );
302 assert_eq!(
303 RefPath::from_keys(vec!["".into()]).unwrap_err(),
304 PathError::EmptySegment { path: "".into() }
305 );
306 assert_eq!(
307 RefPath::from_keys(vec!["a".into(), "".into()]).unwrap_err(),
308 PathError::EmptySegment { path: "a.".into() }
309 );
310 assert_eq!(
312 RefPath::from_keys(vec!["db".into(), "plugins".into()]).unwrap(),
313 "db.plugins".parse().unwrap()
314 );
315 }
316
317 #[test]
318 fn try_into_keys_passes_all_key_paths_whole() {
319 let parsed: RefPath = "db.plugins".parse().unwrap();
320 assert_eq!(parsed.try_into_keys().unwrap(), ["db", "plugins"]);
321 }
322
323 #[test]
324 fn try_into_keys_rejects_index_steps_with_the_full_path() {
325 for (indexed, want) in [("a[0]", "a[0]"), ("a.b[2].c", "a.b[2].c")] {
326 let err = indexed
327 .parse::<RefPath>()
328 .unwrap()
329 .try_into_keys()
330 .unwrap_err();
331 assert_eq!(err, PathError::IndexInKeyPath { path: want.into() });
332 }
333 let err = "a[0]"
334 .parse::<RefPath>()
335 .unwrap()
336 .try_into_keys()
337 .unwrap_err();
338 assert_eq!(
339 err.to_string(),
340 "`a[0]` contains an array index; merge paths take keys only"
341 );
342 }
343
344 #[test]
345 fn equals_is_an_ordinary_key_character() {
346 let parsed: RefPath = "a=b".parse().unwrap();
347 assert_eq!(parsed.try_into_keys().unwrap(), ["a=b"]);
348 }
349
350 fn doc() -> Value {
351 let mut inner = Map::new();
352 inner.insert("host".into(), Value::String("h".into()));
353 let mut root = Map::new();
354 root.insert("db".into(), Value::Object(inner));
355 root.insert("tags".into(), Value::Array(vec![Value::Number(1.into())]));
356 Value::Object(root)
357 }
358
359 #[test]
360 fn lookup_walks_objects_and_arrays() {
361 let doc = doc();
362 assert_eq!(lookup(&doc, &[]), Some(&doc));
363 assert_eq!(
364 lookup(&doc, &[Seg::Key("db".into()), Seg::Key("host".into())]),
365 Some(&Value::String("h".into()))
366 );
367 assert_eq!(
368 lookup(&doc, &[Seg::Key("tags".into()), Seg::Index(0)]),
369 Some(&Value::Number(1.into()))
370 );
371 }
372
373 #[test]
374 fn lookup_misses_are_none() {
375 let doc = doc();
376 assert_eq!(lookup(&doc, &[Seg::Key("nope".into())]), None);
377 assert_eq!(lookup(&doc, &[Seg::Key("db".into()), Seg::Index(0)]), None);
378 assert_eq!(
379 lookup(&doc, &[Seg::Key("tags".into()), Seg::Index(9)]),
380 None
381 );
382 }
383}