use std::path::Path;
pub fn read_state_file_or_fresh(path: &Path) -> Result<Option<String>, std::io::Error> {
if path.as_os_str().is_empty() || !path.exists() {
return Ok(None);
}
let raw = std::fs::read_to_string(path)?;
if raw.trim().is_empty() {
Ok(None)
} else {
Ok(Some(raw))
}
}
pub fn load_or_fresh<T, F>(manager_name: &'static str, path: &Path, parse: F) -> T
where
T: Default,
F: FnOnce(&str) -> Result<T, serde_json::Error>,
{
let raw = match read_state_file_or_fresh(path) {
Ok(Some(s)) => s,
Ok(None) => {
tracing::info!(
manager = manager_name,
path = %path.display(),
"state file missing or empty; starting fresh",
);
return T::default();
}
Err(e) => {
tracing::warn!(
manager = manager_name,
path = %path.display(),
error = %e,
"state file read failed; starting fresh — file left in place for inspection",
);
crate::metrics::record_state_file_load_failure(manager_name, "read_error");
return T::default();
}
};
match parse(&raw) {
Ok(mgr) => mgr,
Err(e) => {
tracing::warn!(
manager = manager_name,
path = %path.display(),
error = %e,
"state file parse failed (corrupted JSON); starting fresh — file left in place for inspection",
);
crate::metrics::record_state_file_load_failure(manager_name, "parse_error");
T::default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write as _;
#[derive(Debug, Default, PartialEq, Eq)]
struct ToyManager {
items: Vec<String>,
}
impl ToyManager {
fn from_json(s: &str) -> Result<Self, serde_json::Error> {
let items: Vec<String> = serde_json::from_str(s)?;
Ok(Self { items })
}
}
#[test]
fn load_or_fresh_with_valid_json_returns_parsed() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("snap.json");
std::fs::write(&path, r#"["a","b","c"]"#).expect("write");
let got: ToyManager = load_or_fresh("toy", &path, ToyManager::from_json);
assert_eq!(
got,
ToyManager {
items: vec!["a".into(), "b".into(), "c".into()],
},
"valid snapshot must round-trip into the typed manager",
);
}
#[test]
fn load_or_fresh_with_corrupted_json_logs_warn_and_returns_default() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("snap.json");
let mut f = std::fs::File::create(&path).expect("create");
f.write_all(br#"{ "broken json"#).expect("write");
drop(f);
let pre_bytes = std::fs::read(&path).expect("pre read");
let got: ToyManager = load_or_fresh("toy", &path, ToyManager::from_json);
assert_eq!(
got,
ToyManager::default(),
"corrupted snapshot must fall back to T::default(), not propagate Err",
);
let post_bytes = std::fs::read(&path).expect("post read");
assert_eq!(
pre_bytes, post_bytes,
"the operator's snapshot bytes MUST be left untouched on parse failure",
);
}
#[test]
fn load_or_fresh_with_missing_file_returns_default() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("does-not-exist.json");
let got: ToyManager = load_or_fresh("toy", &path, ToyManager::from_json);
assert_eq!(
got,
ToyManager::default(),
"missing snapshot must fall back to T::default()",
);
}
#[test]
fn load_or_fresh_with_empty_file_returns_default() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("empty.json");
std::fs::write(&path, " \n \t\n").expect("write");
let got: ToyManager = load_or_fresh("toy", &path, ToyManager::from_json);
assert_eq!(got, ToyManager::default());
}
#[test]
fn read_state_file_or_fresh_normalises_empty_path() {
let raw = read_state_file_or_fresh(Path::new("")).expect("ok");
assert!(raw.is_none(), "empty path must surface as Ok(None)");
}
}