1use super::{PathError, PathSegment};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7#[non_exhaustive]
8pub enum PathParseError {
9 UnclosedIndex,
11 InvalidIndex(String),
13 TrailingBackslash,
15 EmptySegment,
18 MissingSeparator,
21}
22
23impl std::fmt::Display for PathParseError {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 match self {
26 PathParseError::UnclosedIndex => {
27 write!(f, "unclosed '[' in path")
28 }
29 PathParseError::InvalidIndex(text) => {
30 write!(f, "invalid path index [{text}]")
31 }
32 PathParseError::TrailingBackslash => {
33 write!(f, "path ends with an unescaped '\\'")
34 }
35 PathParseError::EmptySegment => write!(f, "empty segment in path"),
36 PathParseError::MissingSeparator => {
37 write!(f, "expected '.' or '[' after ']' in path")
38 }
39 }
40 }
41}
42
43impl std::error::Error for PathParseError {}
44
45pub fn try_parse_path(path: &str) -> Result<Vec<PathSegment>, PathParseError> {
96 #[derive(PartialEq)]
97 enum State {
98 Start,
100 AfterDot,
102 InSegment,
103 AfterIndex,
104 }
105
106 let mut segments = Vec::new();
107 let mut current = String::new();
108 let mut current_has_escape = false;
111 let mut state = State::Start;
112 let mut chars = path.chars();
113
114 while let Some(ch) = chars.next() {
115 match (ch, &state) {
116 ('.', State::Start | State::AfterDot) | ('[', State::AfterDot) => {
117 return Err(PathParseError::EmptySegment);
118 }
119 ('.', State::InSegment) => {
120 segments.push(dotted_segment(¤t, current_has_escape));
121 current.clear();
122 current_has_escape = false;
123 state = State::AfterDot;
124 }
125 ('.', State::AfterIndex) => state = State::AfterDot,
126 ('[', _) => {
127 if state == State::InSegment {
128 segments.push(PathSegment::Key(std::mem::take(&mut current)));
129 current_has_escape = false;
130 }
131 let mut index_str = String::new();
132 loop {
133 match chars.next() {
134 Some(']') => break,
135 Some(c) => index_str.push(c),
136 None => return Err(PathParseError::UnclosedIndex),
137 }
138 }
139 let index = parse_index(&index_str)
140 .ok_or_else(|| PathParseError::InvalidIndex(index_str.clone()))?;
141 segments.push(PathSegment::Index(index));
142 state = State::AfterIndex;
143 }
144 (_, State::AfterIndex) => return Err(PathParseError::MissingSeparator),
145 ('\\', _) => {
146 current.push(chars.next().ok_or(PathParseError::TrailingBackslash)?);
147 current_has_escape = true;
148 state = State::InSegment;
149 }
150 _ => {
151 current.push(ch);
152 state = State::InSegment;
153 }
154 }
155 }
156
157 match state {
158 State::AfterDot => return Err(PathParseError::EmptySegment),
159 State::InSegment => segments.push(dotted_segment(¤t, current_has_escape)),
160 State::Start | State::AfterIndex => {}
161 }
162
163 Ok(segments)
164}
165
166fn parse_index(text: &str) -> Option<usize> {
169 if text.is_empty() || !text.bytes().all(|b| b.is_ascii_digit()) {
170 return None;
171 }
172 text.parse().ok()
173}
174
175fn dotted_segment(text: &str, has_escape: bool) -> PathSegment {
178 match parse_index(text) {
179 Some(index) if !has_escape => PathSegment::Index(index),
180 _ => PathSegment::Key(text.to_string()),
181 }
182}
183
184#[deprecated(note = "use try_parse_path; a bad `[index]` used to return an empty Vec")]
189pub fn parse_path(path: &str) -> Vec<PathSegment> {
190 try_parse_path(path).unwrap_or_default()
191}
192
193pub(super) fn path_segments_required(path: &str) -> Result<Vec<PathSegment>, PathError> {
197 let segments = try_parse_path(path)?;
198 if segments.is_empty() {
199 Err(PathError::EmptyPath)
200 } else {
201 Ok(segments)
202 }
203}
204
205pub(super) fn segment_display(segment: &PathSegment) -> String {
208 match segment {
209 PathSegment::Key(key) => key.clone(),
210 PathSegment::Index(index) => format!("[{index}]"),
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn test_parse_path_basic() {
220 assert_eq!(try_parse_path("").unwrap(), Vec::<PathSegment>::new());
221 assert_eq!(
222 try_parse_path("key").unwrap(),
223 vec![PathSegment::Key("key".to_string())]
224 );
225 assert_eq!(
226 try_parse_path("a.b").unwrap(),
227 vec![
228 PathSegment::Key("a".to_string()),
229 PathSegment::Key("b".to_string())
230 ]
231 );
232 assert_eq!(
233 try_parse_path("a.b.c.d").unwrap(),
234 vec![
235 PathSegment::Key("a".to_string()),
236 PathSegment::Key("b".to_string()),
237 PathSegment::Key("c".to_string()),
238 PathSegment::Key("d".to_string())
239 ]
240 );
241 }
242
243 #[test]
244 fn test_try_parse_path_invalid_brackets() {
245 assert_eq!(
246 try_parse_path("items[abc].name"),
247 Err(PathParseError::InvalidIndex("abc".to_string()))
248 );
249 assert_eq!(
250 try_parse_path("items[].name"),
251 Err(PathParseError::InvalidIndex(String::new()))
252 );
253 assert_eq!(
254 try_parse_path("items[0"),
255 Err(PathParseError::UnclosedIndex)
256 );
257 }
258
259 #[test]
260 fn test_try_parse_path_trailing_backslash() {
261 assert_eq!(
262 try_parse_path("a\\"),
263 Err(PathParseError::TrailingBackslash)
264 );
265 assert_eq!(
266 try_parse_path("a.\\"),
267 Err(PathParseError::TrailingBackslash)
268 );
269 }
270
271 #[test]
272 fn test_try_parse_path_empty_segment() {
273 for path in [".", "..", ".a", "a.", "a..b", "a.[0]", "a[0].", "a[0]..b"] {
274 assert_eq!(
275 try_parse_path(path),
276 Err(PathParseError::EmptySegment),
277 "{path:?}"
278 );
279 }
280 }
281
282 #[test]
283 fn test_try_parse_path_missing_separator_after_index() {
284 assert_eq!(
285 try_parse_path("a[0]b"),
286 Err(PathParseError::MissingSeparator)
287 );
288 assert_eq!(
289 try_parse_path("a[0]\\.b"),
290 Err(PathParseError::MissingSeparator)
291 );
292 }
293
294 #[test]
295 fn test_try_parse_path_index_must_be_digits() {
296 assert_eq!(
297 try_parse_path("a[+1]"),
298 Err(PathParseError::InvalidIndex("+1".to_string()))
299 );
300 assert_eq!(
301 try_parse_path("a.+1").unwrap(),
302 vec![
303 PathSegment::Key("a".to_string()),
304 PathSegment::Key("+1".to_string())
305 ]
306 );
307 }
308
309 #[test]
310 fn test_try_parse_path_index_chains() {
311 assert_eq!(
312 try_parse_path("[0][1].a").unwrap(),
313 vec![
314 PathSegment::Index(0),
315 PathSegment::Index(1),
316 PathSegment::Key("a".to_string())
317 ]
318 );
319 assert_eq!(
320 try_parse_path("a[0].1").unwrap(),
321 vec![
322 PathSegment::Key("a".to_string()),
323 PathSegment::Index(0),
324 PathSegment::Index(1)
325 ]
326 );
327 }
328
329 #[test]
330 #[allow(deprecated)]
331 fn test_parse_path_deprecated_wrapper_empty_on_bad_index() {
332 assert_eq!(parse_path("items[abc].name"), Vec::<PathSegment>::new());
333 assert_eq!(parse_path("items[0"), Vec::<PathSegment>::new());
334 }
335
336 #[test]
337 fn test_parse_path_with_array_indices() {
338 assert_eq!(
339 try_parse_path("items[0]").unwrap(),
340 vec![PathSegment::Key("items".to_string()), PathSegment::Index(0)]
341 );
342 assert_eq!(
343 try_parse_path("items[0].name").unwrap(),
344 vec![
345 PathSegment::Key("items".to_string()),
346 PathSegment::Index(0),
347 PathSegment::Key("name".to_string())
348 ]
349 );
350 assert_eq!(
351 try_parse_path("data.items[5].value").unwrap(),
352 vec![
353 PathSegment::Key("data".to_string()),
354 PathSegment::Key("items".to_string()),
355 PathSegment::Index(5),
356 PathSegment::Key("value".to_string())
357 ]
358 );
359 }
360
361 #[test]
362 fn test_parse_path_with_numeric_indices() {
363 assert_eq!(
364 try_parse_path("items.0").unwrap(),
365 vec![PathSegment::Key("items".to_string()), PathSegment::Index(0)]
366 );
367 assert_eq!(
368 try_parse_path("items.0.name").unwrap(),
369 vec![
370 PathSegment::Key("items".to_string()),
371 PathSegment::Index(0),
372 PathSegment::Key("name".to_string())
373 ]
374 );
375 }
376
377 #[test]
378 fn test_parse_path_with_escaping() {
379 assert_eq!(
380 try_parse_path("key\\.with\\.dots").unwrap(),
381 vec![PathSegment::Key("key.with.dots".to_string())]
382 );
383 assert_eq!(
384 try_parse_path("a.key\\.with\\.dots.b").unwrap(),
385 vec![
386 PathSegment::Key("a".to_string()),
387 PathSegment::Key("key.with.dots".to_string()),
388 PathSegment::Key("b".to_string())
389 ]
390 );
391 assert_eq!(
393 try_parse_path("a.\\0").unwrap(),
394 vec![
395 PathSegment::Key("a".to_string()),
396 PathSegment::Key("0".to_string())
397 ]
398 );
399 assert_eq!(
400 try_parse_path("\\1.b").unwrap(),
401 vec![
402 PathSegment::Key("1".to_string()),
403 PathSegment::Key("b".to_string())
404 ]
405 );
406 }
407}