Skip to main content

ctl_core/input/
field_path.rs

1//! The steps from a document's root to a field, and the place where a field is
2//! written.
3
4use std::fmt;
5
6/// One step down a [`FieldPath`]: a mapping key or a sequence index.
7///
8/// The set is closed, because a node of a YAML or TOML document is entered by
9/// one of these two ways and by no other.
10#[derive(Clone, Debug, PartialEq, Eq, Hash)]
11pub enum Step {
12    /// A key of a mapping or table. Any text: it may hold `.`, `*`, `/`, `[`,
13    /// or only digits.
14    Key(String),
15    /// An item of a sequence or array.
16    Index(usize),
17}
18
19/// The steps from a document's root down to a field.
20///
21/// Build one with [`FieldPath::root`], [`FieldPath::key`], and
22/// [`FieldPath::index`], or, with the `validate` feature, from garde's path
23/// with `FieldPath::from`. [`Input::place`](super::Input::place) turns it into
24/// a line and column.
25///
26/// A key is kept as written, so
27/// `FieldPath::root().key("files").key("docs/**/*.md")` names one key of
28/// `files`, not four. `Display` joins the steps the way garde prints a field,
29/// `files.docs/**/*.md`, which cannot tell a key that holds a dot from nesting.
30/// It is for people, and nothing reads it back.
31#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
32pub struct FieldPath(Vec<Step>);
33
34impl FieldPath {
35    /// The document's root.
36    #[must_use]
37    pub fn root() -> Self {
38        Self::default()
39    }
40
41    /// This path extended by a mapping or table key.
42    #[must_use]
43    pub fn key(mut self, key: impl Into<String>) -> Self {
44        self.0.push(Step::Key(key.into()));
45        self
46    }
47
48    /// This path extended by a sequence or array index.
49    #[must_use]
50    pub fn index(mut self, index: usize) -> Self {
51        self.0.push(Step::Index(index));
52        self
53    }
54
55    /// The steps from the root, in order.
56    #[must_use]
57    pub fn steps(&self) -> &[Step] {
58        &self.0
59    }
60
61    /// Whether this path's leading steps are the steps of `prefix`.
62    #[cfg(feature = "validate")]
63    pub(crate) fn starts_with(&self, prefix: &Self) -> bool {
64        self.0.starts_with(&prefix.0)
65    }
66
67    /// This path without the steps at `positions`, each an index into
68    /// [`Self::steps`].
69    #[cfg(feature = "validate")]
70    pub(crate) fn without(&self, positions: &[usize]) -> Self {
71        Self(
72            self.0
73                .iter()
74                .enumerate()
75                .filter(|(position, _)| !positions.contains(position))
76                .map(|(_, step)| step.clone())
77                .collect(),
78        )
79    }
80}
81
82/// The path as garde prints a field: keys joined by `.`, indexes in brackets,
83/// and nothing for the root.
84impl fmt::Display for FieldPath {
85    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
86        for (position, step) in self.0.iter().enumerate() {
87            match step {
88                Step::Key(key) => {
89                    if position > 0 {
90                        formatter.write_str(".")?;
91                    }
92                    formatter.write_str(key)?;
93                }
94                Step::Index(index) => write!(formatter, "[{index}]")?,
95            }
96        }
97        Ok(())
98    }
99}
100
101/// The steps of garde's path, read from its parts and never from its printed
102/// string, so a key that holds a `.` stays one step.
103///
104/// A component garde marks as an index becomes [`Step::Index`], one it marks as
105/// a key becomes [`Step::Key`], and one it marks with no key, which names a
106/// wrapper such as a one-field tuple struct, is dropped. An index component
107/// that is not a number is kept as a key.
108#[cfg(feature = "validate")]
109impl From<&garde::error::Path> for FieldPath {
110    fn from(path: &garde::error::Path) -> Self {
111        use garde::error::Kind;
112
113        let mut steps = Vec::new();
114        // garde 0.23.0 keeps a path's parts behind `__iter`, which is public
115        // and `#[doc(hidden)]`. It yields the innermost component first, as
116        // `Display` reads it with `rev`. The workspace pins garde exactly so a
117        // release cannot change it unseen, and
118        // `a_garde_path_converts_from_its_parts` fails first if one does.
119        for (kind, component) in path.__iter().rev() {
120            match kind {
121                Kind::Key => steps.push(Step::Key(component.to_string())),
122                Kind::Index => steps.push(
123                    component
124                        .parse::<usize>()
125                        .map_or_else(|_| Step::Key(component.to_string()), Step::Index),
126                ),
127                Kind::None => {}
128            }
129        }
130        Self(steps)
131    }
132}
133
134/// The 1-based line and column where a field is written.
135///
136/// The column counts characters. [`Input::place`](super::Input::place) returns
137/// one. The struct is non-exhaustive so a field such as an end position can
138/// join it.
139#[derive(Clone, Copy, Debug, PartialEq, Eq)]
140#[non_exhaustive]
141pub struct Place {
142    /// 1-based line in the whole file, frontmatter lines included.
143    pub line: usize,
144    /// 1-based column, counted in characters.
145    pub column: usize,
146}
147
148impl Place {
149    pub(crate) const fn new(line: usize, column: usize) -> Self {
150        Self { line, column }
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn a_path_prints_as_garde_prints_a_field() {
160        let path = FieldPath::root()
161            .key("files")
162            .key("docs/**/*.md")
163            .key("vocab")
164            .index(1);
165        assert_eq!(path.to_string(), "files.docs/**/*.md.vocab[1]");
166        assert_eq!(FieldPath::root().index(0).key("a").to_string(), "[0].a");
167        assert_eq!(FieldPath::root().to_string(), "");
168    }
169
170    #[cfg(feature = "validate")]
171    #[test]
172    fn a_garde_path_converts_from_its_parts() {
173        let path = garde::error::Path::new("files")
174            .join("docs/**/*.md")
175            .join("vocab")
176            .join(0_usize);
177        assert_eq!(
178            FieldPath::from(&path).steps(),
179            [
180                Step::Key("files".to_owned()),
181                Step::Key("docs/**/*.md".to_owned()),
182                Step::Key("vocab".to_owned()),
183                Step::Index(0),
184            ]
185        );
186        assert_eq!(path.to_string(), "files.docs/**/*.md.vocab[0]");
187    }
188
189    #[cfg(feature = "validate")]
190    #[test]
191    fn a_numeric_key_stays_a_key_and_a_keyless_part_is_dropped() {
192        let path = garde::error::Path::new("files")
193            .join("2")
194            .join(garde::error::NoKey::default())
195            .join("label");
196        assert_eq!(
197            FieldPath::from(&path).steps(),
198            [
199                Step::Key("files".to_owned()),
200                Step::Key("2".to_owned()),
201                Step::Key("label".to_owned()),
202            ]
203        );
204    }
205}