firebase_rs_sdk/firestore/api/
database.rs1use 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 pub fn app(&self) -> &FirebaseApp {
40 &self.inner.app
41 }
42
43 pub fn database_id(&self) -> &DatabaseId {
45 &self.inner.database_id
46 }
47
48 pub fn collection(&self, path: &str) -> FirestoreResult<CollectionReference> {
53 let resource = ResourcePath::from_string(path)?;
54 CollectionReference::new(self.clone(), resource)
55 }
56
57 pub fn doc(&self, path: &str) -> FirestoreResult<DocumentReference> {
61 let resource = ResourcePath::from_string(path)?;
62 DocumentReference::new(self.clone(), resource)
63 }
64
65 pub fn from_arc(arc: Arc<Self>) -> Self {
67 arc.as_ref().clone()
68 }
69
70 pub fn project_id(&self) -> &str {
72 self.inner.database_id.project_id()
73 }
74
75 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
150pub 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}