firebase_rs_sdk/firestore/model/
resource_path.rs1use 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}