firebase_rs_sdk/platform/
environment.rs

1//! Shared runtime environment detection and default configuration helpers.
2
3use std::env;
4use std::fs;
5#[cfg(not(target_arch = "wasm32"))]
6use std::path::Path;
7
8use serde_json::{Map, Value};
9
10/// Returns the parsed `__FIREBASE_DEFAULTS__` object when available.
11fn firebase_defaults() -> Option<Value> {
12    defaults_from_env()
13        .or_else(defaults_from_path)
14        .or_else(defaults_from_global)
15}
16
17fn defaults_from_env() -> Option<Value> {
18    let raw = env::var("__FIREBASE_DEFAULTS__").ok()?;
19    parse_json_value(raw)
20}
21
22fn defaults_from_path() -> Option<Value> {
23    let path = env::var("__FIREBASE_DEFAULTS_PATH").ok()?;
24    let content = fs::read_to_string(path).ok()?;
25    parse_json_value(content)
26}
27
28#[cfg(all(target_arch = "wasm32", feature = "wasm-web"))]
29fn defaults_from_global() -> Option<Value> {
30    use wasm_bindgen::JsValue;
31
32    let global = js_sys::global();
33    let value = js_sys::Reflect::get(&global, &JsValue::from_str("__FIREBASE_DEFAULTS__")).ok()?;
34    if value.is_null() || value.is_undefined() {
35        return None;
36    }
37    let serialized = js_sys::JSON::stringify(&value).ok()?.as_string()?;
38    serde_json::from_str(&serialized).ok()
39}
40
41#[cfg(not(all(target_arch = "wasm32", feature = "wasm-web")))]
42fn defaults_from_global() -> Option<Value> {
43    None
44}
45
46fn parse_json_value(raw: String) -> Option<Value> {
47    serde_json::from_str::<Value>(&raw).ok()
48}
49
50fn parse_config_source(raw: &str) -> Option<Value> {
51    if let Ok(json) = serde_json::from_str::<Value>(raw) {
52        if json.is_object() {
53            return Some(json);
54        }
55    }
56
57    if let Some(path) = treat_as_path(raw) {
58        if let Ok(contents) = fs::read_to_string(&path) {
59            if let Ok(json) = serde_json::from_str::<Value>(&contents) {
60                if json.is_object() {
61                    return Some(json);
62                }
63            }
64        }
65    }
66
67    parse_key_value_config(raw)
68}
69
70#[cfg(not(target_arch = "wasm32"))]
71fn treat_as_path(raw: &str) -> Option<String> {
72    if raw.contains('=') {
73        return None;
74    }
75    let trimmed = raw.trim();
76    let path = Path::new(trimmed);
77    if path.exists() {
78        Some(trimmed.to_string())
79    } else {
80        None
81    }
82}
83
84#[cfg(target_arch = "wasm32")]
85fn treat_as_path(_raw: &str) -> Option<String> {
86    None
87}
88
89fn parse_key_value_config(raw: &str) -> Option<Value> {
90    let mut map = Map::new();
91    for entry in raw.split(',') {
92        let mut parts = entry.splitn(2, '=');
93        let key = parts.next()?.trim();
94        let value = parts.next()?.trim();
95        if key.is_empty() || value.is_empty() {
96            continue;
97        }
98        map.insert(key.to_string(), Value::String(value.to_string()));
99    }
100    if map.is_empty() {
101        None
102    } else {
103        Some(Value::Object(map))
104    }
105}
106
107fn firebase_config_from_env() -> Option<Value> {
108    if let Ok(raw) = env::var("FIREBASE_CONFIG") {
109        if let Some(value) = parse_config_source(&raw) {
110            return Some(value);
111        }
112    }
113
114    if let Ok(raw) = env::var("FIREBASE_OPTIONS") {
115        if let Some(value) = parse_config_source(&raw) {
116            return Some(value);
117        }
118    }
119
120    if let Ok(raw) = env::var("FIREBASE_WEBAPP_CONFIG") {
121        if let Some(value) = parse_config_source(&raw) {
122            return Some(value);
123        }
124    }
125
126    None
127}
128
129/// Retrieves the default app configuration as a JSON map when available.
130pub fn default_app_config_json() -> Option<Map<String, Value>> {
131    if let Some(defaults) = firebase_defaults() {
132        if let Some(config) = defaults.get("config").and_then(Value::as_object) {
133            return Some(config.clone());
134        }
135    }
136
137    firebase_config_from_env()?.as_object().cloned()
138}
139
140fn force_environment() -> Option<String> {
141    firebase_defaults()
142        .and_then(|defaults| defaults.get("forceEnvironment").cloned())
143        .or_else(|| env::var("FIREBASE_ENV_FORCE").ok().map(Value::String))
144        .and_then(|value| match value {
145            Value::String(text) => Some(text.to_lowercase()),
146            _ => None,
147        })
148}
149
150/// Returns `true` if the runtime should behave as a browser environment.
151pub fn is_browser() -> bool {
152    if let Some(forced) = force_environment() {
153        return forced == "browser";
154    }
155
156    #[cfg(all(target_arch = "wasm32", feature = "wasm-web"))]
157    {
158        use wasm_bindgen::JsCast;
159        js_sys::global().dyn_into::<web_sys::Window>().is_ok()
160    }
161
162    #[cfg(not(all(target_arch = "wasm32", feature = "wasm-web")))]
163    {
164        false
165    }
166}
167
168/// Returns `true` if the runtime appears to be a Web Worker.
169pub fn is_web_worker() -> bool {
170    if let Some(forced) = force_environment() {
171        if forced == "browser" {
172            return false;
173        }
174    }
175
176    #[cfg(all(target_arch = "wasm32", feature = "wasm-web"))]
177    {
178        use wasm_bindgen::JsCast;
179        js_sys::global()
180            .dyn_into::<web_sys::WorkerGlobalScope>()
181            .is_ok()
182    }
183
184    #[cfg(not(all(target_arch = "wasm32", feature = "wasm-web")))]
185    {
186        false
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn parses_key_value_configs() {
196        let value = parse_key_value_config("apiKey=foo,projectId=my-proj").unwrap();
197        let map = value.as_object().unwrap();
198        assert_eq!(map.get("apiKey").unwrap().as_str(), Some("foo"));
199        assert_eq!(map.get("projectId").unwrap().as_str(), Some("my-proj"));
200    }
201
202    #[test]
203    fn parse_config_source_accepts_files_and_json() {
204        let json = parse_config_source("{\"apiKey\":\"foo\"}").unwrap();
205        assert_eq!(json["apiKey"], "foo");
206
207        let mut path = std::env::temp_dir();
208        path.push(format!(
209            "firebase_rs_sdk_test_{}.json",
210            std::time::SystemTime::now()
211                .duration_since(std::time::UNIX_EPOCH)
212                .unwrap()
213                .as_nanos()
214        ));
215        fs::write(&path, "{\"projectId\":\"demo\"}").unwrap();
216        let path_str = path.to_string_lossy().to_string();
217        let file_json = parse_config_source(&path_str).unwrap();
218        assert_eq!(file_json["projectId"], "demo");
219        let _ = fs::remove_file(path);
220    }
221}