use std::fmt::Debug;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use crate::reconcile::{Reconcile, ReconcileErrors};
use crate::{Lifecycle, OptativeSet};
pub struct OptativeJsonSet<T: Lifecycle> {
file: PathBuf,
inner: OptativeSet<T>,
persist_error: Option<io::Error>,
}
#[derive(serde::Serialize)]
struct WriteEntry<'a, K, V> {
key: &'a K,
value: &'a V,
}
#[derive(serde::Deserialize)]
struct ReadEntry<K, V> {
key: K,
value: V,
}
fn clear_stale_new_files(file: &Path) -> io::Result<()> {
let Some(prefix) = file.file_name().and_then(|n| n.to_str()) else {
return Ok(());
};
let prefix = format!("{prefix}.new.");
let dir = match file.parent() {
Some(p) if !p.as_os_str().is_empty() => p,
_ => Path::new("."),
};
let entries = match fs::read_dir(dir) {
Ok(entries) => entries,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(e),
};
for entry in entries {
let entry = entry?;
if entry
.file_name()
.to_str()
.is_some_and(|n| n.starts_with(&prefix))
{
fs::remove_file(entry.path())?;
}
}
Ok(())
}
impl<T: Lifecycle> OptativeJsonSet<T>
where
T::Error: Debug,
T::State: serde::Serialize + serde::de::DeserializeOwned,
{
pub fn open(file: impl Into<PathBuf>) -> io::Result<Self> {
let file = file.into();
clear_stale_new_files(&file)?;
let items = Self::load(&file)?;
Ok(Self {
file,
inner: OptativeSet::with_initial_state(items),
persist_error: None,
})
}
fn load(file: &Path) -> io::Result<Vec<(T::Key, T::State)>> {
let contents = match fs::read_to_string(file) {
Ok(contents) => contents,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(e),
};
contents
.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| {
let entry: ReadEntry<T::Key, T::State> = serde_json::from_str(line)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
Ok((entry.key, entry.value))
})
.collect()
}
fn persist(&self) -> io::Result<()> {
if let Some(parent) = self.file.parent()
&& !parent.as_os_str().is_empty()
{
fs::create_dir_all(parent)?;
}
let mut lines: Vec<(String, String)> = self
.inner
.iter()
.map(|(key, value)| {
let sort_key = serde_json::to_string(key).expect("Lifecycle::Key must serialize");
let line = serde_json::to_string(&WriteEntry { key, value })
.expect("Lifecycle::State must serialize");
(sort_key, line)
})
.collect();
lines.sort_by(|a, b| a.0.cmp(&b.0));
let mut out = String::new();
for (_, line) in lines {
out.push_str(&line);
out.push('\n');
}
let new_file = self.new_file_path();
fs::write(&new_file, out)?;
fs::rename(&new_file, &self.file)
}
fn new_file_path(&self) -> PathBuf {
let mut new_file = self.file.clone().into_os_string();
new_file.push(format!(".new.{}", std::process::id()));
PathBuf::from(new_file)
}
pub fn persist_error(&self) -> Option<&io::Error> {
self.persist_error.as_ref()
}
pub fn get(&self, key: &T::Key) -> Option<&T::State> {
self.inner.get(key)
}
pub fn iter(&self) -> impl Iterator<Item = (&T::Key, &T::State)> {
self.inner.iter()
}
}
impl<T: Lifecycle> Reconcile<T> for OptativeJsonSet<T>
where
T::Error: Debug,
T::State: serde::Serialize + serde::de::DeserializeOwned,
{
fn reconcile(
&mut self,
desired: impl IntoIterator<Item = T>,
ctx: &mut T::Context,
output: &mut T::Output,
) -> ReconcileErrors<T::Key, T::Error> {
let errors = self.inner.reconcile(desired, ctx, output);
self.persist_error = self.persist().err();
errors
}
}