Skip to main content

fig_schema/
path.rs

1//! An owned fig path and patterns over it.
2//!
3//! A concrete [`Seg`] path addresses one node in a [`fig::Value`] tree (a
4//! mapping key or a sequence index); a [`PathPat`] is the same vocabulary
5//! generalized to also reach *every* item of a sequence, *every* entry of a
6//! mapping, or an entire subtree, so one rule can govern each element of a list
7//! field or everything nested under a key.
8
9/// One step of a fig path: a mapping key or a sequence index. Owned (unlike
10/// `fig::Segment<'a>`, which borrows), so a path can outlive a single FFI call.
11#[derive(Clone, Debug, PartialEq, Eq, Hash)]
12pub enum Seg {
13    Key(String),
14    Index(usize),
15}
16
17/// A path pattern. Unlike a concrete [`Seg`] path it can reach every element of
18/// a sequence ([`SegPat::EachItem`]), every entry of a mapping
19/// ([`SegPat::AnyKey`]), or a whole subtree ([`SegPat::AnyDepth`]), so a rule
20/// can constrain *each item* of a list field (`tags:`, `audience:`) or
21/// everything beneath a key.
22#[derive(Debug, Clone, PartialEq, Eq, Hash)]
23pub struct PathPat(pub Vec<SegPat>);
24
25/// One step of a [`PathPat`].
26#[derive(Debug, Clone, PartialEq, Eq, Hash)]
27pub enum SegPat {
28    /// An exact mapping key.
29    Key(String),
30    /// Any mapping key at this depth.
31    AnyKey,
32    /// An exact sequence index.
33    Index(usize),
34    /// Any sequence item at this depth.
35    EachItem,
36    /// Zero or more segments of any kind — the `**` of this vocabulary. Lets a
37    /// rule govern a whole subtree (`meta` and everything under it) or a key at
38    /// an unknown depth.
39    AnyDepth,
40}
41
42impl PathPat {
43    /// A single top-level key — the common case (`audience`, `title`).
44    pub fn key(name: impl Into<String>) -> Self {
45        PathPat(vec![SegPat::Key(name.into())])
46    }
47
48    /// A top-level list field whose *each item* the rule governs
49    /// (`audience:` as a sequence).
50    pub fn each_item_of(name: impl Into<String>) -> Self {
51        PathPat(vec![SegPat::Key(name.into()), SegPat::EachItem])
52    }
53
54    /// A top-level key and everything nested beneath it, the key itself
55    /// included (`meta`, `meta.author`, `meta.tags.0`).
56    pub fn subtree_of(name: impl Into<String>) -> Self {
57        PathPat(vec![SegPat::Key(name.into()), SegPat::AnyDepth])
58    }
59
60    /// Whether this pattern matches the concrete fig `path`. Without an
61    /// [`SegPat::AnyDepth`] this is a segment-wise match of equal lengths; with
62    /// one, the pattern may span any number of segments there.
63    pub fn matches(&self, path: &[Seg]) -> bool {
64        matches_from(&self.0, path)
65    }
66}
67
68/// Match `pats` against `path`, allowing [`SegPat::AnyDepth`] to consume any
69/// number of segments. Paths are a handful of segments deep, so the
70/// backtracking here is never hot.
71fn matches_from(pats: &[SegPat], path: &[Seg]) -> bool {
72    let Some((pat, rest)) = pats.split_first() else {
73        return path.is_empty();
74    };
75    if let SegPat::AnyDepth = pat {
76        // Try consuming 0, 1, … segments here and matching the tail after each.
77        return (0..=path.len()).any(|taken| matches_from(rest, &path[taken..]));
78    }
79    match path.split_first() {
80        Some((seg, tail)) if seg_matches(pat, seg) => matches_from(rest, tail),
81        _ => false,
82    }
83}
84
85/// Whether one pattern segment accepts one concrete segment.
86fn seg_matches(pat: &SegPat, seg: &Seg) -> bool {
87    match (pat, seg) {
88        (SegPat::Key(k), Seg::Key(s)) => k == s,
89        (SegPat::AnyKey, Seg::Key(_)) => true,
90        (SegPat::Index(i), Seg::Index(j)) => i == j,
91        (SegPat::EachItem, Seg::Index(_)) => true,
92        // Handled by `matches_from`; unreachable here.
93        (SegPat::AnyDepth, _) => true,
94        _ => false,
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    fn key(k: &str) -> Seg {
103        Seg::Key(k.into())
104    }
105
106    #[test]
107    fn path_pattern_matches_keys_and_each_item() {
108        let pat = PathPat::each_item_of("audience");
109        assert!(pat.matches(&[key("audience"), Seg::Index(0)]));
110        assert!(pat.matches(&[key("audience"), Seg::Index(3)]));
111        assert!(!pat.matches(&[key("audience")]));
112        assert!(!pat.matches(&[key("tags"), Seg::Index(0)]));
113    }
114
115    #[test]
116    fn subtree_matches_the_key_itself_and_everything_under_it() {
117        let pat = PathPat::subtree_of("meta");
118        assert!(pat.matches(&[key("meta")]));
119        assert!(pat.matches(&[key("meta"), key("author")]));
120        assert!(pat.matches(&[key("meta"), key("tags"), Seg::Index(2)]));
121        assert!(!pat.matches(&[key("other")]));
122        assert!(!pat.matches(&[]));
123    }
124
125    #[test]
126    fn any_depth_matches_a_key_at_an_unknown_depth() {
127        // `**.title` — a `title` key anywhere, including at the root.
128        let pat = PathPat(vec![SegPat::AnyDepth, SegPat::Key("title".into())]);
129        assert!(pat.matches(&[key("title")]));
130        assert!(pat.matches(&[key("meta"), key("title")]));
131        assert!(pat.matches(&[key("a"), Seg::Index(0), key("title")]));
132        assert!(!pat.matches(&[key("title"), key("sub")]));
133    }
134
135    #[test]
136    fn any_depth_between_two_fixed_segments() {
137        let pat = PathPat(vec![
138            SegPat::Key("a".into()),
139            SegPat::AnyDepth,
140            SegPat::Key("z".into()),
141        ]);
142        assert!(pat.matches(&[key("a"), key("z")]));
143        assert!(pat.matches(&[key("a"), key("m"), key("z")]));
144        assert!(pat.matches(&[key("a"), key("m"), Seg::Index(1), key("z")]));
145        assert!(!pat.matches(&[key("a"), key("m")]));
146    }
147
148    #[test]
149    fn a_pattern_without_any_depth_still_requires_an_exact_length() {
150        let pat = PathPat::key("meta");
151        assert!(pat.matches(&[key("meta")]));
152        assert!(!pat.matches(&[key("meta"), key("author")]));
153    }
154
155    #[test]
156    fn any_key_does_not_match_an_index() {
157        let pat = PathPat(vec![SegPat::AnyKey]);
158        assert!(pat.matches(&[key("whatever")]));
159        assert!(!pat.matches(&[Seg::Index(0)]));
160    }
161}