1use crate::parser::differ::DiffResult;
2use serde_json::{json, Value};
3
4pub fn diff_json_value(result: &DiffResult) -> Value {
5 let changes: Vec<Value> = result
6 .changes
7 .iter()
8 .map(|c| {
9 json!({
10 "entityId": c.entity_id,
11 "changeType": c.change_type,
12 "entityType": c.entity_type,
13 "entityName": c.entity_name,
14 "startLine": c.start_line,
15 "endLine": c.end_line,
16 "oldStartLine": c.old_start_line,
17 "oldEndLine": c.old_end_line,
18 "oldEntityName": c.old_entity_name,
19 "filePath": c.file_path,
20 "oldFilePath": c.old_file_path,
21 "oldParentId": c.old_parent_id,
22 "beforeContent": c.before_content,
23 "afterContent": c.after_content,
24 "commitSha": c.commit_sha,
25 "author": c.author,
26 "structuralChange": c.structural_change,
27 })
28 })
29 .collect();
30
31 json!({
32 "summary": {
33 "fileCount": result.file_count,
34 "added": result.added_count,
35 "modified": result.modified_count,
36 "deleted": result.deleted_count,
37 "moved": result.moved_count,
38 "renamed": result.renamed_count,
39 "reordered": result.reordered_count,
40 "orphan": result.orphan_count,
41 "total": result.changes.len(),
42 },
43 "changes": changes,
44 })
45}
46
47pub fn format_diff_json(result: &DiffResult) -> String {
48 serde_json::to_string(&diff_json_value(result)).unwrap_or_default()
49}
50
51#[cfg(test)]
52mod tests {
53 use super::*;
54 use crate::model::change::{ChangeType, SemanticChange};
55
56 #[test]
57 fn diff_json_value_matches_cli_envelope() {
58 let result = DiffResult {
59 changes: vec![SemanticChange {
60 id: "internal-change-id".to_string(),
61 entity_id: "src/lib.rs::function::foo".to_string(),
62 change_type: ChangeType::Modified,
63 entity_type: "function".to_string(),
64 entity_name: "foo".to_string(),
65 entity_line: 12,
66 start_line: 12,
67 end_line: 12,
68 old_start_line: None,
69 old_end_line: None,
70 parent_name: Some("module".to_string()),
71 file_path: "src/lib.rs".to_string(),
72 old_entity_name: Some("bar".to_string()),
73 old_file_path: Some("src/old.rs".to_string()),
74 old_parent_id: Some("old-parent".to_string()),
75 before_content: Some("fn bar() {}".to_string()),
76 after_content: Some("fn foo() {}".to_string()),
77 commit_sha: Some("abc123".to_string()),
78 author: Some("Ada".to_string()),
79 timestamp: Some("2026-05-26".to_string()),
80 structural_change: Some(true),
81 }],
82 file_count: 1,
83 added_count: 0,
84 modified_count: 1,
85 deleted_count: 0,
86 moved_count: 0,
87 renamed_count: 0,
88 reordered_count: 0,
89 orphan_count: 0,
90 total_entities_before: 1,
91 total_entities_after: 1,
92 };
93
94 let value = diff_json_value(&result);
95
96 assert_eq!(
97 value,
98 json!({
99 "summary": {
100 "fileCount": 1,
101 "added": 0,
102 "modified": 1,
103 "deleted": 0,
104 "moved": 0,
105 "renamed": 0,
106 "reordered": 0,
107 "orphan": 0,
108 "total": 1,
109 },
110 "changes": [{
111 "entityId": "src/lib.rs::function::foo",
112 "changeType": "modified",
113 "entityType": "function",
114 "entityName": "foo",
115 "startLine": 12,
116 "endLine": 12,
117 "oldStartLine": null,
118 "oldEndLine": null,
119 "oldEntityName": "bar",
120 "filePath": "src/lib.rs",
121 "oldFilePath": "src/old.rs",
122 "oldParentId": "old-parent",
123 "beforeContent": "fn bar() {}",
124 "afterContent": "fn foo() {}",
125 "commitSha": "abc123",
126 "author": "Ada",
127 "structuralChange": true,
128 }],
129 })
130 );
131 }
132}