firebase_rs_sdk/firestore/api/
snapshot.rs

1use std::collections::BTreeMap;
2use std::sync::Arc;
3
4use crate::firestore::error::FirestoreResult;
5use crate::firestore::model::DocumentKey;
6use crate::firestore::value::{FirestoreValue, MapValue};
7
8use super::reference::DocumentReference;
9use super::Firestore;
10use super::FirestoreDataConverter;
11
12/// Metadata about the state of a document snapshot.
13#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
14pub struct SnapshotMetadata {
15    from_cache: bool,
16    has_pending_writes: bool,
17}
18
19impl SnapshotMetadata {
20    /// Creates metadata with the provided cache/pending-write flags.
21    pub fn new(from_cache: bool, has_pending_writes: bool) -> Self {
22        Self {
23            from_cache,
24            has_pending_writes,
25        }
26    }
27
28    /// Indicates whether the snapshot was served from a local cache.
29    pub fn from_cache(&self) -> bool {
30        self.from_cache
31    }
32
33    /// Indicates whether the snapshot contains uncommitted local mutations.
34    pub fn has_pending_writes(&self) -> bool {
35        self.has_pending_writes
36    }
37}
38
39/// Snapshot of a document's contents.
40#[derive(Clone, Debug)]
41pub struct DocumentSnapshot {
42    key: DocumentKey,
43    data: Option<MapValue>,
44    metadata: SnapshotMetadata,
45}
46
47impl DocumentSnapshot {
48    pub fn new(key: DocumentKey, data: Option<MapValue>, metadata: SnapshotMetadata) -> Self {
49        Self {
50            key,
51            data,
52            metadata,
53        }
54    }
55
56    /// Returns whether the document exists on the backend.
57    pub fn exists(&self) -> bool {
58        self.data.is_some()
59    }
60
61    /// Returns the decoded document fields if the snapshot contains data.
62    ///
63    /// The returned map borrows from the snapshot; mutate a clone before
64    /// writing it back to Firestore.
65    pub fn data(&self) -> Option<&BTreeMap<String, FirestoreValue>> {
66        self.data.as_ref().map(|map| map.fields())
67    }
68
69    /// Returns snapshot metadata describing cache and mutation state.
70    pub fn metadata(&self) -> &SnapshotMetadata {
71        &self.metadata
72    }
73
74    /// Returns the underlying map value for advanced conversions.
75    pub fn map_value(&self) -> Option<&MapValue> {
76        self.data.as_ref()
77    }
78
79    /// Convenience accessor matching the JS API.
80    pub fn from_cache(&self) -> bool {
81        self.metadata.from_cache()
82    }
83
84    /// Convenience accessor matching the JS API.
85    pub fn has_pending_writes(&self) -> bool {
86        self.metadata.has_pending_writes()
87    }
88
89    /// Returns the identifier of the document represented by this snapshot.
90    pub fn id(&self) -> &str {
91        self.key.id()
92    }
93
94    pub(crate) fn document_key(&self) -> &DocumentKey {
95        &self.key
96    }
97
98    /// Creates a document reference pointing at the same location as this snapshot.
99    pub fn reference(&self, firestore: Firestore) -> FirestoreResult<DocumentReference> {
100        DocumentReference::new(firestore, self.key.path().clone())
101    }
102    /// Converts this snapshot into a typed snapshot using the provided converter.
103    pub fn into_typed<C>(self, converter: Arc<C>) -> TypedDocumentSnapshot<C>
104    where
105        C: FirestoreDataConverter,
106    {
107        TypedDocumentSnapshot::new(self, converter)
108    }
109
110    /// Returns a typed snapshot by cloning the underlying data and converter.
111    pub fn to_typed<C>(&self, converter: Arc<C>) -> TypedDocumentSnapshot<C>
112    where
113        C: FirestoreDataConverter,
114    {
115        self.clone().into_typed(converter)
116    }
117}
118
119/// Document snapshot carrying a converter for typed access.
120#[derive(Clone)]
121pub struct TypedDocumentSnapshot<C>
122where
123    C: FirestoreDataConverter,
124{
125    base: DocumentSnapshot,
126    converter: Arc<C>,
127}
128
129impl<C> TypedDocumentSnapshot<C>
130where
131    C: FirestoreDataConverter,
132{
133    pub fn new(base: DocumentSnapshot, converter: Arc<C>) -> Self {
134        Self { base, converter }
135    }
136
137    pub fn exists(&self) -> bool {
138        self.base.exists()
139    }
140
141    pub fn id(&self) -> &str {
142        self.base.id()
143    }
144
145    pub fn metadata(&self) -> &SnapshotMetadata {
146        self.base.metadata()
147    }
148
149    pub fn from_cache(&self) -> bool {
150        self.base.from_cache()
151    }
152
153    pub fn has_pending_writes(&self) -> bool {
154        self.base.has_pending_writes()
155    }
156
157    pub fn reference(&self, firestore: Firestore) -> FirestoreResult<DocumentReference> {
158        self.base.reference(firestore)
159    }
160
161    pub fn raw(&self) -> &DocumentSnapshot {
162        &self.base
163    }
164
165    pub fn into_raw(self) -> DocumentSnapshot {
166        self.base
167    }
168
169    /// Returns the typed model using the embedded converter.
170    pub fn data(&self) -> FirestoreResult<Option<C::Model>> {
171        match self.base.map_value() {
172            Some(map) => self.converter.from_map(map).map(Some),
173            None => Ok(None),
174        }
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use crate::firestore::api::converter::{FirestoreDataConverter, PassthroughConverter};
182    use crate::firestore::model::DocumentKey;
183    use crate::firestore::value::ValueKind;
184    use std::collections::BTreeMap;
185
186    #[test]
187    fn metadata_flags() {
188        let meta = SnapshotMetadata::new(true, false);
189        assert!(meta.from_cache());
190        assert!(!meta.has_pending_writes());
191    }
192
193    #[test]
194    fn snapshot_reports_existence() {
195        let key = DocumentKey::from_string("cities/sf").unwrap();
196        let snapshot = DocumentSnapshot::new(key, None, SnapshotMetadata::default());
197        assert!(!snapshot.exists());
198    }
199
200    #[derive(Clone)]
201    struct NameConverter;
202
203    impl FirestoreDataConverter for NameConverter {
204        type Model = String;
205
206        fn to_map(&self, value: &Self::Model) -> FirestoreResult<BTreeMap<String, FirestoreValue>> {
207            let mut map = BTreeMap::new();
208            map.insert("name".to_string(), FirestoreValue::from_string(value));
209            Ok(map)
210        }
211
212        fn from_map(&self, value: &MapValue) -> FirestoreResult<Self::Model> {
213            match value.fields().get("name").and_then(|val| match val.kind() {
214                ValueKind::String(s) => Some(s.clone()),
215                _ => None,
216            }) {
217                Some(name) => Ok(name),
218                None => Err(crate::firestore::error::invalid_argument(
219                    "missing name field",
220                )),
221            }
222        }
223    }
224
225    #[test]
226    fn typed_snapshot_uses_converter() {
227        let key = DocumentKey::from_string("cities/sf").unwrap();
228        let mut map = BTreeMap::new();
229        map.insert(
230            "name".to_string(),
231            FirestoreValue::from_string("San Francisco"),
232        );
233        let snapshot = DocumentSnapshot::new(
234            key,
235            Some(MapValue::new(map)),
236            SnapshotMetadata::new(false, false),
237        );
238
239        let typed = snapshot.into_typed(Arc::new(NameConverter));
240        let name = typed.data().unwrap();
241        assert_eq!(name.as_deref(), Some("San Francisco"));
242    }
243
244    #[test]
245    fn passthrough_converter_roundtrip() {
246        let key = DocumentKey::from_string("cities/sf").unwrap();
247        let mut map = BTreeMap::new();
248        map.insert("name".to_string(), FirestoreValue::from_string("SF"));
249        let snapshot = DocumentSnapshot::new(
250            key,
251            Some(MapValue::new(map.clone())),
252            SnapshotMetadata::default(),
253        );
254
255        let typed = snapshot.into_typed(Arc::new(PassthroughConverter::default()));
256        let raw = typed.data().unwrap().unwrap();
257        assert_eq!(raw.get("name"), map.get("name"));
258    }
259}