1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
5pub struct Change {
6 pub line: usize,
8 pub before: String,
10 pub after: Option<String>,
12 #[serde(skip_serializing_if = "Option::is_none")]
14 pub context: Option<ChangeContext>,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
19pub struct ChangeContext {
20 pub before: Vec<String>,
21 pub after: Vec<String>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
26pub struct FileChanges {
27 pub path: String,
28 pub changes: Vec<Change>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct OpResult {
34 pub operation_index: usize,
35 pub files: Vec<FileChanges>,
36}
37
38#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
40pub struct Summary {
41 pub files_matched: usize,
42 pub files_modified: usize,
43 pub total_replacements: usize,
44}
45
46pub fn compute_summary(results: &[OpResult]) -> Summary {
52 use std::collections::HashSet;
53
54 let mut matched_paths: HashSet<&str> = HashSet::new();
55 let mut modified_paths: HashSet<&str> = HashSet::new();
56 let mut total_replacements: usize = 0;
57
58 for result in results {
59 for file_changes in &result.files {
60 matched_paths.insert(&file_changes.path);
61 if !file_changes.changes.is_empty() {
62 modified_paths.insert(&file_changes.path);
63 total_replacements += file_changes.changes.len();
64 }
65 }
66 }
67
68 Summary {
69 files_matched: matched_paths.len(),
70 files_modified: modified_paths.len(),
71 total_replacements,
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 #[test]
80 fn test_compute_summary_empty() {
81 let results: Vec<OpResult> = vec![];
82 let summary = compute_summary(&results);
83 assert_eq!(
84 summary,
85 Summary {
86 files_matched: 0,
87 files_modified: 0,
88 total_replacements: 0,
89 }
90 );
91 }
92
93 #[test]
94 fn test_compute_summary_single_result() {
95 let results = vec![OpResult {
96 operation_index: 0,
97 files: vec![FileChanges {
98 path: "src/main.rs".to_string(),
99 changes: vec![
100 Change {
101 line: 1,
102 before: "old".to_string(),
103 after: Some("new".to_string()),
104 context: None,
105 },
106 Change {
107 line: 5,
108 before: "old2".to_string(),
109 after: Some("new2".to_string()),
110 context: None,
111 },
112 ],
113 }],
114 }];
115 let summary = compute_summary(&results);
116 assert_eq!(
117 summary,
118 Summary {
119 files_matched: 1,
120 files_modified: 1,
121 total_replacements: 2,
122 }
123 );
124 }
125
126 #[test]
127 fn test_compute_summary_multiple_files() {
128 let results = vec![
129 OpResult {
130 operation_index: 0,
131 files: vec![
132 FileChanges {
133 path: "a.rs".to_string(),
134 changes: vec![Change {
135 line: 1,
136 before: "x".to_string(),
137 after: Some("y".to_string()),
138 context: None,
139 }],
140 },
141 FileChanges {
142 path: "b.rs".to_string(),
143 changes: vec![Change {
144 line: 2,
145 before: "x".to_string(),
146 after: Some("y".to_string()),
147 context: None,
148 }],
149 },
150 ],
151 },
152 OpResult {
153 operation_index: 1,
154 files: vec![FileChanges {
155 path: "a.rs".to_string(),
156 changes: vec![Change {
157 line: 3,
158 before: "z".to_string(),
159 after: Some("w".to_string()),
160 context: None,
161 }],
162 }],
163 },
164 ];
165 let summary = compute_summary(&results);
166 assert_eq!(
167 summary,
168 Summary {
169 files_matched: 2,
170 files_modified: 2,
171 total_replacements: 3,
172 }
173 );
174 }
175
176 #[test]
177 fn test_compute_summary_file_with_no_changes() {
178 let results = vec![OpResult {
179 operation_index: 0,
180 files: vec![FileChanges {
181 path: "empty.rs".to_string(),
182 changes: vec![],
183 }],
184 }];
185 let summary = compute_summary(&results);
186 assert_eq!(
187 summary,
188 Summary {
189 files_matched: 1,
190 files_modified: 0,
191 total_replacements: 0,
192 }
193 );
194 }
195
196 #[test]
197 fn test_compute_summary_deletions_counted() {
198 let results = vec![OpResult {
199 operation_index: 0,
200 files: vec![FileChanges {
201 path: "file.rs".to_string(),
202 changes: vec![
203 Change {
204 line: 1,
205 before: "deleted line".to_string(),
206 after: None, context: None,
208 },
209 Change {
210 line: 3,
211 before: "replaced".to_string(),
212 after: Some("new".to_string()),
213 context: None,
214 },
215 ],
216 }],
217 }];
218 let summary = compute_summary(&results);
219 assert_eq!(summary.total_replacements, 2);
220 }
221}