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