use std::sync::{Arc, RwLock, RwLockReadGuard};
use crate::{Config, ConfigError};
#[derive(Clone, Debug)]
pub struct ConfigHandle {
inner: Arc<RwLock<Config>>,
}
impl ConfigHandle {
pub fn new(config: Config) -> Self {
Self {
inner: Arc::new(RwLock::new(config)),
}
}
pub fn read(&self) -> RwLockReadGuard<'_, Config> {
self.inner.read().unwrap_or_else(|e| e.into_inner())
}
pub fn reload(&self) -> Result<(), ConfigError> {
self.inner.write()
.unwrap_or_else(|e| e.into_inner())
.reload()
}
pub fn str(&self, path: &str) -> String {
self.read().str(path)
}
pub fn get_int(&self, path: &str) -> Option<i64> {
self.read().get_int(path)
}
pub fn get_float(&self, path: &str) -> Option<f64> {
self.read().get_float(path)
}
pub fn get_bool(&self, path: &str) -> Option<bool> {
self.read().get_bool(path)
}
pub fn contains(&self, path: &str) -> bool {
self.read().contains(path)
}
}
impl From<Config> for ConfigHandle {
fn from(config: Config) -> Self {
Self::new(config)
}
}
#[cfg(test)]
mod tests {
use super::*;
const YAML: &str = "
app:
port: 8080
debug: true
timeout: 3.14
";
#[test]
fn new_and_read() {
let handle = ConfigHandle::new(Config::load_yaml(YAML, "/").unwrap());
assert_eq!(handle.str("app/port"), "8080");
assert_eq!(handle.get_int("app/port"), Some(8080));
assert_eq!(handle.get_bool("app/debug"), Some(true));
assert_eq!(handle.get_float("app/timeout"), Some(3.14));
assert!(handle.contains("app/port"));
assert!(!handle.contains("app/missing"));
}
#[test]
fn clone_shares_state() {
let handle1 = ConfigHandle::new(Config::load_yaml(YAML, "/").unwrap());
let handle2 = handle1.clone();
assert_eq!(handle1.str("app/port"), handle2.str("app/port"));
assert!(Arc::ptr_eq(&handle1.inner, &handle2.inner));
}
#[test]
fn from_config() {
let config = Config::load_yaml(YAML, "/").unwrap();
let handle: ConfigHandle = config.into();
assert_eq!(handle.str("app/port"), "8080");
}
#[test]
fn reload_picks_up_changes() {
use std::fs::{self, File};
use std::io::Write;
let path = "test_handle_reload.yaml";
let mut f = File::create(path).unwrap();
writeln!(f, "app:\n port: 8080").unwrap();
drop(f);
let handle = ConfigHandle::new(
Config::load_required(path, "/", None).unwrap()
);
assert_eq!(handle.str("app/port"), "8080");
let mut f = File::create(path).unwrap();
writeln!(f, "app:\n port: 9090").unwrap();
drop(f);
handle.reload().unwrap();
assert_eq!(handle.str("app/port"), "9090");
fs::remove_file(path).ok();
}
#[test]
fn reload_visible_to_all_clones() {
use std::fs::{self, File};
use std::io::Write;
let path = "test_handle_reload_clones.yaml";
let mut f = File::create(path).unwrap();
writeln!(f, "app:\n port: 1111").unwrap();
drop(f);
let handle1 = ConfigHandle::new(
Config::load_required(path, "/", None).unwrap()
);
let handle2 = handle1.clone();
let mut f = File::create(path).unwrap();
writeln!(f, "app:\n port: 2222").unwrap();
drop(f);
handle1.reload().unwrap();
assert_eq!(handle2.str("app/port"), "2222");
fs::remove_file(path).ok();
}
#[test]
fn reload_preserves_config_on_failure() {
use std::fs::{self, File};
use std::io::Write;
let path = "test_handle_reload_fail.yaml";
let mut f = File::create(path).unwrap();
writeln!(f, "app:\n port: 8080").unwrap();
drop(f);
let handle = ConfigHandle::new(
Config::load_required(path, "/", None).unwrap()
);
let mut f = File::create(path).unwrap();
writeln!(f, "invalid: [unclosed").unwrap();
drop(f);
assert!(handle.reload().is_err());
assert_eq!(handle.str("app/port"), "8080");
fs::remove_file(path).ok();
}
#[test]
fn multithreaded_reads() {
use std::thread;
let handle = ConfigHandle::new(Config::load_yaml(YAML, "/").unwrap());
let threads: Vec<_> = (0..8).map(|_| {
let h = handle.clone();
thread::spawn(move || {
assert_eq!(h.str("app/port"), "8080");
assert_eq!(h.get_int("app/port"), Some(8080));
})
}).collect();
for t in threads { t.join().unwrap(); }
}
}
#[cfg(test)]
const _: () = {
fn _assert_send_sync<T: Send + Sync>() {}
fn _check() {
_assert_send_sync::<ConfigHandle>();
}
};