1use serde::{Deserialize, Serialize};
8
9use crate::ToolInfo;
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
29pub struct DiffRow {
30 pub lang: String,
31 pub old_code: usize,
32 pub new_code: usize,
33 pub delta_code: i64,
34 pub old_lines: usize,
35 pub new_lines: usize,
36 pub delta_lines: i64,
37 pub old_files: usize,
38 pub new_files: usize,
39 pub delta_files: i64,
40 pub old_bytes: usize,
41 pub new_bytes: usize,
42 pub delta_bytes: i64,
43 pub old_tokens: usize,
44 pub new_tokens: usize,
45 pub delta_tokens: i64,
46}
47
48#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
61pub struct DiffTotals {
62 pub old_code: usize,
63 pub new_code: usize,
64 pub delta_code: i64,
65 pub old_lines: usize,
66 pub new_lines: usize,
67 pub delta_lines: i64,
68 pub old_files: usize,
69 pub new_files: usize,
70 pub delta_files: i64,
71 pub old_bytes: usize,
72 pub new_bytes: usize,
73 pub delta_bytes: i64,
74 pub old_tokens: usize,
75 pub new_tokens: usize,
76 pub delta_tokens: i64,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct DiffReceipt {
82 pub schema_version: u32,
83 pub generated_at_ms: u128,
84 pub tool: ToolInfo,
85 pub mode: String,
86 pub from_source: String,
87 pub to_source: String,
88 pub diff_rows: Vec<DiffRow>,
89 pub totals: DiffTotals,
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 fn sample_diff_row() -> DiffRow {
97 DiffRow {
98 lang: "Rust".to_string(),
99 old_code: 100,
100 new_code: 150,
101 delta_code: 50,
102 old_lines: 200,
103 new_lines: 260,
104 delta_lines: 60,
105 old_files: 5,
106 new_files: 7,
107 delta_files: 2,
108 old_bytes: 5_000,
109 new_bytes: 6_500,
110 delta_bytes: 1_500,
111 old_tokens: 1_200,
112 new_tokens: 1_700,
113 delta_tokens: 500,
114 }
115 }
116
117 fn sample_diff_totals() -> DiffTotals {
118 DiffTotals {
119 old_code: 1_000,
120 new_code: 1_200,
121 delta_code: 200,
122 old_lines: 1_500,
123 new_lines: 1_800,
124 delta_lines: 300,
125 old_files: 10,
126 new_files: 12,
127 delta_files: 2,
128 old_bytes: 40_000,
129 new_bytes: 48_000,
130 delta_bytes: 8_000,
131 old_tokens: 10_000,
132 new_tokens: 12_000,
133 delta_tokens: 2_000,
134 }
135 }
136
137 #[test]
139 fn diff_row_serde_roundtrip() {
140 let row = sample_diff_row();
141 let json = serde_json::to_string(&row).expect("serialize");
142 let back: DiffRow = serde_json::from_str(&json).expect("deserialize");
143 assert_eq!(back, row);
144 }
145
146 #[test]
147 fn diff_row_field_names_stable() {
148 let row = sample_diff_row();
149 let value = serde_json::to_value(&row).expect("to_value");
150 for key in [
151 "lang",
152 "old_code",
153 "new_code",
154 "delta_code",
155 "old_lines",
156 "new_lines",
157 "delta_lines",
158 "old_files",
159 "new_files",
160 "delta_files",
161 "old_bytes",
162 "new_bytes",
163 "delta_bytes",
164 "old_tokens",
165 "new_tokens",
166 "delta_tokens",
167 ] {
168 assert!(
169 value.get(key).is_some(),
170 "missing expected field `{key}` in DiffRow JSON: {value}"
171 );
172 }
173 }
174
175 #[test]
177 fn diff_totals_serde_roundtrip() {
178 let totals = sample_diff_totals();
179 let json = serde_json::to_string(&totals).expect("serialize");
180 let back: DiffTotals = serde_json::from_str(&json).expect("deserialize");
181 assert_eq!(back, totals);
182 }
183
184 #[test]
185 fn diff_totals_default_is_all_zero() {
186 let totals = DiffTotals::default();
187 let value = serde_json::to_value(&totals).expect("to_value");
188 for key in [
189 "old_code",
190 "new_code",
191 "delta_code",
192 "old_lines",
193 "new_lines",
194 "delta_lines",
195 "old_files",
196 "new_files",
197 "delta_files",
198 "old_bytes",
199 "new_bytes",
200 "delta_bytes",
201 "old_tokens",
202 "new_tokens",
203 "delta_tokens",
204 ] {
205 assert_eq!(
206 value[key], 0,
207 "expected default DiffTotals.{key} to serialize to 0, got {}",
208 value[key]
209 );
210 }
211 }
212
213 #[test]
214 fn diff_totals_negative_deltas_roundtrip() {
215 let totals = DiffTotals {
216 old_code: 200,
217 new_code: 50,
218 delta_code: -150,
219 old_files: 8,
220 new_files: 4,
221 delta_files: -4,
222 ..DiffTotals::default()
223 };
224 let json = serde_json::to_string(&totals).expect("serialize");
225 let back: DiffTotals = serde_json::from_str(&json).expect("deserialize");
226 assert_eq!(back, totals);
227 assert_eq!(back.delta_code, -150);
228 assert_eq!(back.delta_files, -4);
229 }
230
231 #[test]
233 fn diff_receipt_serde_roundtrip() {
234 let receipt = DiffReceipt {
235 schema_version: crate::SCHEMA_VERSION,
236 generated_at_ms: 1_700_000_000_000,
237 tool: ToolInfo {
238 name: "tokmd".into(),
239 version: "1.2.3".into(),
240 },
241 mode: "diff".into(),
242 from_source: "old.json".into(),
243 to_source: "new.json".into(),
244 diff_rows: vec![sample_diff_row()],
245 totals: sample_diff_totals(),
246 };
247
248 let json = serde_json::to_string(&receipt).expect("serialize");
249 let value: serde_json::Value = serde_json::from_str(&json).expect("to value");
250 for key in [
252 "schema_version",
253 "generated_at_ms",
254 "tool",
255 "mode",
256 "from_source",
257 "to_source",
258 "diff_rows",
259 "totals",
260 ] {
261 assert!(
262 value.get(key).is_some(),
263 "missing expected field `{key}` in DiffReceipt JSON"
264 );
265 }
266
267 let back: DiffReceipt = serde_json::from_str(&json).expect("deserialize");
268 assert_eq!(back.schema_version, receipt.schema_version);
269 assert_eq!(back.mode, "diff");
270 assert_eq!(back.from_source, "old.json");
271 assert_eq!(back.to_source, "new.json");
272 assert_eq!(back.diff_rows.len(), 1);
273 assert_eq!(back.diff_rows[0], receipt.diff_rows[0]);
274 assert_eq!(back.totals, receipt.totals);
275 }
276
277 #[test]
278 fn diff_receipt_preserves_row_order() {
279 let mut rows = Vec::new();
280 for (i, lang) in ["Rust", "Go", "C++", "Python"].iter().enumerate() {
281 let mut row = sample_diff_row();
282 row.lang = (*lang).to_string();
283 row.delta_code = i as i64;
284 rows.push(row);
285 }
286 let receipt = DiffReceipt {
287 schema_version: crate::SCHEMA_VERSION,
288 generated_at_ms: 0,
289 tool: ToolInfo::default(),
290 mode: "diff".into(),
291 from_source: "a".into(),
292 to_source: "b".into(),
293 diff_rows: rows.clone(),
294 totals: DiffTotals::default(),
295 };
296 let json = serde_json::to_string(&receipt).expect("serialize");
297 let back: DiffReceipt = serde_json::from_str(&json).expect("deserialize");
298 let back_langs: Vec<_> = back.diff_rows.iter().map(|r| r.lang.as_str()).collect();
299 assert_eq!(back_langs, vec!["Rust", "Go", "C++", "Python"]);
300 }
301}