firebase_rs_sdk/firestore/api/
reference.rs1use rand::distributions::Alphanumeric;
2use rand::{thread_rng, Rng};
3use std::fmt::{Display, Formatter};
4use std::sync::Arc;
5
6use crate::firestore::error::{invalid_argument, FirestoreResult};
7use crate::firestore::model::{DocumentKey, ResourcePath};
8
9use super::query::{ConvertedQuery, Query};
10use super::Firestore;
11use super::FirestoreDataConverter;
12
13#[derive(Clone, Debug)]
14pub struct CollectionReference {
15 firestore: Firestore,
16 path: ResourcePath,
17}
18
19impl CollectionReference {
20 pub(crate) fn new(firestore: Firestore, path: ResourcePath) -> FirestoreResult<Self> {
21 if path.len() % 2 == 0 {
22 return Err(invalid_argument(
23 "Collection references must point to a collection (odd number of segments)",
24 ));
25 }
26 Ok(Self { firestore, path })
27 }
28
29 pub fn firestore(&self) -> &Firestore {
31 &self.firestore
32 }
33
34 pub fn path(&self) -> &ResourcePath {
36 &self.path
37 }
38
39 pub fn id(&self) -> &str {
41 self.path
42 .last_segment()
43 .expect("Collection path always has id")
44 }
45
46 pub fn parent(&self) -> Option<DocumentReference> {
48 self.path.pop_last().and_then(|parent_path| {
49 if parent_path.is_empty() || parent_path.len() % 2 != 0 {
50 return None;
51 }
52 DocumentReference::new(self.firestore.clone(), parent_path).ok()
53 })
54 }
55
56 pub fn doc(&self, document_id: Option<&str>) -> FirestoreResult<DocumentReference> {
60 let id = document_id
61 .map(|id| id.to_string())
62 .unwrap_or_else(generate_auto_id);
63 if id.contains('/') {
64 return Err(invalid_argument("Document ID cannot contain '/'."));
65 }
66 let path = self.path.child([id]);
67 DocumentReference::new(self.firestore.clone(), path)
68 }
69
70 pub fn with_converter<C>(&self, converter: C) -> ConvertedCollectionReference<C>
71 where
72 C: FirestoreDataConverter,
73 {
74 ConvertedCollectionReference {
75 inner: self.clone(),
76 converter: Arc::new(converter),
77 }
78 }
79
80 pub fn query(&self) -> Query {
82 Query::new(self.firestore.clone(), self.path.clone())
83 .expect("CollectionReference always points to a valid collection")
84 }
85}
86
87impl Display for CollectionReference {
88 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
89 write!(f, "CollectionReference({})", self.path.canonical_string())
90 }
91}
92
93#[derive(Clone, Debug)]
94pub struct DocumentReference {
95 firestore: Firestore,
96 key: DocumentKey,
97}
98
99impl DocumentReference {
100 pub(crate) fn new(firestore: Firestore, path: ResourcePath) -> FirestoreResult<Self> {
101 let key = DocumentKey::from_path(path)?;
102 Ok(Self { firestore, key })
103 }
104
105 pub fn firestore(&self) -> &Firestore {
107 &self.firestore
108 }
109
110 pub fn id(&self) -> &str {
112 self.key.id()
113 }
114
115 pub fn path(&self) -> &ResourcePath {
117 self.key.path()
118 }
119
120 pub fn parent(&self) -> CollectionReference {
122 CollectionReference::new(self.firestore.clone(), self.key.collection_path())
123 .expect("Document parent path is always a collection")
124 }
125
126 pub fn collection(&self, path: &str) -> FirestoreResult<CollectionReference> {
128 let sub_path = ResourcePath::from_string(path)?;
129 let full_path = self.key.path().child(sub_path.as_vec().clone());
130 CollectionReference::new(self.firestore.clone(), full_path)
131 }
132
133 pub fn with_converter<C>(&self, converter: C) -> ConvertedDocumentReference<C>
135 where
136 C: FirestoreDataConverter,
137 {
138 ConvertedDocumentReference::new(self.clone(), Arc::new(converter))
139 }
140}
141
142impl Display for DocumentReference {
143 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
144 write!(
145 f,
146 "DocumentReference({})",
147 self.key.path().canonical_string()
148 )
149 }
150}
151
152fn generate_auto_id() -> String {
153 thread_rng()
154 .sample_iter(&Alphanumeric)
155 .map(char::from)
156 .take(20)
157 .collect()
158}
159
160#[derive(Clone)]
161pub struct ConvertedCollectionReference<C>
162where
163 C: FirestoreDataConverter,
164{
165 inner: CollectionReference,
166 converter: Arc<C>,
167}
168
169impl<C> ConvertedCollectionReference<C>
170where
171 C: FirestoreDataConverter,
172{
173 pub fn firestore(&self) -> &Firestore {
175 self.inner.firestore()
176 }
177
178 pub fn path(&self) -> &ResourcePath {
180 self.inner.path()
181 }
182
183 pub fn id(&self) -> &str {
185 self.inner.id()
186 }
187
188 pub fn doc(&self, document_id: Option<&str>) -> FirestoreResult<ConvertedDocumentReference<C>> {
190 let document = self.inner.doc(document_id)?;
191 Ok(ConvertedDocumentReference::new(
192 document,
193 Arc::clone(&self.converter),
194 ))
195 }
196
197 pub fn raw(&self) -> &CollectionReference {
199 &self.inner
200 }
201
202 pub fn query(&self) -> ConvertedQuery<C> {
204 ConvertedQuery::new(self.inner.query(), Arc::clone(&self.converter))
205 }
206}
207
208#[derive(Clone)]
209pub struct ConvertedDocumentReference<C>
210where
211 C: FirestoreDataConverter,
212{
213 reference: DocumentReference,
214 converter: Arc<C>,
215}
216
217impl<C> ConvertedDocumentReference<C>
218where
219 C: FirestoreDataConverter,
220{
221 fn new(reference: DocumentReference, converter: Arc<C>) -> Self {
222 Self {
223 reference,
224 converter,
225 }
226 }
227
228 pub fn firestore(&self) -> &Firestore {
230 self.reference.firestore()
231 }
232
233 pub fn id(&self) -> &str {
235 self.reference.id()
236 }
237
238 pub fn path(&self) -> &ResourcePath {
240 self.reference.path()
241 }
242
243 pub fn parent(&self) -> CollectionReference {
245 self.reference.parent()
246 }
247
248 pub fn raw(&self) -> &DocumentReference {
250 &self.reference
251 }
252
253 pub fn converter(&self) -> Arc<C> {
255 Arc::clone(&self.converter)
256 }
257
258 pub fn with_converter<D>(&self, converter: D) -> ConvertedDocumentReference<D>
259 where
260 D: FirestoreDataConverter,
261 {
262 ConvertedDocumentReference::new(self.reference.clone(), Arc::new(converter))
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269 use crate::app::api::initialize_app;
270 use crate::app::{FirebaseAppSettings, FirebaseOptions};
271 use crate::firestore::api::get_firestore;
272
273 fn unique_settings() -> FirebaseAppSettings {
274 use std::sync::atomic::{AtomicUsize, Ordering};
275 static COUNTER: AtomicUsize = AtomicUsize::new(0);
276 FirebaseAppSettings {
277 name: Some(format!(
278 "firestore-ref-{}",
279 COUNTER.fetch_add(1, Ordering::SeqCst)
280 )),
281 ..Default::default()
282 }
283 }
284
285 fn setup_firestore() -> Firestore {
286 let options = FirebaseOptions {
287 project_id: Some("test-project".into()),
288 ..Default::default()
289 };
290 let app = initialize_app(options, Some(unique_settings())).unwrap();
291 let firestore = get_firestore(Some(app)).unwrap();
292 Firestore::from_arc(firestore)
293 }
294
295 #[test]
296 fn collection_and_document_roundtrip() {
297 let firestore = setup_firestore();
298 let collection = firestore.collection("cities").unwrap();
299 assert_eq!(collection.id(), "cities");
300 let document = collection.doc(Some("sf")).unwrap();
301 assert_eq!(document.id(), "sf");
302 assert_eq!(document.parent().id(), "cities");
303 }
304
305 #[test]
306 fn auto_id_generation() {
307 let firestore = setup_firestore();
308 let collection = firestore.collection("cities").unwrap();
309 let document = collection.doc(None).unwrap();
310 assert_eq!(document.parent().id(), "cities");
311 assert_eq!(document.id().len(), 20);
312 }
313}