Skip to main content

knf/
path.rs

1//! The path vocabulary: one step type, one parsed spelling, one witness.
2//!
3//! [`Seg`] is the single step every path in the workspace is built from — an
4//! object key or an array index — and [`render_path`] the one display for a
5//! chain of them. Over `Vec<Seg>` there is one parsed spelling and one witness:
6//!
7//! - [`RefPath`] parses `a.b[2].c` — dotted keys plus `[n]` steps. It is the
8//!   only spelling a `${...}` reference uses, and the grammar `--set`'s path
9//!   parses too.
10//! - A bare `Vec<Seg>` is the *witness*: built by walking a document, never
11//!   parsed, and free to hold [`Index`](Seg::Index) — a value can live inside
12//!   an array, and an error must still be able to say so.
13//!
14//! Writers take keys only. Reading an array element has one obvious meaning;
15//! writing one conflicts with arrays replacing wholesale on the merge side, so
16//! an [`Index`](Seg::Index) step can never address a merge location. That
17//! predicate is enforced once, at the boundary, by [`RefPath::try_into_keys`] —
18//! a path that reaches a writer has been through it, and
19//! [`IndexInKeyPath`](PathError::IndexInKeyPath) is the failure. A key
20//! literally spelled `a[0]` is consequently unwritable from the command line
21//! and unreferenceable from `${...}` — the same accepted loss as keys
22//! containing a literal dot, which the dotted grammar has always split.
23//!
24//! Parsing is pure text: nothing here reads a [`Value`](crate::Value), and the
25//! walkers that do — interpolation, `merge_at` — build
26//! or consume these types rather than living in them. Provenance is the
27//! caller's job too: a [`PathError`] carries the path text and never a flag
28//! name or a filename.
29//!
30//! Two renderers, because there are two representations and they disagree
31//! about the empty case. [`render_path`] takes a witness and renders nothing
32//! for an empty one — a [`RefPath`] cannot be empty, so the case never reaches
33//! it from a parse. [`render_keys`] takes the merge side's `&[String]` key
34//! paths, where empty is reachable and means the document root.
35
36use std::fmt;
37use std::str::FromStr;
38
39use crate::Value;
40
41/// Why a path expression was rejected.
42///
43/// Carries the path text and nothing else — no `--set`, no `--interpolate`,
44/// no filenames. Provenance is the caller's job.
45///
46/// [`IndexInKeyPath`](PathError::IndexInKeyPath) is raised by
47/// [`RefPath::try_into_keys`] alone, never by parsing.
48/// [`MissingEquals`](PathError::MissingEquals) has no producer in this module
49/// — a bare [`RefPath`] has no `=` to miss. It belongs to
50/// [`PathLeaf`](crate::PathLeaf), which parses `key.path=value` over this grammar and shares this
51/// error rather than wrapping it, so one spelling of a bad path reads the same
52/// wherever it was typed.
53#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
54pub enum PathError {
55    /// No `=` in a `key.path=value` expression.
56    #[error("expected KEY.PATH=VALUE")]
57    MissingEquals,
58    /// The path is empty (`=1`, an empty `${...}` body, an empty vec).
59    #[error("empty path")]
60    EmptyPath,
61    /// A path segment is empty (`a..b`, `.a`, `a.`).
62    #[error("empty segment in path `{path}`")]
63    EmptySegment {
64        /// The path that contained an empty segment.
65        path: String,
66    },
67    /// A bracket step is malformed: empty, not a number, too big, or unclosed.
68    #[error("malformed index in path `{path}`")]
69    BadIndex {
70        /// The path that contained the bad bracket.
71        path: String,
72    },
73    /// A path that arrived at a writer contained an array index.
74    ///
75    /// Raised by [`RefPath::try_into_keys`] only, never by parsing: writers
76    /// take keys one per segment because arrays replace wholesale on the
77    /// merge side, so there is no meaning to writing element `n`.
78    #[error("`{path}` contains an array index; merge paths take keys only")]
79    IndexInKeyPath {
80        /// The full path, rendered.
81        path: String,
82    },
83}
84
85/// One step of a path into a document.
86///
87/// The single step vocabulary every path in the workspace is built from.
88/// [`RefPath`] parses a chain of these from text; a bare `Vec<Seg>` is the
89/// witness a walker builds by descending a document, free to hold
90/// [`Index`](Seg::Index) because a value can live inside an array.
91#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
92pub enum Seg {
93    /// An object key.
94    Key(String),
95    /// An array position.
96    Index(usize),
97}
98
99/// Renders a path for display: `servers.primary.host`, `tags[0]`.
100pub fn render_path(path: &[Seg]) -> String {
101    let mut out = String::new();
102    for seg in path {
103        match seg {
104            Seg::Key(k) => {
105                if !out.is_empty() {
106                    out.push('.');
107                }
108                out.push_str(k);
109            }
110            Seg::Index(i) => out.push_str(&format!("[{i}]")),
111        }
112    }
113    out
114}
115
116/// A parsed path: dotted keys plus bracket array indices, `a.b[2].c`.
117///
118/// The one spelling over [`Seg`]. References parse it directly; `--set`'s
119/// path parses it too, and its write-side caller then runs
120/// [`try_into_keys`](RefPath::try_into_keys), which rejects any
121/// [`Index`](Seg::Index) step — reading an array element has one obvious
122/// meaning, writing one does not. Empty paths and empty segments are
123/// unrepresentable: both [`from_str`](RefPath::from_str) and
124/// [`from_keys`](RefPath::from_keys) reject them.
125///
126/// `Display` is [`render_path`]. Not injective with respect to document keys:
127/// a key literally spelled `a[0]` exists, but this grammar reads it as a key
128/// and an index — the same accepted loss as keys containing a literal dot.
129#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
130pub struct RefPath {
131    path: Vec<Seg>,
132}
133
134impl RefPath {
135    /// Build from key strings. Rejects an empty path or any empty segment.
136    pub fn from_keys(keys: Vec<String>) -> Result<Self, PathError> {
137        if keys.is_empty() {
138            return Err(PathError::EmptyPath);
139        }
140        if keys.iter().any(|k| k.is_empty()) {
141            return Err(PathError::EmptySegment {
142                path: keys.join("."),
143            });
144        }
145        Ok(Self {
146            path: keys.into_iter().map(Seg::Key).collect(),
147        })
148    }
149
150    /// The segments as steps.
151    pub fn segs(&self) -> &[Seg] {
152        &self.path
153    }
154
155    /// The segments as steps, zero-copy.
156    pub fn into_segs(self) -> Vec<Seg> {
157        self.path
158    }
159
160    /// The owned key strings, checked all-key.
161    ///
162    /// The one write-side predicate: a caller that assigns into a document
163    /// takes keys one per segment, so an [`Index`](Seg::Index) step fails
164    /// here rather than at the merge — before any file is read, and with the
165    /// whole path rendered into the error.
166    pub fn try_into_keys(self) -> Result<Vec<String>, PathError> {
167        if self.path.iter().any(|seg| matches!(seg, Seg::Index(_))) {
168            return Err(PathError::IndexInKeyPath {
169                path: render_path(&self.path),
170            });
171        }
172        Ok(self
173            .path
174            .into_iter()
175            .map(|seg| match seg {
176                Seg::Key(key) => key,
177                Seg::Index(_) => unreachable!("index steps were rejected above"),
178            })
179            .collect())
180    }
181}
182
183impl FromStr for RefPath {
184    type Err = PathError;
185
186    /// Grammar: a leading key, then any mix of `.key` and `[N]` steps, where
187    /// a key is a run of anything but `.[]` and `N` a bare `usize`.
188    fn from_str(body: &str) -> Result<Self, Self::Err> {
189        if body.is_empty() {
190            return Err(PathError::EmptyPath);
191        }
192        let bad_index = || PathError::BadIndex {
193            path: body.to_string(),
194        };
195        let empty_segment = || PathError::EmptySegment {
196            path: body.to_string(),
197        };
198
199        let bytes = body.as_bytes();
200        let mut path = Vec::new();
201        let mut cursor = 0;
202
203        // The first step must be a key: there is no `[0]` into the root.
204        let (key, next) = take_key(body, cursor);
205        if key.is_empty() {
206            // `.a` and `[0].a` open with no key; a stray `]` opens with none.
207            return Err(if bytes[cursor] == b']' {
208                bad_index()
209            } else {
210                empty_segment()
211            });
212        }
213        path.push(Seg::Key(key));
214        cursor = next;
215
216        while cursor < body.len() {
217            match bytes[cursor] {
218                b'.' => {
219                    let (key, next) = take_key(body, cursor + 1);
220                    if key.is_empty() {
221                        return Err(empty_segment());
222                    }
223                    path.push(Seg::Key(key));
224                    cursor = next;
225                }
226                b'[' => {
227                    let digits_start = cursor + 1;
228                    let mut end = digits_start;
229                    while end < body.len() && bytes[end].is_ascii_digit() {
230                        end += 1;
231                    }
232                    if end == digits_start || bytes.get(end) != Some(&b']') {
233                        return Err(bad_index());
234                    }
235                    let index: usize = body[digits_start..end].parse().map_err(|_| bad_index())?;
236                    path.push(Seg::Index(index));
237                    cursor = end + 1;
238                }
239                // A key run ends at `.[` or `]`; anything left over is a stray
240                // bracket.
241                _ => return Err(bad_index()),
242            }
243        }
244
245        Ok(Self { path })
246    }
247}
248
249/// A maximal run containing none of `.[]` — the delimiters are ASCII, so byte
250/// scanning never splits a multibyte key.
251fn take_key(body: &str, from: usize) -> (String, usize) {
252    let mut end = from;
253    while end < body.len() && !matches!(body.as_bytes()[end], b'.' | b'[' | b']') {
254        end += 1;
255    }
256    (body[from..end].to_string(), end)
257}
258
259impl fmt::Display for RefPath {
260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261        f.write_str(&render_path(&self.path))
262    }
263}
264
265/// Renders a key path for display. An empty path is the document root.
266///
267/// The merge side's paths are `&[String]`: keys only, by the invariant
268/// [`RefPath::try_into_keys`] enforces, and reachable empty because the fold
269/// starts at the root. [`render_path`] is the same rendering over a witness
270/// that may hold indices.
271pub(crate) fn render_keys(path: &[String]) -> String {
272    if path.is_empty() {
273        "<root>".to_string()
274    } else {
275        path.join(".")
276    }
277}
278
279/// The node at `path`, or `None` if nothing lives there.
280///
281/// Only interpolation reads a document by path — the merge descends by
282/// recursion — and it indexes in both directions: where a reference *lives*
283/// and where it *points*, since `${servers[0]}` parses to an [`Seg::Index`].
284pub(crate) fn lookup<'a>(root: &'a Value, path: &[Seg]) -> Option<&'a Value> {
285    let mut node = root;
286    for seg in path {
287        node = match (seg, node) {
288            (Seg::Key(k), Value::Object(map)) => map.get(k)?,
289            (Seg::Index(i), Value::Array(items)) => items.get(*i)?,
290            _ => return None,
291        };
292    }
293    Some(node)
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299    use crate::{Map, Number};
300
301    /// A witness may mix keys and indices.
302    #[test]
303    fn render_path_mixes_keys_and_indices() {
304        assert_eq!(render_path(&[]), "");
305        assert_eq!(
306            render_path(&[Seg::Key("a".into()), Seg::Key("b".into())]),
307            "a.b"
308        );
309        assert_eq!(
310            render_path(&[Seg::Key("tags".into()), Seg::Index(0)]),
311            "tags[0]"
312        );
313    }
314
315    fn seg(key: &str) -> Seg {
316        Seg::Key(key.into())
317    }
318
319    #[test]
320    fn ref_path_parses_mixed_steps() {
321        let cases: &[(&str, &[Seg])] = &[
322            ("a", &[seg("a")]),
323            ("a.b", &[seg("a"), seg("b")]),
324            ("a[0]", &[seg("a"), Seg::Index(0)]),
325            ("a.b[2].c", &[seg("a"), seg("b"), Seg::Index(2), seg("c")]),
326            ("a[0][1]", &[seg("a"), Seg::Index(0), Seg::Index(1)]),
327            // A `:` is an ordinary key character; namespaces are the caller's.
328            ("a:b[3]", &[seg("a:b"), Seg::Index(3)]),
329        ];
330        for (body, want) in cases {
331            let parsed: RefPath = body.parse().unwrap();
332            assert_eq!(parsed.segs(), *want, "{body}");
333        }
334    }
335
336    #[test]
337    fn ref_path_rejects_malformed_bodies() {
338        let cases: &[(&str, PathError)] = &[
339            (".a", empty_segment(".a")),
340            ("a..b", empty_segment("a..b")),
341            ("a[0]..b", empty_segment("a[0]..b")),
342            ("[0].a", empty_segment("[0].a")),
343            ("a[]", bad_index("a[]")),
344            ("a[x]", bad_index("a[x]")),
345            ("a[1", bad_index("a[1")),
346            ("a[-1]", bad_index("a[-1]")),
347            ("a]", bad_index("a]")),
348            (
349                "a[99999999999999999999999999]",
350                bad_index("a[99999999999999999999999999]"),
351            ),
352        ];
353        for (body, want) in cases {
354            assert_eq!(&body.parse::<RefPath>().unwrap_err(), want, "{body}");
355        }
356        assert_eq!("".parse::<RefPath>().unwrap_err(), PathError::EmptyPath);
357    }
358
359    fn empty_segment(path: &str) -> PathError {
360        PathError::EmptySegment { path: path.into() }
361    }
362
363    fn bad_index(path: &str) -> PathError {
364        PathError::BadIndex { path: path.into() }
365    }
366
367    #[test]
368    fn ref_path_displays_as_rendered_witness() {
369        let parsed: RefPath = "a.b[2].c".parse().unwrap();
370        assert_eq!(parsed.to_string(), "a.b[2].c");
371        assert_eq!("a.b".parse::<RefPath>().unwrap().to_string(), "a.b");
372    }
373
374    #[test]
375    fn from_keys_rejects_empty_path_and_empty_segments() {
376        assert_eq!(
377            RefPath::from_keys(vec![]).unwrap_err(),
378            PathError::EmptyPath
379        );
380        assert_eq!(
381            RefPath::from_keys(vec!["".into()]).unwrap_err(),
382            PathError::EmptySegment { path: "".into() }
383        );
384        assert_eq!(
385            RefPath::from_keys(vec!["a".into(), "".into()]).unwrap_err(),
386            PathError::EmptySegment { path: "a.".into() }
387        );
388        // from_keys and parsing agree on the dotted all-key grammar.
389        assert_eq!(
390            RefPath::from_keys(vec!["db".into(), "plugins".into()]).unwrap(),
391            "db.plugins".parse().unwrap()
392        );
393    }
394
395    #[test]
396    fn try_into_keys_passes_all_key_paths_whole() {
397        let parsed: RefPath = "db.plugins".parse().unwrap();
398        assert_eq!(parsed.try_into_keys().unwrap(), ["db", "plugins"]);
399    }
400
401    #[test]
402    fn try_into_keys_rejects_index_steps_with_the_full_path() {
403        for (indexed, want) in [("a[0]", "a[0]"), ("a.b[2].c", "a.b[2].c")] {
404            let err = indexed
405                .parse::<RefPath>()
406                .unwrap()
407                .try_into_keys()
408                .unwrap_err();
409            assert_eq!(err, PathError::IndexInKeyPath { path: want.into() });
410        }
411        let err = "a[0]"
412            .parse::<RefPath>()
413            .unwrap()
414            .try_into_keys()
415            .unwrap_err();
416        assert_eq!(
417            err.to_string(),
418            "`a[0]` contains an array index; merge paths take keys only"
419        );
420    }
421
422    /// A `=` has no special meaning in a bare path: just part of a (weird)
423    /// key rather than a `MissingEquals`-shaped hole.
424    #[test]
425    fn equals_is_an_ordinary_key_character() {
426        let parsed: RefPath = "a=b".parse().unwrap();
427        assert_eq!(parsed.try_into_keys().unwrap(), ["a=b"]);
428    }
429
430    fn doc() -> Value {
431        let mut inner = Map::new();
432        inner.insert("host".into(), Value::String("h".into()));
433        let mut root = Map::new();
434        root.insert("db".into(), Value::Object(inner));
435        root.insert(
436            "tags".into(),
437            Value::Array(vec![Value::Number(Number::I64(1))]),
438        );
439        Value::Object(root)
440    }
441
442    #[test]
443    fn lookup_walks_objects_and_arrays() {
444        let doc = doc();
445        assert_eq!(lookup(&doc, &[]), Some(&doc));
446        assert_eq!(
447            lookup(&doc, &[Seg::Key("db".into()), Seg::Key("host".into())]),
448            Some(&Value::String("h".into()))
449        );
450        assert_eq!(
451            lookup(&doc, &[Seg::Key("tags".into()), Seg::Index(0)]),
452            Some(&Value::Number(Number::I64(1)))
453        );
454    }
455
456    /// A missing key and a segment of the wrong shape are both simply absent —
457    /// the caller reports one unresolved reference either way.
458    #[test]
459    fn lookup_misses_are_none() {
460        let doc = doc();
461        assert_eq!(lookup(&doc, &[Seg::Key("nope".into())]), None);
462        assert_eq!(lookup(&doc, &[Seg::Key("db".into()), Seg::Index(0)]), None);
463        assert_eq!(
464            lookup(&doc, &[Seg::Key("tags".into()), Seg::Index(9)]),
465            None
466        );
467    }
468}