firebase_rs_sdk/firestore/api/
reference.rs

1use 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    /// Returns the Firestore instance that created this collection reference.
30    pub fn firestore(&self) -> &Firestore {
31        &self.firestore
32    }
33
34    /// The full resource path of the collection (e.g. `rooms/eros/messages`).
35    pub fn path(&self) -> &ResourcePath {
36        &self.path
37    }
38
39    /// The last segment of the collection path.
40    pub fn id(&self) -> &str {
41        self.path
42            .last_segment()
43            .expect("Collection path always has id")
44    }
45
46    /// Returns the document that logically contains this collection, if any.
47    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    /// Returns a reference to the document identified by `document_id`.
57    ///
58    /// When `document_id` is `None`, an auto-ID is generated.
59    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    /// Creates a query that targets this collection.
81    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    /// Returns the Firestore instance that created this document reference.
106    pub fn firestore(&self) -> &Firestore {
107        &self.firestore
108    }
109
110    /// The document identifier (the last segment of its path).
111    pub fn id(&self) -> &str {
112        self.key.id()
113    }
114
115    /// The full resource path to the document.
116    pub fn path(&self) -> &ResourcePath {
117        self.key.path()
118    }
119
120    /// The parent collection containing this document.
121    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    /// Returns a reference to a subcollection rooted at this document.
127    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    /// Returns a typed document reference using the provided converter.
134    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    /// Accesses the underlying Firestore instance.
174    pub fn firestore(&self) -> &Firestore {
175        self.inner.firestore()
176    }
177
178    /// Full resource path for the collection.
179    pub fn path(&self) -> &ResourcePath {
180        self.inner.path()
181    }
182
183    /// The collection identifier (last path segment).
184    pub fn id(&self) -> &str {
185        self.inner.id()
186    }
187
188    /// Returns a typed document reference within this collection.
189    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    /// Provides access to the untyped collection reference.
198    pub fn raw(&self) -> &CollectionReference {
199        &self.inner
200    }
201
202    /// Creates a query for the underlying collection using this converter.
203    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    /// Accesses the underlying Firestore instance.
229    pub fn firestore(&self) -> &Firestore {
230        self.reference.firestore()
231    }
232
233    /// The document identifier assigned to this reference.
234    pub fn id(&self) -> &str {
235        self.reference.id()
236    }
237
238    /// Full resource path for the document.
239    pub fn path(&self) -> &ResourcePath {
240        self.reference.path()
241    }
242
243    /// Returns the parent collection.
244    pub fn parent(&self) -> CollectionReference {
245        self.reference.parent()
246    }
247
248    /// Provides access to the untyped document reference.
249    pub fn raw(&self) -> &DocumentReference {
250        &self.reference
251    }
252
253    /// Clones the converter used to map data for this reference.
254    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}