use crate::config::builder::Builder;
use crate::config::error::Error;
use crate::config::source::Source;
use serde::de::DeserializeOwned;
use serde_json::Value;
use std::sync::{Arc, RwLock};
pub struct Store {
sources: Vec<Arc<dyn Source>>,
cache: RwLock<Value>,
}
impl Store {
pub async fn from_builder(builder: Builder) -> Result<Self, Error> {
let sources = builder.build_sources();
let merged = merge_all(&sources).await?;
Ok(Self {
sources,
cache: RwLock::new(merged),
})
}
pub async fn refresh(&self) -> Result<(), Error> {
let merged = merge_all(&self.sources).await?;
let mut cache = self
.cache
.write()
.map_err(|e| Error::Other(format!("cache lock poisoned: {e}")))?;
*cache = merged;
Ok(())
}
#[doc = "let db_host = store.get_str(\"db.host\");"]
pub fn get_str(&self, key: &str) -> Option<String> {
self.navigate(key)
.and_then(|v| v.as_str().map(String::from))
}
#[doc = "let port = store.get_u64(\"port\");"]
pub fn get_u64(&self, key: &str) -> Option<u64> {
self.navigate(key).and_then(|v| v.as_u64())
}
pub fn get_bool(&self, key: &str) -> Option<bool> {
self.navigate(key).and_then(|v| v.as_bool())
}
#[doc = "let db: DbConfig = store.get(\"db\").unwrap();"]
pub fn get<T: DeserializeOwned>(&self, key: &str) -> Result<T, Error> {
let val = self
.navigate(key)
.ok_or_else(|| Error::NotFound(key.to_string()))?;
serde_json::from_value(val).map_err(Error::from)
}
pub fn full_config(&self) -> Value {
self.cache.read().map(|c| c.clone()).unwrap_or_default()
}
fn navigate(&self, key: &str) -> Option<Value> {
let cache = self.cache.read().ok()?;
let mut current: &Value = &cache;
for segment in key.split('.') {
current = current.get(segment)?;
}
Some(current.clone())
}
}
fn deep_merge(target: &mut Value, other: &Value) {
match (target, other) {
(Value::Object(t), Value::Object(o)) => {
for (k, v) in o {
if let Some(existing) = t.get_mut(k) {
deep_merge(existing, v);
} else {
t.insert(k.clone(), v.clone());
}
}
}
(t, o) => *t = o.clone(),
}
}
async fn merge_all(sources: &[Arc<dyn Source>]) -> Result<Value, Error> {
let mut merged = Value::Object(serde_json::Map::new());
for source in sources.iter().rev() {
let loaded = source.load().await?;
deep_merge(&mut merged, &loaded);
}
Ok(merged)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::source::test_helpers::{write_temp, EnvGuard};
#[tokio::test]
async fn loads_and_merges_json_config() {
let base = write_temp(r#"{"host": "localhost", "port": 5432, "debug": false}"#);
let override_file = write_temp(r#"{"host": "prod.example.com", "debug": true}"#);
let store = Builder::default()
.json(base.path(), 100)
.json(override_file.path(), 50)
.build()
.await
.unwrap();
assert_eq!(store.get_str("host").unwrap(), "prod.example.com");
assert_eq!(store.get_u64("port").unwrap(), 5432);
assert!(store.get_bool("debug").unwrap());
}
#[tokio::test]
async fn deep_merges_nested_objects() {
let base = write_temp(r#"{"db": {"host": "localhost", "port": 5432}}"#);
let override_file = write_temp(r#"{"db": {"host": "db.example.com"}}"#);
let store = Builder::default()
.json(base.path(), 100)
.json(override_file.path(), 50)
.build()
.await
.unwrap();
assert_eq!(store.get_str("db.host").unwrap(), "db.example.com");
assert_eq!(store.get_u64("db.port").unwrap(), 5432);
}
#[tokio::test]
async fn refresh_reloads_from_files() {
let f = write_temp(r#"{"version": 1}"#);
let store = Builder::default().json(f.path(), 0).build().await.unwrap();
assert_eq!(store.get_u64("version").unwrap(), 1);
std::fs::write(f.path(), r#"{"version": 2}"#).unwrap();
store.refresh().await.unwrap();
assert_eq!(store.get_u64("version").unwrap(), 2);
}
#[tokio::test]
async fn deserializes_into_struct() {
use serde::Deserialize;
#[derive(Deserialize, Debug, PartialEq)]
struct DbConfig {
host: String,
port: u16,
}
let json_file = write_temp(r#"{"db": {"host": "pg.example.com", "port": 5432}}"#);
let store = Builder::default()
.json(json_file.path(), 0)
.build()
.await
.unwrap();
let db: DbConfig = store.get("db").unwrap();
assert_eq!(db.host, "pg.example.com");
assert_eq!(db.port, 5432);
}
#[tokio::test]
async fn missing_key_returns_none() {
let f = write_temp(r#"{"exists": true}"#);
let store = Builder::default().json(f.path(), 0).build().await.unwrap();
assert!(store.get_str("nonexistent").is_none());
assert!(store.get_u64("nonexistent").is_none());
assert!(store.get_bool("nonexistent").is_none());
}
#[tokio::test]
async fn missing_key_get_typed_returns_error() {
let f = write_temp(r#"{"exists": true}"#);
let store = Builder::default().json(f.path(), 0).build().await.unwrap();
let result: Result<Value, _> = store.get("nonexistent");
assert!(result.is_err());
}
#[tokio::test]
async fn builds_from_empty_builder() {
let store = Builder::default().build().await.unwrap();
assert_eq!(store.full_config(), Value::Object(serde_json::Map::new()));
}
#[tokio::test]
async fn env_and_file_merge_correctly() {
let _guard = EnvGuard::set("CK_T5_HOST", "from-env");
let file = write_temp(r#"{"host": "from-file", "port": 8080}"#);
let store = Builder::default()
.json(file.path(), 100) .env("CK_T5_", "__", 10) .build()
.await
.unwrap();
assert_eq!(store.get_str("host").unwrap(), "from-env");
assert_eq!(store.get_u64("port").unwrap(), 8080);
}
#[tokio::test]
async fn full_config_returns_complete_json() {
let f = write_temp(r#"{"key": "value"}"#);
let store = Builder::default().json(f.path(), 0).build().await.unwrap();
let full = store.full_config();
assert_eq!(full["key"], "value");
}
}