use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Change {
DirectoryCreated {
path: String,
},
FileMoved {
from: String,
to: String,
},
FileRenamed {
from: String,
to: String,
directory: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChangeRecord {
pub operation: String,
pub timestamp: String,
pub base_dir: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<serde_json::Value>,
pub changes: Vec<Change>,
}
impl ChangeRecord {
pub fn new(operation: &str, base_dir: &Path) -> Self {
let timestamp = chrono::Utc::now().to_rfc3339();
ChangeRecord {
operation: operation.to_string(),
timestamp,
base_dir: base_dir.to_string_lossy().to_string(),
options: None,
changes: Vec::new(),
}
}
pub fn with_options<T: Serialize>(mut self, options: &T) -> Self {
self.options = serde_json::to_value(options).ok();
self
}
pub fn add_directory_created(&mut self, path: &str) {
self.changes.push(Change::DirectoryCreated {
path: path.to_string(),
});
}
pub fn add_file_moved(&mut self, from: &str, to: &str) {
self.changes.push(Change::FileMoved {
from: from.to_string(),
to: to.to_string(),
});
}
pub fn add_file_renamed(&mut self, from: &str, to: &str, directory: &str) {
self.changes.push(Change::FileRenamed {
from: from.to_string(),
to: to.to_string(),
directory: directory.to_string(),
});
}
pub fn is_empty(&self) -> bool {
self.changes.is_empty()
}
pub fn len(&self) -> usize {
self.changes.len()
}
pub fn file_moves(&self) -> Vec<(&str, &str)> {
self.changes
.iter()
.filter_map(|c| match c {
Change::FileMoved { from, to } => Some((from.as_str(), to.as_str())),
_ => None,
})
.collect()
}
pub fn write_to_file(&self, path: &Path) -> crate::Result<()> {
let json = serde_json::to_string_pretty(self)?;
fs::write(path, json)?;
Ok(())
}
pub fn read_from_file(path: &Path) -> crate::Result<Self> {
let json = fs::read_to_string(path)?;
let record: ChangeRecord = serde_json::from_str(&json)?;
Ok(record)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_change_record_creation() {
let record = ChangeRecord::new("group", Path::new("/tmp/test"));
assert_eq!(record.operation, "group");
assert!(record.changes.is_empty());
}
#[test]
fn test_add_changes() {
let mut record = ChangeRecord::new("group", Path::new("/tmp/test"));
record.add_directory_created("wbs");
record.add_file_moved("wbs_create.tmpl", "wbs/create.tmpl");
assert_eq!(record.len(), 2);
assert!(!record.is_empty());
}
#[test]
fn test_file_moves() {
let mut record = ChangeRecord::new("group", Path::new("/tmp/test"));
record.add_directory_created("wbs");
record.add_file_moved("wbs_create.tmpl", "wbs/create.tmpl");
record.add_file_moved("wbs_delete.tmpl", "wbs/delete.tmpl");
let moves = record.file_moves();
assert_eq!(moves.len(), 2);
assert_eq!(moves[0], ("wbs_create.tmpl", "wbs/create.tmpl"));
}
#[test]
fn test_serialization() {
let mut record = ChangeRecord::new("group", Path::new("/tmp/test"));
record.add_directory_created("wbs");
record.add_file_moved("wbs_create.tmpl", "wbs/create.tmpl");
let json = serde_json::to_string_pretty(&record).unwrap();
assert!(json.contains("\"operation\": \"group\""));
assert!(json.contains("\"type\": \"directory_created\""));
assert!(json.contains("\"type\": \"file_moved\""));
}
#[test]
fn test_write_and_read() {
let test_dir = std::env::temp_dir().join("reformat_changes_test");
let _ = fs::create_dir_all(&test_dir);
let file_path = test_dir.join("changes.json");
let mut record = ChangeRecord::new("group", Path::new("/tmp/test"));
record.add_file_moved("old.txt", "new/old.txt");
record.write_to_file(&file_path).unwrap();
let loaded = ChangeRecord::read_from_file(&file_path).unwrap();
assert_eq!(loaded.operation, "group");
assert_eq!(loaded.len(), 1);
let _ = fs::remove_dir_all(&test_dir);
}
}