firebase_rs_sdk/firestore/api/
database.rs

1use std::sync::{Arc, LazyLock};
2
3use crate::app;
4use crate::app::api::{get_app, register_version};
5use crate::app::FirebaseApp;
6use crate::app::SDK_VERSION;
7use crate::component::types::{
8    ComponentError, DynService, InstanceFactoryOptions, InstantiationMode,
9};
10use crate::component::{Component, ComponentType};
11use crate::firestore::constants::FIRESTORE_COMPONENT_NAME;
12use crate::firestore::error::{
13    internal_error, invalid_argument, missing_project_id, FirestoreResult,
14};
15use crate::firestore::model::{DatabaseId, ResourcePath};
16
17use super::reference::{CollectionReference, DocumentReference};
18
19#[derive(Clone, Debug)]
20pub struct Firestore {
21    inner: Arc<FirestoreInner>,
22}
23
24#[derive(Debug)]
25struct FirestoreInner {
26    app: FirebaseApp,
27    database_id: DatabaseId,
28}
29
30impl Firestore {
31    pub(crate) fn new(app: FirebaseApp, database_id: DatabaseId) -> Self {
32        let inner = FirestoreInner { app, database_id };
33        Self {
34            inner: Arc::new(inner),
35        }
36    }
37
38    /// Returns the `FirebaseApp` this Firestore instance is scoped to.
39    pub fn app(&self) -> &FirebaseApp {
40        &self.inner.app
41    }
42
43    /// The fully qualified database identifier (project + database name).
44    pub fn database_id(&self) -> &DatabaseId {
45        &self.inner.database_id
46    }
47
48    /// Creates a `CollectionReference` pointing at `path`.
49    ///
50    /// The path is interpreted relative to the Firestore root using forward
51    /// slashes to separate segments (e.g. `"users/alovelace/repos"`).
52    pub fn collection(&self, path: &str) -> FirestoreResult<CollectionReference> {
53        let resource = ResourcePath::from_string(path)?;
54        CollectionReference::new(self.clone(), resource)
55    }
56
57    /// Creates a `DocumentReference` pointing at `path`.
58    ///
59    /// The path must contain an even number of segments (collection/doc pairs).
60    pub fn doc(&self, path: &str) -> FirestoreResult<DocumentReference> {
61        let resource = ResourcePath::from_string(path)?;
62        DocumentReference::new(self.clone(), resource)
63    }
64
65    /// Clones a Firestore handle that has been wrapped in an `Arc`.
66    pub fn from_arc(arc: Arc<Self>) -> Self {
67        arc.as_ref().clone()
68    }
69
70    /// Returns the project identifier backing this database.
71    pub fn project_id(&self) -> &str {
72        self.inner.database_id.project_id()
73    }
74
75    /// Returns the logical database name (usually `"(default)"`).
76    pub fn database(&self) -> &str {
77        self.inner.database_id.database()
78    }
79}
80
81static FIRESTORE_COMPONENT: LazyLock<()> = LazyLock::new(|| {
82    let component = Component::new(
83        FIRESTORE_COMPONENT_NAME,
84        Arc::new(firestore_factory),
85        ComponentType::Public,
86    )
87    .with_instantiation_mode(InstantiationMode::Lazy)
88    .with_multiple_instances(true);
89
90    let _ = app::registry::register_component(component);
91});
92
93fn firestore_factory(
94    container: &crate::component::ComponentContainer,
95    options: InstanceFactoryOptions,
96) -> Result<DynService, ComponentError> {
97    let app = container.root_service::<FirebaseApp>().ok_or_else(|| {
98        ComponentError::InitializationFailed {
99            name: FIRESTORE_COMPONENT_NAME.to_string(),
100            reason: "Firebase app not attached to component container".to_string(),
101        }
102    })?;
103
104    let database_id = match options.instance_identifier.as_deref() {
105        Some(identifier) if !identifier.is_empty() => parse_database_identifier(&app, identifier)
106            .map_err(|err| {
107            ComponentError::InitializationFailed {
108                name: FIRESTORE_COMPONENT_NAME.to_string(),
109                reason: err.to_string(),
110            }
111        })?,
112        _ => DatabaseId::from_app(&app).map_err(|err| ComponentError::InitializationFailed {
113            name: FIRESTORE_COMPONENT_NAME.to_string(),
114            reason: err.to_string(),
115        })?,
116    };
117
118    let firestore = Firestore::new((*app).clone(), database_id);
119
120    register_version("@firebase/firestore", SDK_VERSION, None);
121
122    Ok(Arc::new(firestore) as DynService)
123}
124
125fn parse_database_identifier(app: &FirebaseApp, identifier: &str) -> FirestoreResult<DatabaseId> {
126    let options = app.options();
127    let project_id = options.project_id.clone().ok_or_else(missing_project_id)?;
128
129    if identifier.starts_with("projects/") {
130        let segments: Vec<_> = identifier.split('/').collect();
131        if segments.len() == 4 && segments[0] == "projects" && segments[2] == "databases" {
132            return Ok(DatabaseId::new(segments[1], segments[3]));
133        }
134        return Err(invalid_argument(
135            "Database identifier must follow projects/{project}/databases/{database}",
136        ));
137    }
138
139    Ok(DatabaseId::new(project_id, identifier))
140}
141
142fn ensure_registered() {
143    LazyLock::force(&FIRESTORE_COMPONENT);
144}
145
146pub fn register_firestore_component() {
147    ensure_registered();
148}
149
150/// Resolves (or lazily instantiates) the Firestore service for the provided app.
151///
152/// When `app` is `None` the default Firebase app is used. Multiple calls with
153/// the same app yield the same shared `Arc<Firestore>` handle.
154pub fn get_firestore(app: Option<FirebaseApp>) -> FirestoreResult<Arc<Firestore>> {
155    ensure_registered();
156    let app = match app {
157        Some(app) => app,
158        None => get_app(None).map_err(|err| internal_error(err.to_string()))?,
159    };
160
161    let provider = app::registry::get_provider(&app, FIRESTORE_COMPONENT_NAME);
162    provider
163        .get_immediate_with_options::<Firestore>(None, false)
164        .map_err(|err| internal_error(err.to_string()))?
165        .ok_or_else(|| internal_error("Failed to obtain Firestore instance"))
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use crate::app::api::initialize_app;
172    use crate::app::{FirebaseAppSettings, FirebaseOptions};
173
174    fn unique_settings() -> FirebaseAppSettings {
175        use std::sync::atomic::{AtomicUsize, Ordering};
176        static COUNTER: AtomicUsize = AtomicUsize::new(0);
177        FirebaseAppSettings {
178            name: Some(format!(
179                "firestore-api-{}",
180                COUNTER.fetch_add(1, Ordering::SeqCst)
181            )),
182            ..Default::default()
183        }
184    }
185
186    #[test]
187    fn get_firestore_registers_component() {
188        let options = FirebaseOptions {
189            project_id: Some("project".into()),
190            ..Default::default()
191        };
192        let app = initialize_app(options, Some(unique_settings())).unwrap();
193        let firestore = get_firestore(Some(app)).unwrap();
194        assert_eq!(firestore.project_id(), "project");
195        assert_eq!(firestore.database(), "(default)");
196    }
197
198    #[test]
199    fn custom_database_identifier() {
200        register_firestore_component();
201        let options = FirebaseOptions {
202            project_id: Some("project".into()),
203            ..Default::default()
204        };
205        let app = initialize_app(options, Some(unique_settings())).unwrap();
206        let provider = app::registry::get_provider(&app, FIRESTORE_COMPONENT_NAME);
207        let instance = provider
208            .initialize::<Firestore>(
209                serde_json::Value::Null,
210                Some("projects/project/databases/custom"),
211            )
212            .unwrap();
213        assert_eq!(instance.database(), "custom");
214    }
215}