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
62/// The path as garde prints a field: keys joined by `.`, indexes in brackets,
63/// and nothing for the root.
64impl fmt::Display for FieldPath {
65 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
66 for (position, step) in self.0.iter().enumerate() {
67 match step {
68 Step::Key(key) => {
69 if position > 0 {
70 formatter.write_str(".")?;
71 }
72 formatter.write_str(key)?;
73 }
74 Step::Index(index) => write!(formatter, "[{index}]")?,
75 }
76 }
77 Ok(())
78 }
79}
80
81/// The steps of garde's path, read from its parts and never from its printed
82/// string, so a key that holds a `.` stays one step.
83///
84/// A component garde marks as an index becomes [`Step::Index`], one it marks as
85/// a key becomes [`Step::Key`], and one it marks with no key, which names a
86/// wrapper such as a one-field tuple struct, is dropped. An index component
87/// that is not a number is kept as a key.
88#[cfg(feature = "validate")]
89impl From<&garde::error::Path> for FieldPath {
90 fn from(path: &garde::error::Path) -> Self {
91 use garde::error::Kind;
92
93 let mut steps = Vec::new();
94 // garde 0.23.0 keeps a path's parts behind `__iter`, which is public
95 // and `#[doc(hidden)]`. It yields the innermost component first, as
96 // `Display` reads it with `rev`. The workspace pins garde exactly so a
97 // release cannot change it unseen, and
98 // `a_garde_path_converts_from_its_parts` fails first if one does.
99 for (kind, component) in path.__iter().rev() {
100 match kind {
101 Kind::Key => steps.push(Step::Key(component.to_string())),
102 Kind::Index => steps.push(
103 component
104 .parse::<usize>()
105 .map_or_else(|_| Step::Key(component.to_string()), Step::Index),
106 ),
107 Kind::None => {}
108 }
109 }
110 Self(steps)
111 }
112}
113
114/// The 1-based line and column where a field is written.
115///
116/// The column counts characters. [`Input::place`](super::Input::place) returns
117/// one. The struct is non-exhaustive so a field such as an end position can
118/// join it.
119#[derive(Clone, Copy, Debug, PartialEq, Eq)]
120#[non_exhaustive]
121pub struct Place {
122 /// 1-based line in the whole file, frontmatter lines included.
123 pub line: usize,
124 /// 1-based column, counted in characters.
125 pub column: usize,
126}
127
128impl Place {
129 pub(crate) const fn new(line: usize, column: usize) -> Self {
130 Self { line, column }
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn a_path_prints_as_garde_prints_a_field() {
140 let path = FieldPath::root()
141 .key("files")
142 .key("docs/**/*.md")
143 .key("vocab")
144 .index(1);
145 assert_eq!(path.to_string(), "files.docs/**/*.md.vocab[1]");
146 assert_eq!(FieldPath::root().index(0).key("a").to_string(), "[0].a");
147 assert_eq!(FieldPath::root().to_string(), "");
148 }
149
150 #[cfg(feature = "validate")]
151 #[test]
152 fn a_garde_path_converts_from_its_parts() {
153 let path = garde::error::Path::new("files")
154 .join("docs/**/*.md")
155 .join("vocab")
156 .join(0_usize);
157 assert_eq!(
158 FieldPath::from(&path).steps(),
159 [
160 Step::Key("files".to_owned()),
161 Step::Key("docs/**/*.md".to_owned()),
162 Step::Key("vocab".to_owned()),
163 Step::Index(0),
164 ]
165 );
166 assert_eq!(path.to_string(), "files.docs/**/*.md.vocab[0]");
167 }
168
169 #[cfg(feature = "validate")]
170 #[test]
171 fn a_numeric_key_stays_a_key_and_a_keyless_part_is_dropped() {
172 let path = garde::error::Path::new("files")
173 .join("2")
174 .join(garde::error::NoKey::default())
175 .join("label");
176 assert_eq!(
177 FieldPath::from(&path).steps(),
178 [
179 Step::Key("files".to_owned()),
180 Step::Key("2".to_owned()),
181 Step::Key("label".to_owned()),
182 ]
183 );
184 }
185}