Skip to main content

knf/
path.rs

1//! Paths into a document: [`Seg`] steps, the parsed [`RefPath`] (`a.b[2].c`),
2//! and their renderings.
3//!
4//! Writers take keys only; [`RefPath::try_into_keys`] rejects index steps.
5//! Merge selectors use [`crate::glob`] instead.
6
7use std::fmt;
8use std::str::FromStr;
9
10use crate::{ConfigObject, ConfigValue};
11
12/// Why a path expression was rejected. Carries only the path text.
13#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
14pub enum PathError {
15    /// No `=` in a `key.path=value` expression.
16    #[error("expected KEY.PATH=VALUE")]
17    MissingEquals,
18    /// The path is empty (`=1`, an empty `${...}` body, an empty vec).
19    #[error("empty path")]
20    EmptyPath,
21    /// A path segment is empty (`a..b`, `.a`, `a.`).
22    #[error("empty segment in path `{path}`")]
23    EmptySegment {
24        /// The path that contained an empty segment.
25        path: String,
26    },
27    /// A bracket step is malformed: empty, not a number, too big, or unclosed.
28    #[error("malformed index in path `{path}`")]
29    BadIndex {
30        /// The path that contained the bad bracket.
31        path: String,
32    },
33    /// A path given to a writer contains an array index.
34    #[error("`{path}` contains an array index; merge paths take keys only")]
35    IndexInKeyPath {
36        /// The full path, rendered.
37        path: String,
38    },
39}
40
41/// One step of a path into a document.
42#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
43pub enum Seg {
44    /// An object key.
45    Key(String),
46    /// An array position.
47    Index(usize),
48}
49
50/// Renders a path for display: `servers.primary.host`, `tags[0]`.
51pub 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/// A non-empty parsed path: dotted keys plus bracket indices, `a.b[2].c`.
68///
69/// Keys containing `.`, `[` or `]` are not addressable.
70#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
71pub struct RefPath {
72    path: Vec<Seg>,
73}
74
75impl RefPath {
76    /// Build from key strings. Rejects an empty path or any empty segment.
77    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    /// The segments as steps.
92    pub fn segs(&self) -> &[Seg] {
93        &self.path
94    }
95
96    /// The segments as steps, zero-copy.
97    pub fn into_segs(self) -> Vec<Seg> {
98        self.path
99    }
100
101    /// The keys, or an error if any step is an index.
102    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    /// Grammar: a key, then any mix of `.key` and `[N]` steps.
123    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        // The first step must be a key.
139        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                // A stray `]`.
174                _ => return Err(bad_index()),
175            }
176        }
177
178        Ok(Self { path })
179    }
180}
181
182/// The longest run from `from` containing none of `.[]`.
183fn 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
197/// Renders a key path for display. An empty path is the document root.
198pub(crate) fn render_keys(path: &[String]) -> String {
199    if path.is_empty() {
200        "<root>".to_string()
201    } else {
202        path.join(".")
203    }
204}
205
206/// The node at `path`, or `None` if nothing lives there.
207pub(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            // `:` is an ordinary key character.
250            ("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        // from_keys and parsing agree on the dotted all-key grammar.
311        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}