Skip to main content

ephemeris_documents/
path.rs

1//! Locations inside a structured value, for error messages.
2
3use std::fmt;
4use std::iter::Peekable;
5use std::str::CharIndices;
6
7use crate::error::{OwnershipError, PathSyntaxError};
8
9/// One step of a [`KeyPath`].
10#[derive(Debug, Clone, PartialEq, Eq, Hash)]
11pub enum Segment {
12    /// An object key.
13    Key(String),
14    /// An array index.
15    Index(usize),
16}
17
18/// The location of a value inside a document, such as `jobs.build.steps[2]`.
19///
20/// The root prints as `.`. Keys that are not made only of ASCII letters,
21/// digits, `_` and `-` are written in double quotes.
22#[derive(Debug, Clone, PartialEq, Eq, Hash)]
23pub struct KeyPath(Vec<Segment>);
24
25impl KeyPath {
26    /// The path of the document root.
27    #[must_use]
28    pub const fn root() -> Self {
29        Self(Vec::new())
30    }
31
32    /// Whether this is the root path.
33    #[must_use]
34    pub const fn is_root(&self) -> bool {
35        self.0.is_empty()
36    }
37
38    /// The segments from the root down.
39    #[must_use]
40    pub fn segments(&self) -> &[Segment] {
41        &self.0
42    }
43
44    /// This path with `segment` put in front of it.
45    #[must_use]
46    pub fn under(mut self, segment: Segment) -> Self {
47        self.0.insert(0, segment);
48        self
49    }
50}
51
52impl FromIterator<Segment> for KeyPath {
53    fn from_iter<I: IntoIterator<Item = Segment>>(iter: I) -> Self {
54        Self(iter.into_iter().collect())
55    }
56}
57
58impl From<&[String]> for KeyPath {
59    fn from(keys: &[String]) -> Self {
60        keys.iter().map(|key| Segment::Key(key.clone())).collect()
61    }
62}
63
64impl fmt::Display for Segment {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        match self {
67            Self::Index(index) => write!(f, "[{index}]"),
68            Self::Key(key) => KeyText(key).fmt(f),
69        }
70    }
71}
72
73/// The path grammar for one key: bare, or double quoted with `\"` and `\\`
74/// as the only escapes.
75struct KeyText<'a>(&'a str);
76
77impl KeyText<'_> {
78    const fn is_bare_char(c: char) -> bool {
79        c.is_ascii_alphanumeric() || c == '_' || c == '-'
80    }
81
82    const fn is_escaped_char(c: char) -> bool {
83        matches!(c, '\\' | '"')
84    }
85
86    fn is_bare(&self) -> bool {
87        !self.0.is_empty() && self.0.chars().all(Self::is_bare_char)
88    }
89}
90
91impl fmt::Display for KeyText<'_> {
92    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93        if self.is_bare() {
94            return f.write_str(self.0);
95        }
96        f.write_str("\"")?;
97        for c in self.0.chars() {
98            if Self::is_escaped_char(c) {
99                f.write_str("\\")?;
100            }
101            write!(f, "{c}")?;
102        }
103        f.write_str("\"")
104    }
105}
106
107/// A tokenizer for dot separated keys that tracks byte offsets.
108pub(crate) struct PathTokens<'a> {
109    chars: Peekable<CharIndices<'a>>,
110    end: usize,
111}
112
113impl<'a> PathTokens<'a> {
114    /// Split `text` into keys.
115    ///
116    /// # Errors
117    ///
118    /// Returns [`OwnershipError::PathEmpty`] for empty text and
119    /// [`OwnershipError::PathSyntax`] for text that breaks the grammar.
120    pub(crate) fn parse(text: &'a str) -> Result<Vec<String>, OwnershipError> {
121        Self::new(text).keys()
122    }
123
124    fn new(text: &'a str) -> Self {
125        Self {
126            chars: text.char_indices().peekable(),
127            end: text.len(),
128        }
129    }
130
131    fn offset(&mut self) -> usize {
132        self.chars.peek().map_or(self.end, |&(offset, _)| offset)
133    }
134
135    fn error(&mut self, kind: PathSyntaxError) -> OwnershipError {
136        OwnershipError::PathSyntax {
137            offset: self.offset(),
138            kind,
139        }
140    }
141
142    fn eat(&mut self, expected: char) -> bool {
143        self.chars.next_if(|&(_, c)| c == expected).is_some()
144    }
145
146    fn keys(mut self) -> Result<Vec<String>, OwnershipError> {
147        if self.chars.peek().is_none() {
148            return Err(OwnershipError::PathEmpty);
149        }
150        let mut keys = vec![self.key()?];
151        while self.eat('.') {
152            keys.push(self.key()?);
153        }
154        match self.chars.peek() {
155            None => Ok(keys),
156            Some(_) => Err(self.error(PathSyntaxError::UnexpectedCharacter)),
157        }
158    }
159
160    fn key(&mut self) -> Result<String, OwnershipError> {
161        if self.chars.peek().is_some_and(|&(_, c)| c == '"') {
162            self.quoted()
163        } else {
164            self.bare()
165        }
166    }
167
168    fn bare(&mut self) -> Result<String, OwnershipError> {
169        let mut key = String::new();
170        while let Some((_, c)) = self.chars.next_if(|&(_, c)| KeyText::is_bare_char(c)) {
171            key.push(c);
172        }
173        if key.is_empty() {
174            let kind = match self.chars.peek() {
175                None | Some(&(_, '.')) => PathSyntaxError::EmptyKey,
176                Some(_) => PathSyntaxError::UnexpectedCharacter,
177            };
178            return Err(self.error(kind));
179        }
180        Ok(key)
181    }
182
183    fn quoted(&mut self) -> Result<String, OwnershipError> {
184        let start = self.offset();
185        self.chars.next();
186        let mut key = String::new();
187        loop {
188            let unterminated = OwnershipError::PathSyntax {
189                offset: start,
190                kind: PathSyntaxError::UnterminatedQuote,
191            };
192            match self.chars.next().ok_or_else(|| unterminated.clone())? {
193                (_, '"') => return Ok(key),
194                (escape, '\\') => match self.chars.next().ok_or(unterminated)? {
195                    (_, c) if KeyText::is_escaped_char(c) => key.push(c),
196                    _ => {
197                        return Err(OwnershipError::PathSyntax {
198                            offset: escape,
199                            kind: PathSyntaxError::InvalidEscape,
200                        });
201                    }
202                },
203                (_, c) => key.push(c),
204            }
205        }
206    }
207}
208
209impl fmt::Display for KeyPath {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        if self.is_root() {
212            return f.write_str(".");
213        }
214        for (position, segment) in self.0.iter().enumerate() {
215            if position > 0 && matches!(segment, Segment::Key(_)) {
216                f.write_str(".")?;
217            }
218            write!(f, "{segment}")?;
219        }
220        Ok(())
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::{KeyPath, Segment};
227
228    fn key(name: &str) -> Segment {
229        Segment::Key(name.to_owned())
230    }
231
232    fn path(segments: impl IntoIterator<Item = Segment>) -> KeyPath {
233        segments.into_iter().collect()
234    }
235
236    #[test]
237    fn root_prints_as_a_dot() {
238        assert!(KeyPath::root().is_root());
239        assert_eq!(KeyPath::root().to_string(), ".");
240        assert_eq!(KeyPath::root().segments(), []);
241    }
242
243    #[test]
244    fn keys_and_indexes_are_joined() {
245        let nested = path([key("a"), key("b"), Segment::Index(2), key("c")]);
246        assert_eq!(nested.to_string(), "a.b[2].c");
247        let lists = path([key("list"), Segment::Index(0), Segment::Index(1)]);
248        assert_eq!(lists.to_string(), "list[0][1]");
249        assert_eq!(path([Segment::Index(3), key("a")]).to_string(), "[3].a");
250    }
251
252    #[test]
253    fn unusual_keys_are_quoted() {
254        assert_eq!(path([key("a.b"), key("c")]).to_string(), "\"a.b\".c");
255        assert_eq!(path([key("")]).to_string(), "\"\"");
256        assert_eq!(path([key("x y")]).to_string(), "\"x y\"");
257        assert_eq!(path([key("a\"b\\c")]).to_string(), "\"a\\\"b\\\\c\"");
258        assert_eq!(path([key("a_b-C9")]).to_string(), "a_b-C9");
259    }
260
261    #[test]
262    fn under_puts_the_segment_in_front() {
263        let built = KeyPath::root()
264            .under(key("c"))
265            .under(Segment::Index(1))
266            .under(key("b"));
267        assert_eq!(built.to_string(), "b[1].c");
268        assert!(!built.is_root());
269        assert_eq!(built.segments().len(), 3);
270    }
271
272    #[test]
273    fn keys_convert_from_a_slice() {
274        let keys = ["a".to_owned(), "b".to_owned()];
275        assert_eq!(KeyPath::from(&keys[..]), path([key("a"), key("b")]));
276        assert!(KeyPath::from(&[][..]).is_root());
277    }
278}