Skip to main content

svugy_model/
path.rs

1//! Identify an element by its full path, with segments escaped so any segment content round-trips.
2
3use std::fmt::{self, Write};
4use std::iter::Peekable;
5use std::str::{CharIndices, FromStr};
6
7use serde::de::{self, Visitor};
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use thiserror::Error;
10
11/// Separate a path's segments.
12const SEPARATOR: char = '.';
13/// Open and close a quoted segment.
14const QUOTE: char = '"';
15/// Escape the next character of a quoted segment.
16const ESCAPE: char = '\\';
17/// List the characters a quoted segment escapes and a bare segment cannot hold.
18const ESCAPED_CHARACTERS: [char; 2] = [QUOTE, ESCAPE];
19
20/// Identify an element by its path from the top level down; the empty path is the root level, never an element.
21#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
22pub struct ElementPath {
23    segments: Vec<String>,
24}
25
26impl ElementPath {
27    /// Return the root path, the empty path of the top-level view.
28    pub fn root() -> Self {
29        Self {
30            segments: Vec::new(),
31        }
32    }
33
34    /// Build a path from its segments, outermost first.
35    pub fn from_segments<I, S>(segments: I) -> Self
36    where
37        I: IntoIterator<Item = S>,
38        S: Into<String>,
39    {
40        let segments = segments.into_iter().map(Into::into).collect();
41        Self { segments }
42    }
43
44    /// Return the path of the child `segment` one level below this path.
45    pub fn child(&self, segment: impl Into<String>) -> Self {
46        let mut segments = self.segments.clone();
47        let segment = segment.into();
48        segments.push(segment);
49        Self { segments }
50    }
51
52    /// Return the level this path is drawn in: its parent, or the root for a top-level element and for the root itself.
53    pub fn enclosing_level(&self) -> Self {
54        let parent = self.parent();
55        parent.unwrap_or_else(Self::root)
56    }
57
58    /// Return the path one level up, or `None` for the root.
59    pub fn parent(&self) -> Option<Self> {
60        let (_last_segment, parent_segment_slice) = self.segments.split_last()?;
61        let parent_segments = parent_segment_slice.iter().cloned();
62        let parent = Self::from_segments(parent_segments);
63        Some(parent)
64    }
65
66    /// Tell whether `self` is a proper prefix of `other`.
67    pub fn is_ancestor_of(&self, other: &Self) -> bool {
68        let is_shorter = self.segments.len() < other.segments.len();
69        let is_prefix = other.segments.starts_with(&self.segments);
70        is_shorter && is_prefix
71    }
72
73    /// Tell whether `other` is `self`, its ancestor or its descendant.
74    pub fn shares_lineage_with(&self, other: &Self) -> bool {
75        let is_other_ancestor_or_self = self.segments.starts_with(&other.segments);
76        let is_other_descendant_or_self = other.segments.starts_with(&self.segments);
77        is_other_ancestor_or_self || is_other_descendant_or_self
78    }
79
80    /// Return the longest common prefix of both paths.
81    pub fn lowest_common_ancestor(&self, other: &Self) -> Self {
82        let shared_segments = self
83            .segments
84            .iter()
85            .zip(&other.segments)
86            .take_while(|(own, others)| own == others)
87            .map(|(own, _others)| own.clone());
88        Self::from_segments(shared_segments)
89    }
90
91    /// Yield every proper non-root prefix, outermost first: the elements that must exist for this one to exist.
92    pub fn ancestors(&self) -> impl Iterator<Item = Self> + '_ {
93        let proper_prefix_lengths = 1..self.segments.len();
94        proper_prefix_lengths.map(|depth| self.ancestor_or_self_at_depth(depth))
95    }
96
97    /// Return the prefix of `depth` segments: the ancestor at that depth, or the path itself at its own depth.
98    pub fn ancestor_or_self_at_depth(&self, depth: usize) -> Self {
99        let prefix_segments = self.segments[..depth].iter().cloned();
100        Self::from_segments(prefix_segments)
101    }
102
103    /// Return this path truncated to at most `depth` segments: its ancestor at that depth, or itself when it is no deeper.
104    pub fn truncated_to_depth(&self, depth: usize) -> Self {
105        let own_depth = self.depth();
106        let prefix_length = own_depth.min(depth);
107        self.ancestor_or_self_at_depth(prefix_length)
108    }
109
110    /// Tell whether `text` follows the component id rules: ASCII letters, digits, `_` and single inner `-`, starting with a letter or `_`.
111    pub fn is_component_id(text: &str) -> bool {
112        let mut characters = text.chars();
113        let Some(first_character) = characters.next() else {
114            return false;
115        };
116        let is_valid_start = first_character.is_ascii_alphabetic() || first_character == '_';
117        let has_only_id_characters = characters
118            .all(|character| character.is_ascii_alphanumeric() || matches!(character, '_' | '-'));
119        let has_double_hyphen = text.contains("--");
120        let has_trailing_hyphen = text.ends_with('-');
121        let has_dangling_hyphen = has_double_hyphen || has_trailing_hyphen;
122        let is_every_hyphen_inner_and_single = !has_dangling_hyphen;
123        is_valid_start && has_only_id_characters && is_every_hyphen_inner_and_single
124    }
125
126    /// Write a segment bare when it parses back unchanged, otherwise quoted.
127    fn write_segment(formatter: &mut fmt::Formatter<'_>, segment: &str) -> fmt::Result {
128        let is_empty = segment.is_empty();
129        let has_reserved_character = segment.contains([SEPARATOR, QUOTE, ESCAPE]);
130        let needs_quotes = is_empty || has_reserved_character;
131        if needs_quotes {
132            return Self::write_quoted_segment(formatter, segment);
133        }
134        formatter.write_str(segment)
135    }
136
137    /// Write a segment between `"` with every `"` and `\` backslash-escaped.
138    fn write_quoted_segment(formatter: &mut fmt::Formatter<'_>, segment: &str) -> fmt::Result {
139        formatter.write_char(QUOTE)?;
140        for character in segment.chars() {
141            let is_escaped = ESCAPED_CHARACTERS.contains(&character);
142            if is_escaped {
143                formatter.write_char(ESCAPE)?;
144            }
145            formatter.write_char(character)?;
146        }
147        formatter.write_char(QUOTE)
148    }
149
150    /// Tell whether this is the root path.
151    pub fn is_root(&self) -> bool {
152        self.segments.is_empty()
153    }
154
155    /// Return the path's depth, its segment count: 0 for the root.
156    pub fn depth(&self) -> usize {
157        self.segments.len()
158    }
159
160    /// Return the last segment, the element's own id, or `None` for the root.
161    pub fn name(&self) -> Option<&str> {
162        self.segments.last().map(String::as_str)
163    }
164
165    /// Return the path's segments, outermost first.
166    pub fn segments(&self) -> &[String] {
167        &self.segments
168    }
169}
170
171impl fmt::Display for ElementPath {
172    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
173        for (index, segment) in self.segments.iter().enumerate() {
174            let is_after_first = index > 0;
175            if is_after_first {
176                formatter.write_char(SEPARATOR)?;
177            }
178            Self::write_segment(formatter, segment)?;
179        }
180        Ok(())
181    }
182}
183
184impl FromStr for ElementPath {
185    type Err = PathParseError;
186
187    fn from_str(text: &str) -> Result<Self, Self::Err> {
188        let is_empty = text.is_empty();
189        if is_empty {
190            return Err(PathParseError::Empty);
191        }
192        let mut parser = PathParser::new(text);
193        parser.parse_path()
194    }
195}
196
197/// Explain why a string fails to parse as an element path.
198#[derive(Debug, Clone, PartialEq, Eq, Error)]
199pub enum PathParseError {
200    #[error("an element path must have at least one segment")]
201    Empty,
202    #[error("empty segment at byte {offset}; quote it as \"\" to mean an empty id")]
203    EmptySegment { offset: usize },
204    #[error("unexpected `{character}` at byte {offset}; quote the segment to use it")]
205    UnexpectedCharacter { character: char, offset: usize },
206    #[error("unterminated quoted segment starting at byte {offset}")]
207    UnterminatedQuote { offset: usize },
208    #[error("invalid escape `\\{character}` at byte {offset}; only `\\\"` and `\\\\` are allowed")]
209    InvalidEscape { character: char, offset: usize },
210}
211
212/// Parse the escaped display form of a path back into its segments.
213struct PathParser<'a> {
214    characters: Peekable<CharIndices<'a>>,
215    end_offset: usize,
216}
217
218impl<'a> PathParser<'a> {
219    /// Create a new parser at the start of `text`.
220    fn new(text: &'a str) -> Self {
221        let characters = text.char_indices().peekable();
222        let end_offset = text.len();
223        Self {
224            characters,
225            end_offset,
226        }
227    }
228
229    /// Parse the whole text as separator-joined segments.
230    fn parse_path(&mut self) -> Result<ElementPath, PathParseError> {
231        let mut segments = Vec::new();
232        loop {
233            let segment = self.parse_segment()?;
234            segments.push(segment);
235            let Some((offset, character)) = self.characters.next() else {
236                return Ok(ElementPath { segments });
237            };
238            let is_unexpected = character != SEPARATOR;
239            if is_unexpected {
240                return Err(PathParseError::UnexpectedCharacter { character, offset });
241            }
242        }
243    }
244
245    /// Parse one segment, quoted when it opens with a quote, else bare.
246    fn parse_segment(&mut self) -> Result<String, PathParseError> {
247        let Some(&(opening_offset, QUOTE)) = self.characters.peek() else {
248            return self.parse_bare_segment();
249        };
250        self.characters.next();
251        self.parse_quoted_segment(opening_offset)
252    }
253
254    /// Parse a quoted segment's content up to its closing quote, unescaping as it goes.
255    fn parse_quoted_segment(&mut self, opening_offset: usize) -> Result<String, PathParseError> {
256        let mut segment = String::new();
257        loop {
258            let Some((offset, character)) = self.characters.next() else {
259                return Err(PathParseError::UnterminatedQuote {
260                    offset: opening_offset,
261                });
262            };
263            let is_closing_quote = character == QUOTE;
264            if is_closing_quote {
265                return Ok(segment);
266            }
267            let is_escape = character == ESCAPE;
268            if is_escape {
269                let escaped = self.parse_escaped_character(offset)?;
270                segment.push(escaped);
271                continue;
272            }
273            segment.push(character);
274        }
275    }
276
277    /// Parse the character an escape stands for, only a quote or an escape.
278    fn parse_escaped_character(&mut self, escape_offset: usize) -> Result<char, PathParseError> {
279        let Some((_offset, character)) = self.characters.next() else {
280            return Err(PathParseError::UnterminatedQuote {
281                offset: escape_offset,
282            });
283        };
284        let is_valid_escape = ESCAPED_CHARACTERS.contains(&character);
285        if is_valid_escape {
286            return Ok(character);
287        }
288        Err(PathParseError::InvalidEscape {
289            character,
290            offset: escape_offset,
291        })
292    }
293
294    /// Parse a non-empty bare segment up to the next separator.
295    fn parse_bare_segment(&mut self) -> Result<String, PathParseError> {
296        let start_offset = self.peek_offset();
297        let mut segment = String::new();
298        while let Some(&(offset, character)) = self.characters.peek() {
299            let is_separator = character == SEPARATOR;
300            if is_separator {
301                break;
302            }
303            let is_reserved = ESCAPED_CHARACTERS.contains(&character);
304            if is_reserved {
305                return Err(PathParseError::UnexpectedCharacter { character, offset });
306            }
307            segment.push(character);
308            self.characters.next();
309        }
310        let is_empty = segment.is_empty();
311        if is_empty {
312            return Err(PathParseError::EmptySegment {
313                offset: start_offset,
314            });
315        }
316        Ok(segment)
317    }
318
319    /// Return the byte offset of the next character, or the text's end.
320    fn peek_offset(&mut self) -> usize {
321        let next_offset = self.characters.peek().map(|&(offset, _character)| offset);
322        next_offset.unwrap_or(self.end_offset)
323    }
324}
325
326impl Serialize for ElementPath {
327    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
328        serializer.collect_str(self)
329    }
330}
331
332impl<'de> Deserialize<'de> for ElementPath {
333    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
334        deserializer.deserialize_str(ElementPathVisitor)
335    }
336}
337
338/// Parse a path from a JSON string, map key included.
339struct ElementPathVisitor;
340
341impl Visitor<'_> for ElementPathVisitor {
342    type Value = ElementPath;
343
344    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
345        formatter.write_str("an element path such as `backend.api` or `backend.\"v1.2\"`")
346    }
347
348    fn visit_str<E: de::Error>(self, text: &str) -> Result<ElementPath, E> {
349        text.parse().map_err(E::custom)
350    }
351}