use serde_json::Value;
use std::path::Path;
use std::sync::Mutex;
pub fn open_file<P: AsRef<Path>>(path: P) -> std::io::Result<String> {
std::fs::read_to_string(path)
}
pub fn load_json_config<P: AsRef<Path>>(config_file_path: P) -> Result<Value, String> {
let contents = open_file(config_file_path).map_err(|e| format!("open_file: {}", e))?;
serde_json::from_str(&contents).map_err(|e| format!("json parse: {}", e))
}
pub struct DummyWatcher;
impl DummyWatcher {
pub fn check<P: AsRef<Path>>(&self, _path: P) -> bool {
false
}
pub fn watch<P: AsRef<Path>>(&self, _path: P) {
}
}
#[derive(Debug, Clone)]
pub struct DeferredCall {
pub method: String,
pub path: std::path::PathBuf,
}
pub struct DeferredWatcher {
pub calls: Mutex<Vec<DeferredCall>>,
}
impl Default for DeferredWatcher {
fn default() -> Self {
Self::new()
}
}
impl DeferredWatcher {
pub fn new() -> Self {
Self {
calls: Mutex::new(Vec::new()),
}
}
pub fn check<P: AsRef<Path>>(&self, path: P) {
self.calls.lock().unwrap().push(DeferredCall {
method: "__call__".into(),
path: path.as_ref().to_path_buf(),
});
}
pub fn watch<P: AsRef<Path>>(&self, path: P) {
self.calls.lock().unwrap().push(DeferredCall {
method: "watch".into(),
path: path.as_ref().to_path_buf(),
});
}
pub fn unwatch<P: AsRef<Path>>(&self, path: P) {
self.calls.lock().unwrap().push(DeferredCall {
method: "unwatch".into(),
path: path.as_ref().to_path_buf(),
});
}
pub fn transfer_calls(&self) -> Vec<DeferredCall> {
let mut calls = self.calls.lock().unwrap();
std::mem::take(&mut *calls)
}
}
pub struct ConfigLoader {
pub watcher_type: String,
pub pl: Option<()>,
pub interval: Option<u64>,
pub watched: std::collections::HashMap<std::path::PathBuf, std::collections::HashSet<u64>>,
pub missing: std::collections::HashMap<String, std::collections::HashSet<(u64, u64)>>,
pub loaded: std::collections::HashMap<std::path::PathBuf, Value>,
pub lock: Mutex<()>,
}
impl Default for ConfigLoader {
fn default() -> Self {
Self::new(false)
}
}
impl ConfigLoader {
pub fn new(run_once: bool) -> Self {
let watcher_type = if run_once {
"dummy".to_string()
} else {
"deferred".to_string()
};
Self {
watcher_type,
pl: None,
interval: None,
watched: std::collections::HashMap::new(),
missing: std::collections::HashMap::new(),
loaded: std::collections::HashMap::new(),
lock: Mutex::new(()),
}
}
pub fn set_pl(&mut self, _pl: ()) {
self.pl = Some(());
}
pub fn set_interval(&mut self, interval: u64) {
self.interval = Some(interval);
}
pub fn register<P: AsRef<Path>>(&mut self, function_id: u64, path: P) {
let _g = self.lock.lock().unwrap_or_else(|e| e.into_inner());
self.watched
.entry(path.as_ref().to_path_buf())
.or_default()
.insert(function_id);
}
pub fn register_missing(
&mut self,
condition_function_id: u64,
function_id: u64,
key: impl Into<String>,
) {
let _g = self.lock.lock().unwrap_or_else(|e| e.into_inner());
self.missing
.entry(key.into())
.or_default()
.insert((condition_function_id, function_id));
}
pub fn unregister_functions(&mut self, removed_functions: &std::collections::HashSet<u64>) {
let _g = self.lock.lock().unwrap_or_else(|e| e.into_inner());
let paths: Vec<std::path::PathBuf> = self.watched.keys().cloned().collect();
for path in paths {
if let Some(functions) = self.watched.get_mut(&path) {
for id in removed_functions {
functions.remove(id);
}
if functions.is_empty() {
self.watched.remove(&path);
self.loaded.remove(&path);
}
}
}
}
pub fn unregister_missing(
&mut self,
removed_functions: &std::collections::HashSet<(u64, u64)>,
) {
let _g = self.lock.lock().unwrap_or_else(|e| e.into_inner());
let keys: Vec<String> = self.missing.keys().cloned().collect();
for key in keys {
if let Some(functions) = self.missing.get_mut(&key) {
for pair in removed_functions {
functions.remove(pair);
}
if functions.is_empty() {
self.missing.remove(&key);
}
}
}
}
pub fn load<P, F>(&mut self, path: P, load_fn: F) -> Result<Value, String>
where
P: AsRef<Path>,
F: FnOnce(&Path) -> Result<Value, String>,
{
let path = path.as_ref().to_path_buf();
if let Some(cached) = self.loaded.get(&path) {
return Ok(cached.clone());
}
let r = load_fn(&path)?;
self.loaded.insert(path, r.clone());
Ok(r)
}
}
impl ConfigLoader {
pub fn set_watcher(&mut self, watcher_type: &str, _force: bool) -> bool {
if watcher_type == self.watcher_type {
return false;
}
let _g = self.lock.lock().unwrap_or_else(|e| e.into_inner());
self.watcher_type = watcher_type.to_string();
true
}
pub fn exception(&self, msg: &str, args: &[&str]) -> Result<String, String> {
let mut rendered = msg.to_string();
for (i, arg) in args.iter().enumerate() {
rendered = rendered.replace(&format!("{{{}}}", i), arg);
}
if self.pl.is_some() {
Ok(format!("config_loader: {}", rendered))
} else {
Err(rendered)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn tmp_json(content: &str) -> std::path::PathBuf {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let mut p = std::env::temp_dir();
p.push(format!(
"powerliners-config-test-{}-{}-{}.json",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos(),
COUNTER.fetch_add(1, Ordering::SeqCst)
));
let mut f = std::fs::File::create(&p).unwrap();
f.write_all(content.as_bytes()).unwrap();
p
}
#[test]
fn open_file_reads_utf8_contents() {
let p = tmp_json("héllo, world");
let r = open_file(&p).unwrap();
assert_eq!(r, "héllo, world");
std::fs::remove_file(&p).ok();
}
#[test]
fn load_json_config_parses_basic() {
let p = tmp_json(r#"{"name": "powerline", "version": 1}"#);
let v = load_json_config(&p).unwrap();
assert_eq!(v["name"], "powerline");
assert_eq!(v["version"], 1);
std::fs::remove_file(&p).ok();
}
#[test]
fn load_json_config_returns_err_on_bad_json() {
let p = tmp_json("{ this isn't json }");
assert!(load_json_config(&p).is_err());
std::fs::remove_file(&p).ok();
}
#[test]
fn dummy_watcher_always_returns_false() {
let w = DummyWatcher;
assert!(!w.check("/etc/passwd"));
w.watch("/etc/passwd");
}
#[test]
fn deferred_watcher_queues_calls() {
let w = DeferredWatcher::new();
w.watch("/etc/config1");
w.check("/etc/config2");
w.unwatch("/etc/config1");
let calls = w.transfer_calls();
assert_eq!(calls.len(), 3);
assert_eq!(calls[0].method, "watch");
assert_eq!(calls[1].method, "__call__");
assert_eq!(calls[2].method, "unwatch");
let calls2 = w.transfer_calls();
assert!(calls2.is_empty());
}
#[test]
fn config_loader_run_once_uses_dummy_watcher_type() {
let cl = ConfigLoader::new(true);
assert_eq!(cl.watcher_type, "dummy");
}
#[test]
fn config_loader_default_uses_deferred_watcher_type() {
let cl = ConfigLoader::new(false);
assert_eq!(cl.watcher_type, "deferred");
}
#[test]
fn config_loader_set_pl_records_value() {
let mut cl = ConfigLoader::new(false);
cl.set_pl(());
assert!(cl.pl.is_some());
}
#[test]
fn config_loader_set_interval_records_value() {
let mut cl = ConfigLoader::new(false);
cl.set_interval(5);
assert_eq!(cl.interval, Some(5));
}
#[test]
fn config_loader_register_adds_function_to_watched_path() {
let mut cl = ConfigLoader::new(false);
cl.register(42, "/etc/powerline/config.json");
assert!(cl
.watched
.contains_key(std::path::Path::new("/etc/powerline/config.json")));
assert!(cl.watched[std::path::Path::new("/etc/powerline/config.json")].contains(&42));
}
#[test]
fn config_loader_register_dedupes_function_id() {
let mut cl = ConfigLoader::new(false);
cl.register(42, "/x");
cl.register(42, "/x");
assert_eq!(cl.watched[std::path::Path::new("/x")].len(), 1);
}
#[test]
fn config_loader_register_missing_adds_pair_to_key() {
let mut cl = ConfigLoader::new(false);
cl.register_missing(100, 200, "key1");
assert!(cl.missing.contains_key("key1"));
assert!(cl.missing["key1"].contains(&(100, 200)));
}
#[test]
fn config_loader_unregister_functions_drops_path_when_empty() {
let mut cl = ConfigLoader::new(false);
cl.register(42, "/x");
let mut removed = std::collections::HashSet::new();
removed.insert(42);
cl.unregister_functions(&removed);
assert!(!cl.watched.contains_key(std::path::Path::new("/x")));
}
#[test]
fn config_loader_unregister_functions_keeps_path_with_remaining_functions() {
let mut cl = ConfigLoader::new(false);
cl.register(42, "/x");
cl.register(99, "/x");
let mut removed = std::collections::HashSet::new();
removed.insert(42);
cl.unregister_functions(&removed);
assert!(cl.watched.contains_key(std::path::Path::new("/x")));
assert_eq!(cl.watched[std::path::Path::new("/x")].len(), 1);
}
#[test]
fn config_loader_unregister_functions_clears_loaded_entry() {
let mut cl = ConfigLoader::new(false);
cl.register(42, "/x");
cl.loaded
.insert(std::path::PathBuf::from("/x"), serde_json::json!({"a": 1}));
let mut removed = std::collections::HashSet::new();
removed.insert(42);
cl.unregister_functions(&removed);
assert!(!cl.loaded.contains_key(std::path::Path::new("/x")));
}
#[test]
fn config_loader_unregister_missing_drops_key_when_empty() {
let mut cl = ConfigLoader::new(false);
cl.register_missing(100, 200, "key1");
let mut removed = std::collections::HashSet::new();
removed.insert((100, 200));
cl.unregister_missing(&removed);
assert!(!cl.missing.contains_key("key1"));
}
#[test]
fn config_loader_load_calls_load_fn_on_cache_miss() {
let mut cl = ConfigLoader::new(false);
let p = std::path::PathBuf::from("/test/config.json");
let r = cl
.load(&p, |_| Ok(serde_json::json!({"loaded": true})))
.unwrap();
assert_eq!(r["loaded"], true);
assert!(cl.loaded.contains_key(&p));
}
#[test]
fn config_loader_load_returns_cached_value_on_hit() {
let mut cl = ConfigLoader::new(false);
let p = std::path::PathBuf::from("/test/config.json");
cl.loaded
.insert(p.clone(), serde_json::json!({"cached": true}));
let r = cl
.load(&p, |_| {
panic!("load_fn should not be called on cache hit");
})
.unwrap();
assert_eq!(r["cached"], true);
}
#[test]
fn config_loader_load_propagates_load_fn_errors() {
let mut cl = ConfigLoader::new(false);
let p = std::path::PathBuf::from("/test/missing.json");
let r = cl.load(&p, |_| Err("read fail".to_string()));
assert!(r.is_err());
assert!(!cl.loaded.contains_key(&p));
}
#[test]
fn config_loader_set_watcher_same_type_is_noop() {
let mut cl = ConfigLoader::new(false);
assert!(!cl.set_watcher("deferred", false));
assert_eq!(cl.watcher_type, "deferred");
}
#[test]
fn config_loader_set_watcher_different_type_swaps() {
let mut cl = ConfigLoader::new(false);
assert!(cl.set_watcher("inotify", false));
assert_eq!(cl.watcher_type, "inotify");
}
#[test]
fn config_loader_set_watcher_dummy_to_inotify() {
let mut cl = ConfigLoader::new(true);
assert_eq!(cl.watcher_type, "dummy");
assert!(cl.set_watcher("inotify", false));
assert_eq!(cl.watcher_type, "inotify");
}
#[test]
fn config_loader_exception_no_pl_returns_err() {
let cl = ConfigLoader::new(false);
let r = cl.exception("Error: {0} not found", &["foo"]);
assert!(r.is_err());
assert_eq!(r.unwrap_err(), "Error: foo not found");
}
#[test]
fn config_loader_exception_with_pl_returns_formatted() {
let mut cl = ConfigLoader::new(false);
cl.set_pl(());
let r = cl.exception("Error: {0} broken", &["xyz"]).unwrap();
assert_eq!(r, "config_loader: Error: xyz broken");
}
#[test]
fn config_loader_exception_substitutes_multiple_args() {
let cl = ConfigLoader::new(false);
let r = cl.exception("a={0} b={1}", &["X", "Y"]).unwrap_err();
assert_eq!(r, "a=X b=Y");
}
}