1use serde::{Deserialize, Serialize};
7use std::fs;
8use std::path::Path;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(tag = "type", rename_all = "snake_case")]
13pub enum Change {
14 DirectoryCreated {
16 path: String,
18 },
19 FileMoved {
21 from: String,
23 to: String,
25 },
26 FileRenamed {
28 from: String,
30 to: String,
32 directory: String,
34 },
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct ChangeRecord {
40 pub operation: String,
42 pub timestamp: String,
44 pub base_dir: String,
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub options: Option<serde_json::Value>,
49 pub changes: Vec<Change>,
51}
52
53impl ChangeRecord {
54 pub fn new(operation: &str, base_dir: &Path) -> Self {
56 let timestamp = chrono::Utc::now().to_rfc3339();
57 ChangeRecord {
58 operation: operation.to_string(),
59 timestamp,
60 base_dir: base_dir.to_string_lossy().to_string(),
61 options: None,
62 changes: Vec::new(),
63 }
64 }
65
66 pub fn with_options<T: Serialize>(mut self, options: &T) -> Self {
68 self.options = serde_json::to_value(options).ok();
69 self
70 }
71
72 pub fn add_directory_created(&mut self, path: &str) {
74 self.changes.push(Change::DirectoryCreated {
75 path: path.to_string(),
76 });
77 }
78
79 pub fn add_file_moved(&mut self, from: &str, to: &str) {
81 self.changes.push(Change::FileMoved {
82 from: from.to_string(),
83 to: to.to_string(),
84 });
85 }
86
87 pub fn add_file_renamed(&mut self, from: &str, to: &str, directory: &str) {
89 self.changes.push(Change::FileRenamed {
90 from: from.to_string(),
91 to: to.to_string(),
92 directory: directory.to_string(),
93 });
94 }
95
96 pub fn is_empty(&self) -> bool {
98 self.changes.is_empty()
99 }
100
101 pub fn len(&self) -> usize {
103 self.changes.len()
104 }
105
106 pub fn file_moves(&self) -> Vec<(&str, &str)> {
108 self.changes
109 .iter()
110 .filter_map(|c| match c {
111 Change::FileMoved { from, to } => Some((from.as_str(), to.as_str())),
112 _ => None,
113 })
114 .collect()
115 }
116
117 pub fn write_to_file(&self, path: &Path) -> crate::Result<()> {
119 let json = serde_json::to_string_pretty(self)?;
120 fs::write(path, json)?;
121 Ok(())
122 }
123
124 pub fn read_from_file(path: &Path) -> crate::Result<Self> {
126 let json = fs::read_to_string(path)?;
127 let record: ChangeRecord = serde_json::from_str(&json)?;
128 Ok(record)
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn test_change_record_creation() {
138 let record = ChangeRecord::new("group", Path::new("/tmp/test"));
139 assert_eq!(record.operation, "group");
140 assert!(record.changes.is_empty());
141 }
142
143 #[test]
144 fn test_add_changes() {
145 let mut record = ChangeRecord::new("group", Path::new("/tmp/test"));
146 record.add_directory_created("wbs");
147 record.add_file_moved("wbs_create.tmpl", "wbs/create.tmpl");
148
149 assert_eq!(record.len(), 2);
150 assert!(!record.is_empty());
151 }
152
153 #[test]
154 fn test_file_moves() {
155 let mut record = ChangeRecord::new("group", Path::new("/tmp/test"));
156 record.add_directory_created("wbs");
157 record.add_file_moved("wbs_create.tmpl", "wbs/create.tmpl");
158 record.add_file_moved("wbs_delete.tmpl", "wbs/delete.tmpl");
159
160 let moves = record.file_moves();
161 assert_eq!(moves.len(), 2);
162 assert_eq!(moves[0], ("wbs_create.tmpl", "wbs/create.tmpl"));
163 }
164
165 #[test]
166 fn test_serialization() {
167 let mut record = ChangeRecord::new("group", Path::new("/tmp/test"));
168 record.add_directory_created("wbs");
169 record.add_file_moved("wbs_create.tmpl", "wbs/create.tmpl");
170
171 let json = serde_json::to_string_pretty(&record).unwrap();
172 assert!(json.contains("\"operation\": \"group\""));
173 assert!(json.contains("\"type\": \"directory_created\""));
174 assert!(json.contains("\"type\": \"file_moved\""));
175 }
176
177 #[test]
178 fn test_write_and_read() {
179 let test_dir = std::env::temp_dir().join("reformat_changes_test");
180 let _ = fs::create_dir_all(&test_dir);
181 let file_path = test_dir.join("changes.json");
182
183 let mut record = ChangeRecord::new("group", Path::new("/tmp/test"));
184 record.add_file_moved("old.txt", "new/old.txt");
185 record.write_to_file(&file_path).unwrap();
186
187 let loaded = ChangeRecord::read_from_file(&file_path).unwrap();
188 assert_eq!(loaded.operation, "group");
189 assert_eq!(loaded.len(), 1);
190
191 let _ = fs::remove_dir_all(&test_dir);
192 }
193}