use std::collections::HashMap;
use std::path::Path;
use crate::serde_json::Value as JsonValue;
use crate::storage::UnifiedStore;
use super::config_matrix::{default_for, MATRIX};
pub fn collect_env_overrides() -> HashMap<String, String> {
let mut out = HashMap::new();
for entry in MATRIX {
let env_name = env_name_for(entry.key);
if let Ok(raw) = std::env::var(&env_name) {
if !raw.is_empty() {
out.insert(entry.key.to_string(), raw);
}
}
}
out
}
pub fn env_name_for(key: &str) -> String {
format!("REDDB_{}", key.to_ascii_uppercase().replace('.', "_"))
}
pub fn config_file_path() -> String {
std::env::var("REDDB_CONFIG_FILE").unwrap_or_else(|_| "/etc/reddb/config.json".to_string())
}
pub fn apply_config_file(store: &UnifiedStore, path: &str) -> usize {
let p = Path::new(path);
if !p.exists() {
return 0;
}
let raw = match std::fs::read_to_string(p) {
Ok(s) => s,
Err(err) => {
tracing::warn!(path = %path, error = %err, "reading config overlay file");
return 0;
}
};
let parsed: JsonValue = match crate::serde_json::from_str(&raw) {
Ok(v) => v,
Err(err) => {
tracing::warn!(
path = %path,
error = %err,
"parsing config overlay file as JSON — ignoring"
);
return 0;
}
};
let JsonValue::Object(_) = &parsed else {
tracing::warn!(
path = %path,
"config overlay must be a JSON object — ignoring"
);
return 0;
};
let mut written = 0;
let mut flat: Vec<(String, JsonValue)> = Vec::new();
flatten_json("", &parsed, &mut flat);
for (key, value) in flat {
if key_already_present(store, &key) {
continue;
}
store.set_config_tree(&key, &value);
crate::telemetry::operator_event::OperatorEvent::ConfigChanged {
key: key.clone(),
old_value: String::new(),
new_value: format!("{value}"),
changed_by: format!("config_overlay::{path}"),
}
.emit_global();
written += 1;
}
written
}
fn key_already_present(store: &UnifiedStore, key: &str) -> bool {
let Some(manager) = store.get_collection("red_config") else {
return false;
};
let mut found = false;
manager.for_each_entity(|entity| {
if let Some(row) = entity.data.as_row() {
if let Some(crate::storage::schema::Value::Text(s)) = row.get_field("key") {
if s.as_ref() == key {
found = true;
return false;
}
}
}
true
});
found
}
fn flatten_json(prefix: &str, value: &JsonValue, out: &mut Vec<(String, JsonValue)>) {
match value {
JsonValue::Object(map) => {
for (k, v) in map {
let key = if prefix.is_empty() {
k.clone()
} else {
format!("{prefix}.{k}")
};
flatten_json(&key, v, out);
}
}
_ if !prefix.is_empty() => {
out.push((prefix.to_string(), value.clone()));
}
_ => {
}
}
}
pub fn coerce_env_value(key: &str, raw: &str) -> Option<crate::storage::schema::Value> {
use crate::storage::schema::Value;
let default = default_for(key)?;
match default {
JsonValue::Bool(_) => match raw.to_ascii_lowercase().as_str() {
"true" | "1" | "on" | "yes" => Some(Value::Boolean(true)),
"false" | "0" | "off" | "no" => Some(Value::Boolean(false)),
_ => None,
},
JsonValue::Number(n) => {
if n.fract().abs() < f64::EPSILON {
raw.parse::<u64>().ok().map(Value::UnsignedInteger)
} else {
raw.parse::<f64>().ok().map(Value::Float)
}
}
JsonValue::String(_) => Some(Value::text(raw.to_string())),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn env_name_follows_convention() {
assert_eq!(env_name_for("durability.mode"), "REDDB_DURABILITY_MODE");
assert_eq!(
env_name_for("storage.btree.lehman_yao"),
"REDDB_STORAGE_BTREE_LEHMAN_YAO"
);
assert_eq!(
env_name_for("storage.bulk_insert.max_buffered_rows"),
"REDDB_STORAGE_BULK_INSERT_MAX_BUFFERED_ROWS"
);
}
#[test]
fn coerce_bool_accepts_common_forms() {
use crate::storage::schema::Value;
assert_eq!(
coerce_env_value("concurrency.locking.enabled", "true"),
Some(Value::Boolean(true))
);
assert_eq!(
coerce_env_value("concurrency.locking.enabled", "FALSE"),
Some(Value::Boolean(false))
);
assert_eq!(
coerce_env_value("concurrency.locking.enabled", "1"),
Some(Value::Boolean(true))
);
assert_eq!(
coerce_env_value("concurrency.locking.enabled", "off"),
Some(Value::Boolean(false))
);
assert!(coerce_env_value("concurrency.locking.enabled", "maybe").is_none());
}
#[test]
fn coerce_number_rejects_garbage() {
use crate::storage::schema::Value;
assert_eq!(
coerce_env_value("storage.wal.max_interval_ms", "25"),
Some(Value::UnsignedInteger(25))
);
assert!(coerce_env_value("storage.wal.max_interval_ms", "fast").is_none());
}
#[test]
fn unknown_key_returns_none() {
assert!(coerce_env_value("nonexistent.key", "42").is_none());
}
}