firebase_rs_sdk/app/
types.rs

1use std::collections::HashMap;
2use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
3use std::sync::{Arc, Mutex};
4
5use crate::app::errors::{AppError, AppResult};
6use crate::component::constants::DEFAULT_ENTRY_NAME;
7use crate::component::types::DynService;
8use crate::component::{Component, ComponentContainer};
9use crate::platform::environment;
10use crate::platform::runtime::spawn_detached;
11
12#[allow(dead_code)]
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct VersionService {
15    pub library: String,
16    pub version: String,
17}
18
19#[allow(dead_code)]
20pub trait PlatformLoggerService: Send + Sync {
21    fn platform_info_string(&self) -> String;
22}
23
24#[cfg_attr(
25    all(feature = "wasm-web", target_arch = "wasm32"),
26    async_trait::async_trait(?Send)
27)]
28#[cfg_attr(
29    not(all(feature = "wasm-web", target_arch = "wasm32")),
30    async_trait::async_trait
31)]
32pub trait HeartbeatService: Send + Sync {
33    async fn trigger_heartbeat(&self) -> AppResult<()>;
34    #[allow(dead_code)]
35    async fn heartbeats_header(&self) -> AppResult<Option<String>>;
36}
37
38#[cfg_attr(
39    all(feature = "wasm-web", target_arch = "wasm32"),
40    async_trait::async_trait(?Send)
41)]
42#[cfg_attr(
43    not(all(feature = "wasm-web", target_arch = "wasm32")),
44    async_trait::async_trait
45)]
46pub trait HeartbeatStorage: Send + Sync {
47    async fn read(&self) -> AppResult<HeartbeatsInStorage>;
48    async fn overwrite(&self, value: &HeartbeatsInStorage) -> AppResult<()>;
49}
50
51#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
52pub struct HeartbeatsInStorage {
53    pub last_sent_heartbeat_date: Option<String>,
54    pub heartbeats: Vec<SingleDateHeartbeat>,
55}
56
57#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
58pub struct SingleDateHeartbeat {
59    pub agent: String,
60    pub date: String,
61}
62
63#[derive(Clone, Debug, Default, PartialEq, Eq)]
64pub struct FirebaseOptions {
65    pub api_key: Option<String>,
66    pub auth_domain: Option<String>,
67    pub database_url: Option<String>,
68    pub project_id: Option<String>,
69    pub storage_bucket: Option<String>,
70    pub messaging_sender_id: Option<String>,
71    pub app_id: Option<String>,
72    pub measurement_id: Option<String>,
73}
74
75#[derive(Clone, Debug, PartialEq, Eq, Default)]
76pub struct FirebaseAppSettings {
77    pub name: Option<String>,
78    pub automatic_data_collection_enabled: Option<bool>,
79}
80
81#[derive(Clone, Debug, PartialEq, Eq)]
82pub struct FirebaseAppConfig {
83    pub name: Arc<str>,
84    pub automatic_data_collection_enabled: bool,
85}
86
87#[derive(Clone, Debug, PartialEq, Eq, Default)]
88pub struct FirebaseServerAppSettings {
89    pub automatic_data_collection_enabled: Option<bool>,
90    pub auth_id_token: Option<String>,
91    pub app_check_token: Option<String>,
92    pub release_on_deref: Option<bool>,
93}
94
95#[derive(Clone)]
96pub struct FirebaseApp {
97    inner: Arc<FirebaseAppInner>,
98}
99
100struct FirebaseAppInner {
101    options: FirebaseOptions,
102    config: FirebaseAppConfig,
103    automatic_data_collection_enabled: Mutex<bool>,
104    is_deleted: AtomicBool,
105    container: ComponentContainer,
106}
107
108impl FirebaseApp {
109    /// Creates a new `FirebaseApp` from options, config, and the component container.
110    pub fn new(
111        options: FirebaseOptions,
112        config: FirebaseAppConfig,
113        container: ComponentContainer,
114    ) -> Self {
115        let automatic = config.automatic_data_collection_enabled;
116        let inner = Arc::new(FirebaseAppInner {
117            options,
118            config,
119            automatic_data_collection_enabled: Mutex::new(automatic),
120            is_deleted: AtomicBool::new(false),
121            container,
122        });
123        let app = Self {
124            inner: inner.clone(),
125        };
126        let dyn_service: DynService = Arc::new(app.clone());
127        app.inner.container.attach_root_service(dyn_service);
128        app
129    }
130
131    /// Returns the app's logical name.
132    pub fn name(&self) -> &str {
133        &self.inner.config.name
134    }
135
136    /// Provides a cloned copy of the original Firebase options.
137    pub fn options(&self) -> FirebaseOptions {
138        self.inner.options.clone()
139    }
140
141    /// Returns the configuration metadata associated with the app.
142    pub fn config(&self) -> FirebaseAppConfig {
143        self.inner.config.clone()
144    }
145
146    /// Indicates whether automatic data collection is currently enabled.
147    pub fn automatic_data_collection_enabled(&self) -> bool {
148        *self
149            .inner
150            .automatic_data_collection_enabled
151            .lock()
152            .unwrap_or_else(|poison| poison.into_inner())
153    }
154
155    /// Updates the automatic data collection flag for the app.
156    pub fn set_automatic_data_collection_enabled(&self, value: bool) {
157        *self
158            .inner
159            .automatic_data_collection_enabled
160            .lock()
161            .unwrap_or_else(|poison| poison.into_inner()) = value;
162    }
163
164    /// Exposes the component container for advanced service registration.
165    pub fn container(&self) -> ComponentContainer {
166        self.inner.container.clone()
167    }
168
169    /// Adds a lazily-initialized component to the app.
170    pub fn add_component(&self, component: Component) -> AppResult<()> {
171        self.check_destroyed()?;
172        self.inner
173            .container
174            .add_component(component)
175            .map_err(AppError::from)
176    }
177
178    /// Adds a component, replacing any existing implementation with the same name.
179    pub fn add_or_overwrite_component(&self, component: Component) -> AppResult<()> {
180        self.check_destroyed()?;
181        self.inner.container.add_or_overwrite_component(component);
182        Ok(())
183    }
184
185    /// Removes a cached service instance from the specified provider.
186    pub fn remove_service_instance(&self, name: &str, identifier: Option<&str>) {
187        let provider = self.inner.container.get_provider(name);
188        if let Some(id) = identifier {
189            provider.clear_instance(id);
190        } else {
191            provider.clear_instance(DEFAULT_ENTRY_NAME);
192        }
193    }
194
195    /// Returns whether the app has been explicitly deleted.
196    pub fn is_deleted(&self) -> bool {
197        self.inner.is_deleted.load(Ordering::SeqCst)
198    }
199
200    /// Marks the app as deleted (internal use).
201    pub fn set_is_deleted(&self, value: bool) {
202        self.inner.is_deleted.store(value, Ordering::SeqCst);
203    }
204
205    /// Verifies that the app has not been deleted before performing operations.
206    pub fn check_destroyed(&self) -> AppResult<()> {
207        if self.is_deleted() {
208            return Err(AppError::AppDeleted {
209                app_name: self.name().to_owned(),
210            });
211        }
212        Ok(())
213    }
214}
215
216impl std::fmt::Debug for FirebaseApp {
217    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218        f.debug_struct("FirebaseApp")
219            .field("name", &self.name())
220            .field(
221                "automatic_data_collection_enabled",
222                &self.automatic_data_collection_enabled(),
223            )
224            .finish()
225    }
226}
227
228impl FirebaseAppConfig {
229    /// Creates a configuration value capturing the app name and data collection setting.
230    pub fn new(name: impl Into<String>, automatic: bool) -> Self {
231        Self {
232            name: to_arc_str(name),
233            automatic_data_collection_enabled: automatic,
234        }
235    }
236}
237
238#[derive(Clone)]
239pub struct FirebaseServerApp {
240    inner: Arc<FirebaseServerAppInner>,
241}
242
243struct FirebaseServerAppInner {
244    base: FirebaseApp,
245    settings: FirebaseServerAppSettings,
246    ref_count: AtomicUsize,
247    release_on_drop: AtomicBool,
248}
249
250impl FirebaseServerApp {
251    /// Wraps a `FirebaseApp` with server-specific settings and reference counting.
252    pub fn new(base: FirebaseApp, mut settings: FirebaseServerAppSettings) -> Self {
253        let release_on_drop = settings.release_on_deref.unwrap_or(false);
254        settings.release_on_deref = None;
255        base.set_is_deleted(false);
256
257        Self {
258            inner: Arc::new(FirebaseServerAppInner {
259                base,
260                settings,
261                ref_count: AtomicUsize::new(1),
262                release_on_drop: AtomicBool::new(release_on_drop),
263            }),
264        }
265    }
266
267    /// Returns the underlying base app instance.
268    pub fn base(&self) -> &FirebaseApp {
269        &self.inner.base
270    }
271
272    /// Returns the server-specific configuration for this app.
273    pub fn settings(&self) -> FirebaseServerAppSettings {
274        self.inner.settings.clone()
275    }
276
277    /// Convenience accessor for the app name.
278    pub fn name(&self) -> &str {
279        self.inner.base.name()
280    }
281
282    /// Increments the manual reference count.
283    pub fn inc_ref_count(&self) {
284        self.inner.ref_count.fetch_add(1, Ordering::SeqCst);
285    }
286
287    /// Decrements the reference count, returning the new value.
288    pub fn dec_ref_count(&self) -> usize {
289        self.inner.ref_count.fetch_sub(1, Ordering::SeqCst) - 1
290    }
291
292    /// Enables automatic cleanup when the server app is dropped.
293    pub fn set_release_on_drop(&self, enabled: bool) {
294        self.inner.release_on_drop.store(enabled, Ordering::SeqCst);
295    }
296
297    /// Indicates whether automatic cleanup is currently configured.
298    pub fn release_on_drop(&self) -> bool {
299        self.inner.release_on_drop.load(Ordering::SeqCst)
300    }
301}
302
303/// Returns `true` when the current target behaves like a browser environment.
304pub fn is_browser() -> bool {
305    environment::is_browser()
306}
307
308/// Returns `true` when the current target is a web worker environment.
309pub fn is_web_worker() -> bool {
310    environment::is_web_worker()
311}
312
313/// Provides default app options sourced from environment configuration when available.
314pub fn get_default_app_config() -> Option<FirebaseOptions> {
315    let map = environment::default_app_config_json()?;
316    map_to_options(&map)
317}
318
319/// Compares two `FirebaseOptions` instances for structural equality.
320pub fn deep_equal_options(a: &FirebaseOptions, b: &FirebaseOptions) -> bool {
321    a == b
322}
323
324#[derive(Clone, Debug)]
325pub struct FirebaseAuthTokenData {
326    pub access_token: String,
327}
328
329pub trait FirebaseServiceInternals: Send + Sync {
330    fn delete(&self) -> AppResult<()>;
331}
332
333#[allow(dead_code)]
334pub trait FirebaseService: Send + Sync {
335    fn app(&self) -> FirebaseApp;
336    fn internals(&self) -> Option<&dyn FirebaseServiceInternals> {
337        None
338    }
339}
340
341#[allow(dead_code)]
342pub type AppHook = Arc<dyn Fn(&str, &FirebaseApp) + Send + Sync>;
343
344#[allow(dead_code)]
345pub type FirebaseServiceFactory<T> = Arc<
346    dyn Fn(
347            &FirebaseApp,
348            Option<Arc<dyn Fn(&HashMap<String, serde_json::Value>) + Send + Sync>>,
349            Option<&str>,
350        ) -> T
351        + Send
352        + Sync,
353>;
354
355#[allow(dead_code)]
356pub type FirebaseServiceNamespace<T> = Arc<dyn Fn(Option<&FirebaseApp>) -> T + Send + Sync>;
357
358#[allow(dead_code)]
359pub trait FirebaseAppInternals: Send + Sync {
360    fn get_token(&self, refresh_token: bool) -> AppResult<Option<FirebaseAuthTokenData>>;
361    fn get_uid(&self) -> Option<String>;
362    fn add_auth_token_listener(&self, listener: Arc<dyn Fn(Option<String>) + Send + Sync>);
363    fn remove_auth_token_listener(&self, listener_id: usize);
364    fn log_event(
365        &self,
366        event_name: &str,
367        event_params: HashMap<String, serde_json::Value>,
368        global: bool,
369    );
370}
371
372/// Compares two app configs for equality.
373pub fn deep_equal_config(a: &FirebaseAppConfig, b: &FirebaseAppConfig) -> bool {
374    a == b
375}
376
377fn to_arc_str(value: impl Into<String>) -> Arc<str> {
378    Arc::from(value.into().into_boxed_str())
379}
380
381fn map_to_options(map: &serde_json::Map<String, serde_json::Value>) -> Option<FirebaseOptions> {
382    let mut options = FirebaseOptions::default();
383
384    options.api_key = string_value(map, "apiKey");
385    options.auth_domain = string_value(map, "authDomain");
386    options.database_url = string_value(map, "databaseURL");
387    options.project_id = string_value(map, "projectId");
388    options.storage_bucket = string_value(map, "storageBucket");
389    options.messaging_sender_id = string_value(map, "messagingSenderId");
390    options.app_id = string_value(map, "appId");
391    options.measurement_id = string_value(map, "measurementId");
392
393    if options.api_key.is_some()
394        || options.project_id.is_some()
395        || options.app_id.is_some()
396        || options.database_url.is_some()
397        || options.storage_bucket.is_some()
398        || options.messaging_sender_id.is_some()
399        || options.measurement_id.is_some()
400        || options.auth_domain.is_some()
401    {
402        Some(options)
403    } else {
404        None
405    }
406}
407
408fn string_value(map: &serde_json::Map<String, serde_json::Value>, key: &str) -> Option<String> {
409    map.get(key)
410        .and_then(|value| value.as_str())
411        .map(|value| value.to_string())
412}
413
414impl Drop for FirebaseServerApp {
415    fn drop(&mut self) {
416        if !self.release_on_drop() {
417            return;
418        }
419
420        if self.base().is_deleted() {
421            return;
422        }
423
424        let was_enabled = self.inner.release_on_drop.swap(false, Ordering::SeqCst);
425        if !was_enabled {
426            return;
427        }
428
429        let app = self.inner.base.clone();
430        spawn_detached(async move {
431            let _ = crate::app::api::delete_app(&app).await;
432        });
433    }
434}
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439    use std::sync::{LazyLock, Mutex};
440
441    static ENV_GUARD: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
442
443    #[test]
444    fn map_to_options_returns_some_when_fields_present() {
445        let mut map = serde_json::Map::new();
446        map.insert("apiKey".into(), serde_json::Value::String("foo".into()));
447        let options = map_to_options(&map).expect("options");
448        assert_eq!(options.api_key.as_deref(), Some("foo"));
449    }
450
451    #[test]
452    fn map_to_options_returns_none_for_empty_map() {
453        let map = serde_json::Map::new();
454        assert!(map_to_options(&map).is_none());
455    }
456
457    #[test]
458    fn get_default_app_config_reads_environment() {
459        let _guard = ENV_GUARD.lock().unwrap();
460
461        let key = "FIREBASE_CONFIG";
462        let previous = std::env::var(key).ok();
463        std::env::set_var(key, "{\"apiKey\":\"env-key\"}");
464
465        let options = get_default_app_config().expect("config");
466        assert_eq!(options.api_key.as_deref(), Some("env-key"));
467
468        match previous {
469            Some(value) => std::env::set_var(key, value),
470            None => std::env::remove_var(key),
471        }
472    }
473}