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}