Skip to main content

prov_graph/
field.rs

1//! Field paths — how a `fields` declaration names what it governs, and how a
2//! finding or a repair names the one value it means.
3//!
4//! A declaration names a top-level key (`tags`), a key inside a mapping
5//! (`generated.how`), or a key inside **every item of a list**
6//! (`sources[].resource`, `confirmed[].by`). The three steps compose, and a
7//! [`FieldPath`] is the parsed form: one [`Step`] per key or `[]`. Walking a
8//! path over a document's metadata ([`values_at`]) yields every value it
9//! reaches, each with the concrete [`Address`] it was found at — the same path
10//! with every `[]` filled in (`sources[2].resource`) — which is what a
11//! finding names and what an editor edits. The path grammar is its own
12//! address grammar: a numeric step is a list index, so an address parses as
13//! a path with no `[]` left in it, and displays as one.
14//!
15//! A dot is always a separator, as it is for `prov get`. A bracket group is
16//! `[]` (every item) or `[n]` (one item); anything else in brackets is part
17//! of the key, so a field whose name happens to carry brackets is still
18//! addressable.
19
20use std::fmt;
21
22/// One step of a [`FieldPath`].
23#[derive(Debug, Clone, PartialEq, Eq, Hash)]
24pub enum Step {
25    /// A mapping key.
26    Key(String),
27    /// Every item of a list — the `[]` a declaration writes.
28    Each,
29    /// One item of a list — the `[n]` a concrete address writes.
30    At(usize),
31}
32
33/// A parsed field path: `tags`, `generated.how`, `sources[].resource`.
34#[derive(Debug, Clone, PartialEq, Eq, Hash)]
35pub struct FieldPath {
36    steps: Vec<Step>,
37}
38
39impl FieldPath {
40    /// Parse a path as a declaration or a finding writes it. Never fails: a
41    /// malformed bracket group is read as part of the key it follows.
42    pub fn parse(path: &str) -> Self {
43        let mut steps = Vec::new();
44        for segment in path.split('.') {
45            let (key, brackets) = split_brackets(segment);
46            steps.push(Step::Key(key.to_string()));
47            for group in brackets {
48                steps.push(group);
49            }
50        }
51        Self { steps }
52    }
53
54    /// The steps, in order.
55    pub fn steps(&self) -> &[Step] {
56        &self.steps
57    }
58
59    /// Whether every list step names one item — the form an [`Address`] takes.
60    pub fn is_concrete(&self) -> bool {
61        !self.steps.iter().any(|s| matches!(s, Step::Each))
62    }
63
64    /// Whether the path reaches into a list at all. A path that does not is
65    /// the shape a starting value can be written at.
66    pub fn enters_list(&self) -> bool {
67        self.steps
68            .iter()
69            .any(|s| matches!(s, Step::Each | Step::At(_)))
70    }
71
72    /// The top-level key the path starts at.
73    pub fn head(&self) -> &str {
74        match &self.steps[0] {
75            Step::Key(k) => k,
76            // `parse` always begins with a key.
77            _ => unreachable!("a field path starts with a key"),
78        }
79    }
80}
81
82impl fmt::Display for FieldPath {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        write_steps(f, &self.steps)
85    }
86}
87
88/// A concrete address into a document's metadata — a [`FieldPath`] with every
89/// `[]` filled in. `sources[2].resource`; `contents[3]`; `title`.
90#[derive(Debug, Clone, PartialEq, Eq, Hash)]
91pub struct Address {
92    steps: Vec<Step>,
93}
94
95impl Address {
96    /// Parse a concrete address. `None` when the text still has a `[]` in it.
97    pub fn parse(text: &str) -> Option<Self> {
98        let path = FieldPath::parse(text);
99        path.is_concrete().then_some(Self { steps: path.steps })
100    }
101
102    /// The address of a top-level key.
103    pub fn key(name: &str) -> Self {
104        Self {
105            steps: vec![Step::Key(name.to_string())],
106        }
107    }
108
109    /// This address with a list index appended.
110    pub fn item(mut self, index: usize) -> Self {
111        self.steps.push(Step::At(index));
112        self
113    }
114
115    /// The steps, in order.
116    pub fn steps(&self) -> &[Step] {
117        &self.steps
118    }
119
120    /// The top-level key the address starts at.
121    pub fn head(&self) -> &str {
122        match &self.steps[0] {
123            Step::Key(k) => k,
124            _ => unreachable!("an address starts with a key"),
125        }
126    }
127
128    /// The address as an editor path — one [`fig::Segment`] per step.
129    pub fn segments(&self) -> Vec<fig::Segment<'_>> {
130        self.steps
131            .iter()
132            .map(|s| match s {
133                Step::Key(k) => fig::Segment::Key(k),
134                Step::At(i) => fig::Segment::Index(*i),
135                Step::Each => unreachable!("an address has no `[]` step"),
136            })
137            .collect()
138    }
139
140    /// The address of the list this one is an item of, and the item's
141    /// position — `Some` only when the last step is an index. What a removal
142    /// needs: an item is taken out of its list, not deleted at its own path.
143    pub fn as_item(&self) -> Option<(Address, usize)> {
144        match self.steps.last() {
145            Some(Step::At(i)) => Some((
146                Address {
147                    steps: self.steps[..self.steps.len() - 1].to_vec(),
148                },
149                *i,
150            )),
151            _ => None,
152        }
153    }
154}
155
156impl fmt::Display for Address {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        write_steps(f, &self.steps)
159    }
160}
161
162fn write_steps(f: &mut fmt::Formatter<'_>, steps: &[Step]) -> fmt::Result {
163    for (i, step) in steps.iter().enumerate() {
164        match step {
165            Step::Key(k) => {
166                if i > 0 {
167                    f.write_str(".")?;
168                }
169                f.write_str(k)?;
170            }
171            Step::Each => f.write_str("[]")?,
172            Step::At(n) => write!(f, "[{n}]")?,
173        }
174    }
175    Ok(())
176}
177
178/// Split a dotted segment into its key and the bracket groups that follow it.
179/// The groups must run to the end of the segment and each must be `[]` or
180/// `[digits]`; otherwise the whole segment is the key.
181fn split_brackets(segment: &str) -> (&str, Vec<Step>) {
182    let Some(open) = segment.find('[') else {
183        return (segment, Vec::new());
184    };
185    let (key, rest) = segment.split_at(open);
186    let mut groups = Vec::new();
187    let mut rest = rest;
188    while !rest.is_empty() {
189        let Some(inner) = rest.strip_prefix('[') else {
190            return (segment, Vec::new());
191        };
192        let Some(close) = inner.find(']') else {
193            return (segment, Vec::new());
194        };
195        let (body, after) = inner.split_at(close);
196        let step = if body.is_empty() {
197            Step::Each
198        } else if let Ok(n) = body.parse::<usize>() {
199            Step::At(n)
200        } else {
201            return (segment, Vec::new());
202        };
203        groups.push(step);
204        rest = &after[1..];
205    }
206    if key.is_empty() {
207        return (segment, Vec::new());
208    }
209    (key, groups)
210}
211
212/// A metadata tree a [`FieldPath`] can be walked over. Both value trees prov
213/// reads — its own [`Value`](crate::meta::Value) and `fig`'s — are one.
214pub trait Navigate: Sized {
215    /// The value under `key`, if this is a mapping holding it.
216    fn child(&self, key: &str) -> Option<&Self>;
217    /// The items, if this is a list.
218    fn items(&self) -> Option<&[Self]>;
219    /// The text, if this is a string.
220    fn text(&self) -> Option<&str>;
221}
222
223impl Navigate for crate::meta::Value {
224    fn child(&self, key: &str) -> Option<&Self> {
225        self.get(key)
226    }
227    fn items(&self) -> Option<&[Self]> {
228        self.as_sequence()
229    }
230    fn text(&self) -> Option<&str> {
231        self.as_str()
232    }
233}
234
235impl Navigate for fig::Value {
236    fn child(&self, key: &str) -> Option<&Self> {
237        self.get(key)
238    }
239    fn items(&self) -> Option<&[Self]> {
240        self.as_seq()
241    }
242    fn text(&self) -> Option<&str> {
243        self.as_str()
244    }
245}
246
247/// Every value `path` reaches in `root`, each with the concrete address it
248/// was found at. A `[]` step fans out over the list's items; a key step on
249/// something that is not a mapping, or an index past the end, reaches
250/// nothing. Document order.
251pub fn values_at<'v, V: Navigate>(root: &'v V, path: &FieldPath) -> Vec<(Address, &'v V)> {
252    let mut found = vec![(Address { steps: Vec::new() }, root)];
253    for step in path.steps() {
254        let mut next = Vec::new();
255        for (address, value) in found {
256            match step {
257                Step::Key(key) => {
258                    if let Some(child) = value.child(key) {
259                        let mut address = address;
260                        address.steps.push(step.clone());
261                        next.push((address, child));
262                    }
263                }
264                Step::Each => {
265                    if let Some(items) = value.items() {
266                        for (i, item) in items.iter().enumerate() {
267                            let mut address = address.clone();
268                            address.steps.push(Step::At(i));
269                            next.push((address, item));
270                        }
271                    }
272                }
273                Step::At(i) => {
274                    if let Some(item) = value.items().and_then(|items| items.get(*i)) {
275                        let mut address = address;
276                        address.steps.push(step.clone());
277                        next.push((address, item));
278                    }
279                }
280            }
281        }
282        found = next;
283    }
284    found
285}
286
287/// Every string `path` governs in `root`, with its address: a value that is a
288/// string is one, addressed where the path landed; a value that is a list is
289/// each of its string items, addressed by position, with the items that are
290/// not strings holding their place in the count. This is how a relation
291/// field has always been read (`tags: [a, b]` is two values), applied at the
292/// end of whatever path reached it.
293pub fn strings_at<V: Navigate>(root: &V, path: &FieldPath) -> Vec<(Address, String)> {
294    let mut out = Vec::new();
295    for (address, value) in values_at(root, path) {
296        if let Some(s) = value.text() {
297            out.push((address, s.to_string()));
298        } else if let Some(items) = value.items() {
299            for (i, item) in items.iter().enumerate() {
300                if let Some(s) = item.text() {
301                    out.push((address.clone().item(i), s.to_string()));
302                }
303            }
304        }
305    }
306    out
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312    use crate::meta::{Mapping, Value};
313
314    fn doc() -> Value {
315        let mut a = Mapping::new();
316        a.insert("resource".into(), Value::String("a.md".into()));
317        a.insert("title".into(), Value::String("A".into()));
318        let mut b = Mapping::new();
319        b.insert("resource".into(), Value::String("b.md".into()));
320        let mut generated = Mapping::new();
321        generated.insert("how".into(), Value::String("drafted".into()));
322        let mut root = Mapping::new();
323        root.insert("title".into(), Value::String("x".into()));
324        root.insert(
325            "tags".into(),
326            Value::Sequence(vec![
327                Value::String("t1".into()),
328                Value::Int(3),
329                Value::String("t2".into()),
330            ]),
331        );
332        root.insert("generated".into(), Value::Mapping(generated));
333        root.insert(
334            "sources".into(),
335            Value::Sequence(vec![Value::Mapping(a), Value::Mapping(b), Value::Null]),
336        );
337        Value::Mapping(root)
338    }
339
340    #[test]
341    fn a_path_round_trips_through_display() {
342        for text in [
343            "tags",
344            "generated.how",
345            "sources[].resource",
346            "sources[2].resource[1]",
347        ] {
348            assert_eq!(FieldPath::parse(text).to_string(), text);
349        }
350        assert!(!FieldPath::parse("sources[].resource").is_concrete());
351        assert!(FieldPath::parse("sources[2].resource").is_concrete());
352        assert!(!FieldPath::parse("generated.how").enters_list());
353        assert!(FieldPath::parse("sources[].resource").enters_list());
354    }
355
356    #[test]
357    fn a_malformed_bracket_group_is_part_of_the_key() {
358        let path = FieldPath::parse("odd[x].y");
359        assert_eq!(
360            path.steps(),
361            &[Step::Key("odd[x]".into()), Step::Key("y".into())]
362        );
363        assert_eq!(
364            FieldPath::parse("open[").steps(),
365            &[Step::Key("open[".into())]
366        );
367        assert_eq!(FieldPath::parse("[]").steps(), &[Step::Key("[]".into())]);
368    }
369
370    #[test]
371    fn a_key_path_reaches_one_value_and_a_list_path_reaches_each_item() {
372        let doc = doc();
373        let hits = values_at(&doc, &FieldPath::parse("generated.how"));
374        assert_eq!(hits.len(), 1);
375        assert_eq!(hits[0].0.to_string(), "generated.how");
376        assert_eq!(hits[0].1.as_str(), Some("drafted"));
377
378        let hits = strings_at(&doc, &FieldPath::parse("sources[].resource"));
379        assert_eq!(
380            hits.iter()
381                .map(|(a, s)| (a.to_string(), s.clone()))
382                .collect::<Vec<_>>(),
383            vec![
384                ("sources[0].resource".to_string(), "a.md".to_string()),
385                ("sources[1].resource".to_string(), "b.md".to_string()),
386            ]
387        );
388        // The null third item holds its place in the count and yields nothing.
389        let hits = strings_at(&doc, &FieldPath::parse("sources[2].resource"));
390        assert!(hits.is_empty());
391    }
392
393    #[test]
394    fn a_list_leaf_yields_each_string_item_by_position() {
395        let doc = doc();
396        let hits = strings_at(&doc, &FieldPath::parse("tags"));
397        assert_eq!(
398            hits.iter()
399                .map(|(a, s)| (a.to_string(), s.clone()))
400                .collect::<Vec<_>>(),
401            vec![
402                ("tags[0]".to_string(), "t1".to_string()),
403                ("tags[2]".to_string(), "t2".to_string()),
404            ]
405        );
406        let hits = strings_at(&doc, &FieldPath::parse("title"));
407        assert_eq!(hits[0].0.to_string(), "title");
408    }
409
410    #[test]
411    fn a_path_through_the_wrong_shape_reaches_nothing() {
412        let doc = doc();
413        assert!(values_at(&doc, &FieldPath::parse("title.how")).is_empty());
414        assert!(values_at(&doc, &FieldPath::parse("title[]")).is_empty());
415        assert!(values_at(&doc, &FieldPath::parse("sources[9].resource")).is_empty());
416        assert!(values_at(&doc, &FieldPath::parse("missing")).is_empty());
417    }
418
419    #[test]
420    fn an_address_knows_its_list_and_its_editor_segments() {
421        let address = Address::parse("sources[2].resource").unwrap();
422        assert_eq!(address.head(), "sources");
423        assert!(address.as_item().is_none());
424        let (list, i) = Address::parse("contents[3]").unwrap().as_item().unwrap();
425        assert_eq!(list.to_string(), "contents");
426        assert_eq!(i, 3);
427        assert!(Address::parse("sources[].resource").is_none());
428        let segments = address.segments();
429        assert!(matches!(segments[0], fig::Segment::Key("sources")));
430        assert!(matches!(segments[1], fig::Segment::Index(2)));
431        assert!(matches!(segments[2], fig::Segment::Key("resource")));
432    }
433}