1use std::fmt;
4
5#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
8pub struct ResourcePath {
9 segments: Vec<String>,
10}
11
12impl ResourcePath {
13 pub fn root() -> Self {
14 Self::default()
15 }
16
17 pub fn parse(path: &str) -> Option<Self> {
19 if path.is_empty() {
20 return Some(Self::root());
21 }
22 let segments: Vec<String> = path.split('/').map(str::to_owned).collect();
23 segments
24 .iter()
25 .all(|s| !s.is_empty())
26 .then_some(Self { segments })
27 }
28
29 pub fn from_segments<I, S>(segments: I) -> Self
30 where
31 I: IntoIterator<Item = S>,
32 S: Into<String>,
33 {
34 Self {
35 segments: segments.into_iter().map(Into::into).collect(),
36 }
37 }
38
39 pub fn segments(&self) -> impl ExactSizeIterator<Item = &str> + Clone {
40 self.segments.iter().map(String::as_str)
41 }
42
43 pub fn len(&self) -> usize {
44 self.segments.len()
45 }
46
47 pub fn is_empty(&self) -> bool {
48 self.segments.is_empty()
49 }
50
51 pub fn is_document(&self) -> bool {
52 !self.segments.is_empty() && self.segments.len().is_multiple_of(2)
53 }
54
55 pub fn is_collection(&self) -> bool {
56 !self.segments.len().is_multiple_of(2)
57 }
58
59 pub fn last(&self) -> Option<&str> {
61 self.segments.last().map(String::as_str)
62 }
63
64 pub fn collection_id(&self) -> Option<&str> {
66 if self.is_document() {
67 self.segments
68 .get(self.segments.len() - 2)
69 .map(String::as_str)
70 } else {
71 None
72 }
73 }
74
75 pub fn parent(&self) -> Option<Self> {
76 (!self.segments.is_empty()).then(|| Self {
77 segments: self.segments[..self.segments.len() - 1].to_vec(),
78 })
79 }
80
81 pub fn child(&self, segment: impl Into<String>) -> Self {
82 let mut segments = self.segments.clone();
83 segments.push(segment.into());
84 Self { segments }
85 }
86
87 pub fn starts_with(&self, prefix: &Self) -> bool {
88 self.segments.starts_with(&prefix.segments)
89 }
90}
91
92impl fmt::Display for ResourcePath {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 f.write_str(&self.segments.join("/"))
95 }
96}