Skip to main content

this_me/kernel/
path.rs

1use std::fmt;
2
3pub type Path = Vec<String>;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct ParsedPath {
7    parts: Vec<PathPart>,
8}
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum PathPart {
12    Segment(String),
13    Selector(Selector),
14}
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum Selector {
18    EmptyPlural,
19    Literal(String),
20    Expression(String),
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum PathParseError {
25    Empty,
26    EmptySegment,
27    UnclosedSelector,
28    UnterminatedQuote,
29}
30
31impl fmt::Display for PathParseError {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        match self {
34            Self::Empty => write!(f, "path cannot be empty"),
35            Self::EmptySegment => write!(f, "path segment cannot be empty"),
36            Self::UnclosedSelector => write!(f, "selector is missing a closing bracket"),
37            Self::UnterminatedQuote => write!(f, "selector quote is not terminated"),
38        }
39    }
40}
41
42impl std::error::Error for PathParseError {}
43
44impl ParsedPath {
45    pub fn parse(input: &str) -> Result<Self, PathParseError> {
46        let input = input.trim();
47        if input.is_empty() {
48            return Err(PathParseError::Empty);
49        }
50
51        let mut parts = Vec::new();
52        let mut segment = String::new();
53        let mut last_was_dot = true;
54        let mut i = 0;
55
56        while i < input.len() {
57            let ch = next_char(input, i);
58            match ch {
59                '.' => {
60                    if segment.trim().is_empty() {
61                        if last_was_dot {
62                            return Err(PathParseError::EmptySegment);
63                        }
64                    } else {
65                        flush_segment(&mut parts, &mut segment)?;
66                    }
67                    last_was_dot = true;
68                    i += ch.len_utf8();
69                }
70                '[' => {
71                    if !segment.trim().is_empty() {
72                        flush_segment(&mut parts, &mut segment)?;
73                    }
74                    let (selector, next) = parse_selector(input, i + ch.len_utf8())?;
75                    parts.push(PathPart::Selector(selector));
76                    last_was_dot = false;
77                    i = next;
78                }
79                _ => {
80                    segment.push(ch);
81                    last_was_dot = false;
82                    i += ch.len_utf8();
83                }
84            }
85        }
86
87        if !segment.trim().is_empty() {
88            flush_segment(&mut parts, &mut segment)?;
89        } else if last_was_dot {
90            return Err(PathParseError::EmptySegment);
91        }
92
93        if parts.is_empty() {
94            return Err(PathParseError::Empty);
95        }
96
97        Ok(Self { parts })
98    }
99
100    pub fn parts(&self) -> &[PathPart] {
101        &self.parts
102    }
103
104    pub fn normalized(&self) -> Path {
105        let mut out = Vec::new();
106        for part in &self.parts {
107            match part {
108                PathPart::Segment(segment) => out.push(segment.clone()),
109                PathPart::Selector(Selector::EmptyPlural) => {}
110                PathPart::Selector(Selector::Literal(value))
111                | PathPart::Selector(Selector::Expression(value)) => out.push(value.clone()),
112            }
113        }
114        out
115    }
116}
117
118pub trait IntoPath {
119    fn into_path(self) -> Result<Path, PathParseError>;
120}
121
122impl IntoPath for Path {
123    fn into_path(self) -> Result<Path, PathParseError> {
124        Ok(self)
125    }
126}
127
128impl IntoPath for &[&str] {
129    fn into_path(self) -> Result<Path, PathParseError> {
130        Ok(self.iter().map(|segment| (*segment).to_string()).collect())
131    }
132}
133
134impl<const N: usize> IntoPath for [&str; N] {
135    fn into_path(self) -> Result<Path, PathParseError> {
136        Ok(self.into_iter().map(str::to_string).collect())
137    }
138}
139
140impl IntoPath for &str {
141    fn into_path(self) -> Result<Path, PathParseError> {
142        if self.trim().is_empty() {
143            return Ok(Vec::new());
144        }
145        ParsedPath::parse(self).map(|path| path.normalized())
146    }
147}
148
149impl IntoPath for String {
150    fn into_path(self) -> Result<Path, PathParseError> {
151        self.as_str().into_path()
152    }
153}
154
155fn flush_segment(parts: &mut Vec<PathPart>, segment: &mut String) -> Result<(), PathParseError> {
156    let segment = std::mem::take(segment).trim().to_string();
157    if segment.is_empty() {
158        return Err(PathParseError::EmptySegment);
159    }
160    parts.push(PathPart::Segment(segment));
161    Ok(())
162}
163
164fn parse_selector(input: &str, start: usize) -> Result<(Selector, usize), PathParseError> {
165    let mut raw = String::new();
166    let mut quote = None;
167    let mut escaped = false;
168    let mut i = start;
169
170    while i < input.len() {
171        let ch = next_char(input, i);
172        if escaped {
173            raw.push(ch);
174            escaped = false;
175            i += ch.len_utf8();
176            continue;
177        }
178
179        if let Some(expected_quote) = quote {
180            if ch == '\\' {
181                raw.push(ch);
182                escaped = true;
183            } else {
184                if ch == expected_quote {
185                    quote = None;
186                }
187                raw.push(ch);
188            }
189            i += ch.len_utf8();
190            continue;
191        }
192
193        match ch {
194            '"' | '\'' => {
195                quote = Some(ch);
196                raw.push(ch);
197                i += ch.len_utf8();
198            }
199            ']' => {
200                if quote.is_some() {
201                    return Err(PathParseError::UnterminatedQuote);
202                }
203                return Ok((classify_selector(&raw)?, i + ch.len_utf8()));
204            }
205            _ => {
206                raw.push(ch);
207                i += ch.len_utf8();
208            }
209        }
210    }
211
212    if quote.is_some() {
213        return Err(PathParseError::UnterminatedQuote);
214    }
215    Err(PathParseError::UnclosedSelector)
216}
217
218fn classify_selector(raw: &str) -> Result<Selector, PathParseError> {
219    let selector = raw.trim();
220    if selector.is_empty() {
221        return Ok(Selector::EmptyPlural);
222    }
223
224    if is_quoted(selector) {
225        return Ok(Selector::Literal(unquote(selector)?));
226    }
227
228    if looks_like_expression(selector) {
229        return Ok(Selector::Expression(selector.to_string()));
230    }
231
232    Ok(Selector::Literal(selector.to_string()))
233}
234
235fn is_quoted(value: &str) -> bool {
236    let mut chars = value.chars();
237    let Some(first) = chars.next() else {
238        return false;
239    };
240    let Some(last) = value.chars().last() else {
241        return false;
242    };
243    (first == '"' || first == '\'') && first == last && value.len() >= 2
244}
245
246fn unquote(value: &str) -> Result<String, PathParseError> {
247    let mut chars = value.chars();
248    let quote = chars.next().ok_or(PathParseError::UnterminatedQuote)?;
249    let mut out = String::new();
250    let mut escaped = false;
251
252    for ch in chars.take(value.chars().count().saturating_sub(2)) {
253        if escaped {
254            out.push(ch);
255            escaped = false;
256            continue;
257        }
258        if ch == '\\' {
259            escaped = true;
260        } else {
261            out.push(ch);
262        }
263    }
264
265    if escaped {
266        out.push('\\');
267    }
268
269    if !value.ends_with(quote) {
270        return Err(PathParseError::UnterminatedQuote);
271    }
272
273    Ok(out)
274}
275
276fn looks_like_expression(selector: &str) -> bool {
277    selector.contains("=>")
278        || selector.contains("&&")
279        || selector.contains("||")
280        || selector.contains(">=")
281        || selector.contains("<=")
282        || selector.contains("==")
283        || selector.contains("!=")
284        || selector.contains('>')
285        || selector.contains('<')
286        || selector.contains("..")
287}
288
289fn next_char(input: &str, index: usize) -> char {
290    input[index..]
291        .chars()
292        .next()
293        .expect("index must point at a valid character")
294}