firebase_rs_sdk/firestore/model/
resource_path.rs

1use std::cmp::Ordering;
2use std::fmt::{Display, Formatter};
3use std::ops::Deref;
4
5use crate::firestore::error::{invalid_argument, FirestoreResult};
6
7#[derive(Clone, Debug, PartialEq, Eq, Hash)]
8pub struct ResourcePath {
9    segments: Vec<String>,
10}
11
12impl ResourcePath {
13    pub fn new(segments: Vec<String>) -> Self {
14        Self { segments }
15    }
16
17    pub fn root() -> Self {
18        Self {
19            segments: Vec::new(),
20        }
21    }
22
23    pub fn from_segments<I, S>(segments: I) -> Self
24    where
25        I: IntoIterator<Item = S>,
26        S: Into<String>,
27    {
28        let segments = segments.into_iter().map(Into::into).collect();
29        Self::new(segments)
30    }
31
32    pub fn with_offset(segments: Vec<String>, offset: usize) -> Self {
33        if offset >= segments.len() {
34            return Self::root();
35        }
36        Self::new(segments[offset..].to_vec())
37    }
38
39    pub fn from_string(path: &str) -> FirestoreResult<Self> {
40        if path.trim().is_empty() {
41            return Ok(Self::root());
42        }
43
44        if path.contains("//") {
45            return Err(invalid_argument("Found empty segment in resource path"));
46        }
47
48        Ok(Self::from_segments(
49            path.split('/')
50                .filter(|segment| !segment.is_empty())
51                .map(|segment| segment.to_string()),
52        ))
53    }
54
55    pub fn len(&self) -> usize {
56        self.segments.len()
57    }
58
59    pub fn is_empty(&self) -> bool {
60        self.segments.is_empty()
61    }
62
63    pub fn get(&self, index: usize) -> Option<&str> {
64        self.segments.get(index).map(|s| s.as_str())
65    }
66
67    pub fn segment(&self, index: usize) -> Option<&str> {
68        self.segments.get(index).map(|s| s.as_str())
69    }
70
71    pub fn child<I, S>(&self, segments: I) -> Self
72    where
73        I: IntoIterator<Item = S>,
74        S: Into<String>,
75    {
76        let mut new_segments = self.segments.clone();
77        new_segments.extend(segments.into_iter().map(Into::into));
78        Self::new(new_segments)
79    }
80
81    pub fn pop_last(&self) -> Option<Self> {
82        if self.segments.is_empty() {
83            return None;
84        }
85        let mut segments = self.segments.clone();
86        segments.pop();
87        Some(Self::new(segments))
88    }
89
90    pub fn without_last(&self) -> Self {
91        self.pop_last().unwrap_or_else(Self::root)
92    }
93
94    pub fn pop_first(&self) -> Self {
95        self.pop_first_n(1)
96    }
97
98    pub fn pop_first_n(&self, count: usize) -> Self {
99        if count == 0 {
100            return self.clone();
101        }
102        if count >= self.segments.len() {
103            return Self::root();
104        }
105        Self::new(self.segments[count..].to_vec())
106    }
107
108    pub fn last_segment(&self) -> Option<&str> {
109        self.segments.last().map(|s| s.as_str())
110    }
111
112    pub fn as_vec(&self) -> &Vec<String> {
113        &self.segments
114    }
115
116    pub fn canonical_string(&self) -> String {
117        self.segments.join("/")
118    }
119
120    pub fn is_prefix_of(&self, other: &Self) -> bool {
121        if self.len() > other.len() {
122            return false;
123        }
124        self.segments
125            .iter()
126            .zip(other.segments.iter())
127            .all(|(l, r)| l == r)
128    }
129
130    pub fn comparator(left: &Self, right: &Self) -> Ordering {
131        for (l, r) in left.segments.iter().zip(right.segments.iter()) {
132            match l.cmp(r) {
133                Ordering::Equal => continue,
134                non_eq => return non_eq,
135            }
136        }
137        left.len().cmp(&right.len())
138    }
139}
140
141impl Display for ResourcePath {
142    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
143        write!(f, "{}", self.canonical_string())
144    }
145}
146
147impl Deref for ResourcePath {
148    type Target = [String];
149
150    fn deref(&self) -> &Self::Target {
151        &self.segments
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn parse_and_render_path() {
161        let path = ResourcePath::from_string("cities/sf/neighborhoods/downtown").unwrap();
162        assert_eq!(path.len(), 4);
163        assert_eq!(path.last_segment(), Some("downtown"));
164        assert_eq!(path.canonical_string(), "cities/sf/neighborhoods/downtown");
165    }
166
167    #[test]
168    fn handles_root_path() {
169        let path = ResourcePath::from_string("").unwrap();
170        assert!(path.is_empty());
171    }
172
173    #[test]
174    fn rejects_empty_segments() {
175        let err = ResourcePath::from_string("cities//sf").unwrap_err();
176        assert_eq!(err.code_str(), "firestore/invalid-argument");
177    }
178}