1#[non_exhaustive]
5#[derive(Clone, Debug, PartialEq, Eq, Hash)]
6pub enum PathSegment {
7 Key(String),
9 Index(usize),
11}
12
13#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
31pub struct Path {
32 segments: Vec<PathSegment>,
33}
34
35impl Path {
36 #[must_use]
38 pub const fn new() -> Self {
39 Self {
40 segments: Vec::new(),
41 }
42 }
43
44 pub fn push_key(&mut self, key: impl Into<String>) {
46 self.segments.push(PathSegment::Key(key.into()));
47 }
48
49 pub fn push_index(&mut self, index: usize) {
51 self.segments.push(PathSegment::Index(index));
52 }
53
54 #[must_use]
56 pub fn with_key(mut self, key: impl Into<String>) -> Self {
57 self.push_key(key);
58 self
59 }
60
61 #[must_use]
63 pub fn with_index(mut self, index: usize) -> Self {
64 self.push_index(index);
65 self
66 }
67
68 pub fn pop(&mut self) -> Option<PathSegment> {
70 self.segments.pop()
71 }
72
73 #[must_use]
75 pub fn segments(&self) -> &[PathSegment] {
76 &self.segments
77 }
78
79 #[must_use]
81 pub fn is_empty(&self) -> bool {
82 self.segments.is_empty()
83 }
84
85 #[must_use]
87 pub fn len(&self) -> usize {
88 self.segments.len()
89 }
90}
91
92#[macro_export]
105macro_rules! path {
106 () => { $crate::Path::new() };
107 ($($segment:expr),+ $(,)?) => {{
108 let mut p = $crate::Path::new();
109 $( $crate::__private::push_segment(&mut p, $segment); )+
110 p
111 }};
112}
113
114#[doc(hidden)]
116pub mod __private {
117 use super::{Path, PathSegment};
118
119 pub trait IntoSegment {
121 fn into_segment(self) -> PathSegment;
123 }
124
125 impl IntoSegment for &str {
126 fn into_segment(self) -> PathSegment {
127 PathSegment::Key(self.to_owned())
128 }
129 }
130 impl IntoSegment for String {
131 fn into_segment(self) -> PathSegment {
132 PathSegment::Key(self)
133 }
134 }
135 impl IntoSegment for &String {
136 fn into_segment(self) -> PathSegment {
137 PathSegment::Key(self.clone())
138 }
139 }
140 impl IntoSegment for usize {
141 fn into_segment(self) -> PathSegment {
142 PathSegment::Index(self)
143 }
144 }
145 impl IntoSegment for i32 {
147 fn into_segment(self) -> PathSegment {
148 PathSegment::Index(usize::try_from(self).unwrap_or(usize::MAX))
149 }
150 }
151 impl IntoSegment for u32 {
152 fn into_segment(self) -> PathSegment {
153 PathSegment::Index(usize::try_from(self).unwrap_or(usize::MAX))
154 }
155 }
156 impl IntoSegment for u64 {
157 fn into_segment(self) -> PathSegment {
158 PathSegment::Index(usize::try_from(self).unwrap_or(usize::MAX))
159 }
160 }
161
162 pub fn push_segment(path: &mut Path, segment: impl IntoSegment) {
164 path.segments.push(segment.into_segment());
165 }
166}
167
168#[derive(Clone, Debug, PartialEq, Eq, Hash)]
170pub struct ParsePathError {
171 offset: usize,
172 reason: &'static str,
173}
174
175impl ParsePathError {
176 #[must_use]
178 pub fn offset(&self) -> usize {
179 self.offset
180 }
181
182 #[must_use]
184 pub fn reason(&self) -> &'static str {
185 self.reason
186 }
187}
188
189impl core::fmt::Display for ParsePathError {
190 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
191 write!(f, "invalid path at byte {}: {}", self.offset, self.reason)
192 }
193}
194
195impl std::error::Error for ParsePathError {}
196
197fn bare_char(c: char) -> bool {
200 !(matches!(c, '.' | '[' | ']' | '"' | '\\') || c.is_whitespace() || c.is_control())
201}
202
203impl Path {
204 pub fn parse(s: &str) -> Result<Path, ParsePathError> {
247 let err = |offset: usize, reason: &'static str| ParsePathError { offset, reason };
248 let mut path = Path::new();
249 if s.is_empty() {
253 return Err(err(0, "an empty path: write `.` for the root"));
254 }
255 if s == "." {
256 return Ok(path);
257 }
258 let mut chars = s.char_indices().peekable();
259 let mut first = true;
261 while let Some(&(at, c)) = chars.peek() {
262 match c {
263 '[' => {
264 chars.next();
265 let mut index: usize = 0;
266 let mut digits = 0usize;
267 loop {
268 match chars.next() {
269 Some((_, ']')) if digits > 0 => break,
270 Some((_, ']')) => return Err(err(at, "an index needs digits: `[0]`")),
271 Some((p, d)) => {
272 let Some(v) = d.to_digit(10) else {
273 return Err(err(p, "an index is decimal digits only"));
274 };
275 index = index
276 .checked_mul(10)
277 .and_then(|i| i.checked_add(v as usize))
278 .ok_or_else(|| err(at, "the index is too large"))?;
279 digits = digits.saturating_add(1);
280 }
281 None => return Err(err(at, "unclosed `[`")),
282 }
283 }
284 path.push_index(index);
285 }
286 '.' if !first => {
287 chars.next();
288 let next = chars.peek().copied();
289 match next {
290 Some((_, '"')) => path.push_key(quoted(&mut chars)?),
291 Some((p, c)) if bare_char(c) => {
292 path.push_key(bare(s, p, &mut chars));
293 }
294 Some((p, _)) => {
295 return Err(err(p, "expected a key after `.`"));
296 }
297 None => return Err(err(s.len(), "expected a key after `.`")),
298 }
299 }
300 '"' if first => path.push_key(quoted(&mut chars)?),
301 c if first && bare_char(c) => path.push_key(bare(s, at, &mut chars)),
302 '.' => return Err(err(at, "a path starts with a key or an index")),
303 _ if first => {
304 return Err(err(at, "quote this key: `\"…\"`"));
305 }
306 _ => return Err(err(at, "expected `.` or `[` after a segment")),
307 }
308 first = false;
309 }
310 Ok(path)
311 }
312}
313
314impl core::str::FromStr for Path {
315 type Err = ParsePathError;
316
317 fn from_str(s: &str) -> Result<Self, Self::Err> {
319 Path::parse(s)
320 }
321}
322
323type Chars<'a> = core::iter::Peekable<core::str::CharIndices<'a>>;
324
325fn bare(s: &str, start: usize, chars: &mut Chars<'_>) -> String {
327 let mut end = s.len();
328 while let Some(&(p, c)) = chars.peek() {
329 if !bare_char(c) {
330 end = p;
331 break;
332 }
333 chars.next();
334 }
335 s.get(start..end).unwrap_or_default().to_owned()
336}
337
338fn quoted(chars: &mut Chars<'_>) -> Result<String, ParsePathError> {
340 let err = |offset: usize, reason: &'static str| ParsePathError { offset, reason };
341 let open = chars.next().map_or(0, |(p, _)| p);
342 let mut key = String::new();
343 loop {
344 match chars.next() {
345 Some((_, '"')) => return Ok(key),
346 Some((p, '\\')) => match chars.next() {
347 Some((_, '"')) => key.push('"'),
348 Some((_, '\\')) => key.push('\\'),
349 Some((_, 'u')) => {
350 if !matches!(chars.next(), Some((_, '{'))) {
351 return Err(err(p, "write `\\u{hex}`"));
352 }
353 let mut code: u32 = 0;
354 let mut digits = 0usize;
355 loop {
356 match chars.next() {
357 Some((_, '}')) if digits > 0 => break,
358 Some((_, h)) if digits < 6 && h.is_ascii_hexdigit() => {
359 let v = h.to_digit(16).unwrap_or(0);
360 code = code.wrapping_mul(16).wrapping_add(v);
361 digits = digits.saturating_add(1);
362 }
363 _ => return Err(err(p, "write `\\u{hex}` with 1 to 6 hex digits")),
364 }
365 }
366 let c = char::from_u32(code)
367 .ok_or_else(|| err(p, "`\\u{…}` names no character"))?;
368 key.push(c);
369 }
370 _ => return Err(err(p, "unknown escape: use `\\\"`, `\\\\` or `\\u{hex}`")),
371 },
372 Some((_, c)) => key.push(c),
373 None => return Err(err(open, "unclosed `\"`")),
374 }
375 }
376}
377
378fn needs_quotes(key: &str) -> bool {
379 key.is_empty()
380 || key.chars().any(|c| {
381 matches!(c, '.' | '[' | ']' | '"' | '\\') || c.is_whitespace() || c.is_control()
382 })
383}
384
385impl core::fmt::Display for Path {
386 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
390 if self.segments.is_empty() {
391 return f.write_str(".");
392 }
393 for (i, segment) in self.segments.iter().enumerate() {
394 match segment {
395 PathSegment::Index(n) => write!(f, "[{n}]")?,
396 PathSegment::Key(key) => {
397 if i != 0 {
398 f.write_str(".")?;
399 }
400 if needs_quotes(key) {
401 f.write_str("\"")?;
402 for c in key.chars() {
403 match c {
404 '"' => f.write_str("\\\"")?,
405 '\\' => f.write_str("\\\\")?,
406 c if c.is_control() => write!(f, "\\u{{{:x}}}", u32::from(c))?,
407 c => write!(f, "{c}")?,
408 }
409 }
410 f.write_str("\"")?;
411 } else {
412 f.write_str(key)?;
413 }
414 }
415 }
416 }
417 Ok(())
418 }
419}
420
421#[cfg(test)]
422mod tests {
423 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
424
425 use super::*;
426
427 #[test]
428 fn display() {
429 assert_eq!(Path::new().to_string(), ".");
430 let p = Path::new()
431 .with_key("spec")
432 .with_key("ports")
433 .with_index(2)
434 .with_key("port");
435 assert_eq!(p.to_string(), "spec.ports[2].port");
436 assert_eq!(Path::new().with_index(0).to_string(), "[0]");
437 assert_eq!(
438 Path::new()
439 .with_key("a.b")
440 .with_key("")
441 .with_key("q\"\\\n")
442 .to_string(),
443 "\"a.b\".\"\".\"q\\\"\\\\\\u{a}\""
444 );
445 }
446
447 #[test]
448 fn path_macro() {
449 let key = String::from("k");
450 let p = crate::path!["a", 0, &key, key.clone(), 3usize, 4u32, 5u64, -1];
451 assert_eq!(
452 p.segments(),
453 &[
454 PathSegment::Key("a".into()),
455 PathSegment::Index(0),
456 PathSegment::Key("k".into()),
457 PathSegment::Key("k".into()),
458 PathSegment::Index(3),
459 PathSegment::Index(4),
460 PathSegment::Index(5),
461 PathSegment::Index(usize::MAX),
462 ]
463 );
464 assert_eq!(crate::path![], Path::new());
465 assert_eq!(crate::path!["x",], Path::new().with_key("x"));
466 }
467
468 #[test]
469 fn parse_round_trips_display() {
470 let keys = [
471 "a", "spec", "a.b", "", "q\"\\\n", "[0]", "a]", "x y", "\u{85}", "été", "1", "-", "\t",
472 "\\", "\"",
473 ];
474 for k in keys {
475 for p in [
476 Path::new().with_key(k),
477 Path::new().with_key(k).with_index(3).with_key(k),
478 Path::new().with_index(0).with_key(k).with_key("z"),
479 Path::new().with_index(1).with_index(22),
480 ] {
481 assert_eq!(Path::parse(&p.to_string()), Ok(p.clone()), "{p}");
482 assert_eq!(p.to_string().parse::<Path>(), Ok(p));
483 }
484 }
485 assert_eq!(Path::parse("."), Ok(Path::new()));
486 assert_eq!(Path::parse(&Path::new().to_string()), Ok(Path::new()));
487 assert_eq!(
488 Path::parse("spec.template.containers[0].image"),
489 Ok(crate::path!["spec", "template", "containers", 0, "image"])
490 );
491 assert_eq!(Path::parse("a.\"b.c\"[0]"), Ok(crate::path!["a", "b.c", 0]));
492 assert_eq!(Path::parse("a.1"), Ok(crate::path!["a", "1"]));
493 assert_eq!(Path::parse("[007]"), Ok(crate::path![7]));
494 assert_eq!(Path::parse("\"\\u{1F600}\""), Ok(crate::path!["\u{1F600}"]));
495 }
496
497 #[test]
498 fn parse_errors() {
499 let cases: [(&str, usize); 23] = [
500 ("", 0),
501 ("a..b", 2),
502 ("a.", 2),
503 (".a", 0),
504 ("..", 0),
505 ("a b", 1),
506 ("a]", 1),
507 ("]", 0),
508 ("a[", 1),
509 ("a[]", 1),
510 ("a[x]", 2),
511 ("a[-1]", 2),
512 ("a[1", 1),
513 ("a[99999999999999999999999]", 1),
514 ("\"a", 0),
515 ("\"a\"b", 3),
516 ("[0]x", 3),
517 ("\"\\x\"", 1),
518 ("\"\\u{110000}\"", 1),
519 ("\"\\u{}\"", 1),
520 ("\"\\u{1234567}\"", 1),
521 ("\"\\u41\"", 1),
522 (" a", 0),
523 ];
524 for (s, offset) in cases {
525 let e = Path::parse(s).unwrap_err();
526 assert_eq!(e.offset(), offset, "{s:?}: {e}");
527 assert!(!e.reason().is_empty());
528 assert!(e.to_string().starts_with("invalid path at byte "));
529 }
530 }
531
532 #[test]
533 fn push_pop_and_accessors() {
534 let mut p = Path::new();
535 assert!(p.is_empty());
536 p.push_key("a");
537 p.push_index(3);
538 assert_eq!(p.len(), 2);
539 assert_eq!(
540 p.segments(),
541 &[PathSegment::Key("a".into()), PathSegment::Index(3)]
542 );
543 assert_eq!(p.pop(), Some(PathSegment::Index(3)));
544 assert_eq!(p.pop(), Some(PathSegment::Key("a".into())));
545 assert_eq!(p.pop(), None);
546 assert_eq!(p, Path::default());
547 }
548}