use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct FirebaseConfig {
pub api_key: String,
pub app_id: Option<String>,
pub project_id: Option<String>,
pub project_number: Option<String>,
pub gcm_sender_id: Option<String>,
pub storage_bucket: Option<String>,
pub database_url: Option<String>,
}
#[derive(Debug, Default)]
#[allow(dead_code)]
pub struct FirebaseProjectInfo {
pub project_id: Option<String>,
pub project_number: Option<String>,
pub authorized_domains: Vec<String>,
pub enabled_providers: Vec<String>,
pub signup_disabled: Option<bool>,
pub anonymous_auth_enabled: Option<bool>,
pub email_auth_enabled: Option<bool>,
pub realtime_db_public_read: Option<bool>,
pub realtime_db_public_write: Option<bool>,
pub storage_bucket: Option<String>,
pub storage_public: Option<bool>,
pub fcm_enabled: Option<bool>,
pub fcm_token_obtained: Option<bool>,
pub fcm_topics_accessible: Option<bool>,
pub installations_api_enabled: Option<bool>,
pub api_restrictions: Vec<String>,
pub raw_errors: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct TestResult {
pub success: bool,
pub status_code: Option<u16>,
pub error: Option<String>,
pub detail: Option<String>,
pub extra: HashMap<String, String>,
}
impl TestResult {
pub fn ok(status_code: u16, detail: impl Into<String>) -> Self {
Self {
success: true,
status_code: Some(status_code),
error: None,
detail: Some(detail.into()),
extra: HashMap::new(),
}
}
pub fn fail(status_code: Option<u16>, error: impl Into<String>, detail: Option<String>) -> Self {
Self {
success: false,
status_code,
error: Some(error.into()),
detail,
extra: HashMap::new(),
}
}
pub fn with_extra(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.extra.insert(key.into(), value.into());
self
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SavedConfig {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub package: Option<String>,
pub api_key: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub app_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub project_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub gcm_sender_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub storage_bucket: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub database_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub saved_at: Option<String>,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct SavedConfigStore {
pub configs: indexmap::IndexMap<String, SavedConfig>,
}
pub fn config_file_path() -> PathBuf {
let base = dirs_next().join("flintbase");
base.join("configs.json")
}
pub fn load_saved_configs() -> SavedConfigStore {
let path = config_file_path();
if !path.exists() {
return SavedConfigStore::default();
}
match std::fs::read_to_string(&path) {
Ok(content) => serde_json::from_str(&content).unwrap_or_else(|e| {
eprintln!(
"Warning: Failed to parse {}: {}\nUsing empty config store.",
path.display(),
e
);
SavedConfigStore::default()
}),
Err(e) => {
eprintln!(
"Warning: Failed to read {}: {}\nUsing empty config store.",
path.display(),
e
);
SavedConfigStore::default()
}
}
}
pub fn save_config_store(store: &SavedConfigStore) -> anyhow::Result<()> {
let path = config_file_path();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let json = serde_json::to_string_pretty(store)?;
std::fs::write(&path, json)?;
Ok(())
}
pub fn save_firebase_configs(
package: &str,
configs: &[FirebaseConfig],
) -> anyhow::Result<usize> {
let mut store = load_saved_configs();
let now = chrono_now();
let count = configs.len();
for (i, cfg) in configs.iter().enumerate() {
let key = if count == 1 {
package.to_string()
} else {
format!("{}.{}", package, i + 1)
};
let name = if count == 1 {
package.to_string()
} else {
format!("{} (key {})", package, i + 1)
};
store.configs.insert(
key,
SavedConfig {
name,
package: Some(package.to_string()),
api_key: cfg.api_key.clone(),
app_id: cfg.app_id.clone(),
project_id: cfg.project_id.clone(),
gcm_sender_id: cfg.gcm_sender_id.clone(),
storage_bucket: cfg.storage_bucket.clone(),
database_url: cfg.database_url.clone(),
saved_at: Some(now.clone()),
},
);
}
save_config_store(&store)?;
Ok(count)
}
impl SavedConfig {
pub fn to_firebase_config(&self) -> FirebaseConfig {
FirebaseConfig {
api_key: self.api_key.clone(),
app_id: self.app_id.clone(),
project_id: self.project_id.clone(),
project_number: None,
gcm_sender_id: self.gcm_sender_id.clone(),
storage_bucket: self.storage_bucket.clone(),
database_url: self.database_url.clone(),
}
}
}
fn dirs_next() -> PathBuf {
if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
return PathBuf::from(xdg);
}
if let Ok(home) = std::env::var("HOME") {
return PathBuf::from(home).join(".config");
}
PathBuf::from(".config")
}
fn chrono_now() -> String {
let output = std::process::Command::new("date")
.arg("-u")
.arg("+%Y-%m-%dT%H:%M:%SZ")
.output();
match output {
Ok(o) if o.status.success() => {
String::from_utf8_lossy(&o.stdout).trim().to_string()
}
_ => String::from("unknown"),
}
}