use std::{ borrow::Borrow, collections::{HashMap, HashSet}, ops::{Deref, DerefMut}, path::{Path, PathBuf}, }; use serde::{Deserialize, Serialize}; use crate::Delta; /// Struct that represents a current state for a backup. This struct acts as a smart pointer around /// a HashMap. #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct State(HashMap>); impl State { /// Apply the delta to the current state. pub fn apply(&mut self, delta: &Delta) { // First we add new files, then we remove the old ones for (dir, added) in delta.added.iter() { if let Some(current) = self.0.get_mut(dir) { current.extend(added.iter().cloned()); } else { self.0.insert(dir.clone(), added.clone()); } } for (dir, removed) in delta.removed.iter() { if let Some(current) = self.0.get_mut(dir) { current.retain(|k| !removed.contains(k)); } } } /// Returns whether the provided relative path is part of the given state. pub fn contains>(&self, path: P) -> bool { let path = path.as_ref(); self.0.iter().any(|(dir, files)| { path.starts_with(dir) && files.contains(path.strip_prefix(dir).unwrap()) }) } /// Returns whether the state is empty. /// /// Note that this does not necessarily mean that the state does not contain any sets, but /// rather that any sets that it does contain are also empty. pub fn is_empty(&self) -> bool { self.0.values().all(|s| s.is_empty()) } pub fn append_dir(&mut self, dir: impl Into, files: I) where I: IntoIterator, I::Item: Into, { let dir = dir.into(); let files = files.into_iter().map(Into::into); if let Some(dir_files) = self.0.get_mut(&dir) { dir_files.extend(files); } else { self.0.insert(dir, files.collect()); } } pub fn with_dir(mut self, dir: impl Into, files: I) -> Self where I: IntoIterator, I::Item: Into, { self.append_dir(dir, files); self } } impl PartialEq for State { fn eq(&self, other: &Self) -> bool { let self_non_empty = self.0.values().filter(|files| !files.is_empty()).count(); let other_non_empty = other.0.values().filter(|files| !files.is_empty()).count(); if self_non_empty != other_non_empty { return false; } // If both states have the same number of non-empty directories, then comparing each // directory of one with the other will only be true if their list of non-empty directories // is identical. self.0 .iter() .all(|(dir, files)| files.is_empty() || other.0.get(dir).map_or(false, |v| v == files)) } } impl Eq for State {} impl From for State where T: IntoIterator, T::Item: Borrow, { fn from(deltas: T) -> Self { let mut state = State::default(); for delta in deltas { state.apply(delta.borrow()); } state } } impl AsRef>> for State { fn as_ref(&self) -> &HashMap> { &self.0 } } impl Deref for State { type Target = HashMap>; fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for State { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } #[cfg(test)] mod tests { use super::*; #[test] fn test_eq() { let a = State::default().with_dir("dir1", ["file1", "file2"]); let b = State::default().with_dir("dir1", ["file1", "file2"]); assert_eq!(a, b); let b = b.with_dir("dir2", ["file3"]); assert_ne!(a, b); } #[test] fn test_eq_empty_dirs() { let a = State::default().with_dir("dir1", ["file1", "file2"]); let b = State::default() .with_dir("dir1", ["file1", "file2"]) .with_dir("dir2", Vec::::new()); assert_eq!(a, b); let b = b.with_dir("dir2", ["file3"]); assert_ne!(a, b); } }