Skip to main content

reformat_core/
changes.rs

1//! Change tracking for refactoring operations
2//!
3//! This module provides types for recording changes made during refactoring
4//! operations like file grouping, enabling subsequent reference fixing.
5
6use serde::{Deserialize, Serialize};
7use std::fs;
8use std::path::Path;
9
10/// A single change record from a refactoring operation
11#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(tag = "type", rename_all = "snake_case")]
13pub enum Change {
14    /// A directory was created
15    DirectoryCreated {
16        /// Path to the created directory (relative to base_dir)
17        path: String,
18    },
19    /// A file was moved (and optionally renamed)
20    FileMoved {
21        /// Original path (relative to base_dir)
22        from: String,
23        /// New path (relative to base_dir)
24        to: String,
25    },
26    /// A file was renamed in place
27    FileRenamed {
28        /// Original filename
29        from: String,
30        /// New filename
31        to: String,
32        /// Directory containing the file
33        directory: String,
34    },
35}
36
37/// Record of all changes from a refactoring operation
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct ChangeRecord {
40    /// Name of the operation (e.g., "group", "rename_files")
41    pub operation: String,
42    /// ISO 8601 timestamp of when the operation occurred
43    pub timestamp: String,
44    /// Base directory where the operation was performed (absolute path)
45    pub base_dir: String,
46    /// Options used for the operation (operation-specific)
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub options: Option<serde_json::Value>,
49    /// List of changes made
50    pub changes: Vec<Change>,
51}
52
53impl ChangeRecord {
54    /// Creates a new change record
55    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    /// Sets the options for this operation
67    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    /// Adds a directory creation change
73    pub fn add_directory_created(&mut self, path: &str) {
74        self.changes.push(Change::DirectoryCreated {
75            path: path.to_string(),
76        });
77    }
78
79    /// Adds a file move change
80    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    /// Adds a file rename change
88    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    /// Returns true if there are no changes recorded
97    pub fn is_empty(&self) -> bool {
98        self.changes.is_empty()
99    }
100
101    /// Returns the number of changes
102    pub fn len(&self) -> usize {
103        self.changes.len()
104    }
105
106    /// Returns only the file move changes
107    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    /// Writes the change record to a JSON file
118    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    /// Reads a change record from a JSON file
125    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}