Skip to main content

hidane_core/
path.rs

1//! Document and collection paths, relative to `projects/{p}/databases/{d}/documents`.
2
3use std::fmt;
4
5/// A slash-separated path: an even number of segments names a document (`users/alice`), an
6/// odd number a collection (`users`, `users/alice/posts`). The empty path is the database root.
7#[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    /// Splits on `/`. Returns `None` for empty segments (`a//b`, a leading or trailing `/`).
18    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    /// The last segment: a document ID or a collection ID.
60    pub fn last(&self) -> Option<&str> {
61        self.segments.last().map(String::as_str)
62    }
63
64    /// The ID of the collection this document belongs to.
65    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}