use crate::auth::AuthInner;
use crate::error::FirebaseError;
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::sync::{Arc, Weak};
use tokio::sync::RwLock;
static APP_INSTANCES: Lazy<RwLock<HashMap<String, App>>> =
Lazy::new(|| RwLock::new(HashMap::new()));
#[derive(Clone)]
pub struct App {
inner: Arc<AppInner>,
}
struct AppInner {
name: String,
options: AppOptions,
auth_ref: RwLock<Option<Weak<AuthInner>>>,
}
#[derive(Clone, Debug)]
pub struct AppOptions {
pub api_key: String,
pub project_id: String,
pub app_name: Option<String>,
}
impl App {
pub async fn create(options: AppOptions) -> Result<Self, FirebaseError> {
if options.api_key.is_empty() {
return Err(FirebaseError::ApiKeyNotConfigured);
}
if options.project_id.is_empty() {
return Err(FirebaseError::Internal(
"Project ID cannot be empty".to_string(),
));
}
let name = match options.app_name.clone() {
None => "[DEFAULT]".to_string(),
Some(n) => n,
};
let mut instances = APP_INSTANCES.write().await;
if let Some(app) = instances.get(&name) {
return Ok(app.clone());
}
let app = App {
inner: Arc::new(AppInner {
name: name.clone(),
options,
auth_ref: RwLock::new(None),
}),
};
instances.insert(name, app.clone());
Ok(app)
}
pub async fn get_instance() -> Result<Self, FirebaseError> {
let instances = APP_INSTANCES.read().await;
instances.get("[DEFAULT]").cloned().ok_or_else(|| {
FirebaseError::Internal(
"Default Firebase App not initialized. Call App::create() first.".to_string(),
)
})
}
pub async fn get_instance_with_name(name: &str) -> Result<Self, FirebaseError> {
let instances = APP_INSTANCES.read().await;
instances.get(name).cloned().ok_or_else(|| {
FirebaseError::Internal(format!(
"Firebase App '{}' not found. Call App::create() first.",
name
))
})
}
pub fn name(&self) -> &str {
&self.inner.name
}
pub fn options(&self) -> &AppOptions {
&self.inner.options
}
pub(crate) async fn register_auth(&self, auth_inner: Weak<AuthInner>) {
*self.inner.auth_ref.write().await = Some(auth_inner);
}
#[allow(dead_code)]
pub(crate) async fn unregister_auth(&self) {
*self.inner.auth_ref.write().await = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_create_app() {
let options = AppOptions {
api_key: "test-api-key".to_string(),
project_id: "test-project".to_string(),
app_name: Some("test-app".to_string()),
};
let app = App::create(options).await.expect("Failed to create app");
assert_eq!(app.name(), "test-app");
}
#[tokio::test]
async fn test_create_app_singleton() {
let options = AppOptions {
api_key: "test-api-key-2".to_string(),
project_id: "test-project-2".to_string(),
app_name: Some("test-app-2".to_string()),
};
let app1 = App::create(options.clone())
.await
.expect("Failed to create app");
let app2 = App::create(options).await.expect("Failed to create app");
assert_eq!(app1.name(), app2.name());
}
#[tokio::test]
async fn test_empty_api_key_error() {
let options = AppOptions {
api_key: "".to_string(),
project_id: "test-project".to_string(),
app_name: None,
};
let result = App::create(options).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_default_app_name() {
let options = AppOptions {
api_key: "test-api-key-3".to_string(),
project_id: "test-project-3".to_string(),
app_name: None,
};
let app = App::create(options).await.expect("Failed to create app");
assert_eq!(app.name(), "[DEFAULT]");
}
}