1use std::collections::{HashMap, HashSet};
2
3use serde::Serialize;
4
5use nomograph_core::traits::KnowledgeGraph;
6
7use crate::element::SysmlElement;
8use crate::graph::SysmlGraph;
9use crate::relationship::SysmlRelationship;
10
11#[derive(Debug, Clone, Serialize)]
12pub struct DiffResult {
13 pub elements_added: Vec<ElementChange>,
14 pub elements_removed: Vec<ElementChange>,
15 pub elements_modified: Vec<ElementModification>,
16 pub relationships_added: Vec<RelationshipChange>,
17 pub relationships_removed: Vec<RelationshipChange>,
18 pub summary: DiffSummary,
19}
20
21#[derive(Debug, Clone, Serialize)]
22pub struct ElementChange {
23 pub qualified_name: String,
24 pub kind: String,
25 pub file_path: String,
26}
27
28#[derive(Debug, Clone, Serialize)]
29pub struct ElementModification {
30 pub qualified_name: String,
31 pub kind: String,
32 pub changes: Vec<String>,
33}
34
35#[derive(Debug, Clone, Serialize)]
36pub struct RelationshipChange {
37 pub source: String,
38 pub target: String,
39 pub kind: String,
40}
41
42#[derive(Debug, Clone, Serialize)]
43pub struct DiffSummary {
44 pub elements_added: usize,
45 pub elements_removed: usize,
46 pub elements_modified: usize,
47 pub relationships_added: usize,
48 pub relationships_removed: usize,
49 pub total_changes: usize,
50}
51
52fn element_key(e: &SysmlElement) -> String {
53 e.qualified_name.to_lowercase()
54}
55
56fn rel_key(r: &SysmlRelationship) -> String {
57 format!(
58 "{}|{}|{}",
59 r.source.to_lowercase(),
60 r.kind.to_lowercase(),
61 r.target.to_lowercase()
62 )
63}
64
65pub fn diff_graphs(base: &SysmlGraph, head: &SysmlGraph) -> DiffResult {
66 let base_elements: HashMap<String, &SysmlElement> = base
67 .elements()
68 .iter()
69 .map(|e| (element_key(e), e))
70 .collect();
71 let head_elements: HashMap<String, &SysmlElement> = head
72 .elements()
73 .iter()
74 .map(|e| (element_key(e), e))
75 .collect();
76
77 let base_keys: HashSet<&String> = base_elements.keys().collect();
78 let head_keys: HashSet<&String> = head_elements.keys().collect();
79
80 let mut elements_added = Vec::new();
81 for key in head_keys.difference(&base_keys) {
82 if let Some(e) = head_elements.get(*key) {
83 elements_added.push(ElementChange {
84 qualified_name: e.qualified_name.clone(),
85 kind: e.kind.clone(),
86 file_path: e.file_path.to_string_lossy().to_string(),
87 });
88 }
89 }
90 elements_added.sort_by(|a, b| a.qualified_name.cmp(&b.qualified_name));
91
92 let mut elements_removed = Vec::new();
93 for key in base_keys.difference(&head_keys) {
94 if let Some(e) = base_elements.get(*key) {
95 elements_removed.push(ElementChange {
96 qualified_name: e.qualified_name.clone(),
97 kind: e.kind.clone(),
98 file_path: e.file_path.to_string_lossy().to_string(),
99 });
100 }
101 }
102 elements_removed.sort_by(|a, b| a.qualified_name.cmp(&b.qualified_name));
103
104 let mut elements_modified = Vec::new();
105 for key in base_keys.intersection(&head_keys) {
106 if let (Some(base_e), Some(head_e)) = (base_elements.get(*key), head_elements.get(*key)) {
107 let mut changes = Vec::new();
108 if base_e.kind != head_e.kind {
109 changes.push(format!("kind: {} -> {}", base_e.kind, head_e.kind));
110 }
111 if base_e.doc != head_e.doc {
112 changes.push("doc changed".to_string());
113 }
114 if base_e.layer != head_e.layer {
115 changes.push(format!("layer: {:?} -> {:?}", base_e.layer, head_e.layer));
116 }
117 if base_e.members != head_e.members {
118 let base_set: HashSet<&String> = base_e.members.iter().collect();
119 let head_set: HashSet<&String> = head_e.members.iter().collect();
120 let added: Vec<_> = head_set.difference(&base_set).collect();
121 let removed: Vec<_> = base_set.difference(&head_set).collect();
122 if !added.is_empty() {
123 changes.push(format!("members added: {}", added.len()));
124 }
125 if !removed.is_empty() {
126 changes.push(format!("members removed: {}", removed.len()));
127 }
128 }
129 if !changes.is_empty() {
130 elements_modified.push(ElementModification {
131 qualified_name: head_e.qualified_name.clone(),
132 kind: head_e.kind.clone(),
133 changes,
134 });
135 }
136 }
137 }
138 elements_modified.sort_by(|a, b| a.qualified_name.cmp(&b.qualified_name));
139
140 let base_rels: HashSet<String> = base.relationships().iter().map(rel_key).collect();
141 let head_rels: HashSet<String> = head.relationships().iter().map(rel_key).collect();
142 let head_rel_map: HashMap<String, &SysmlRelationship> = head
143 .relationships()
144 .iter()
145 .map(|r| (rel_key(r), r))
146 .collect();
147 let base_rel_map: HashMap<String, &SysmlRelationship> = base
148 .relationships()
149 .iter()
150 .map(|r| (rel_key(r), r))
151 .collect();
152
153 let mut relationships_added: Vec<RelationshipChange> = head_rels
154 .difference(&base_rels)
155 .filter_map(|k| head_rel_map.get(k))
156 .map(|r| RelationshipChange {
157 source: r.source.clone(),
158 target: r.target.clone(),
159 kind: r.kind.clone(),
160 })
161 .collect();
162 relationships_added.sort_by(|a, b| a.source.cmp(&b.source).then(a.kind.cmp(&b.kind)));
163
164 let mut relationships_removed: Vec<RelationshipChange> = base_rels
165 .difference(&head_rels)
166 .filter_map(|k| base_rel_map.get(k))
167 .map(|r| RelationshipChange {
168 source: r.source.clone(),
169 target: r.target.clone(),
170 kind: r.kind.clone(),
171 })
172 .collect();
173 relationships_removed.sort_by(|a, b| a.source.cmp(&b.source).then(a.kind.cmp(&b.kind)));
174
175 let total = elements_added.len()
176 + elements_removed.len()
177 + elements_modified.len()
178 + relationships_added.len()
179 + relationships_removed.len();
180
181 let summary = DiffSummary {
182 elements_added: elements_added.len(),
183 elements_removed: elements_removed.len(),
184 elements_modified: elements_modified.len(),
185 relationships_added: relationships_added.len(),
186 relationships_removed: relationships_removed.len(),
187 total_changes: total,
188 };
189
190 DiffResult {
191 elements_added,
192 elements_removed,
193 elements_modified,
194 relationships_added,
195 relationships_removed,
196 summary,
197 }
198}
199
200pub fn format_compact(result: &DiffResult) -> Vec<String> {
201 let mut lines = Vec::new();
202 for e in &result.elements_added {
203 lines.push(format!("+ {} ({})", e.qualified_name, e.kind));
204 }
205 for e in &result.elements_removed {
206 lines.push(format!("- {} ({})", e.qualified_name, e.kind));
207 }
208 for e in &result.elements_modified {
209 lines.push(format!("~ {} [{}]", e.qualified_name, e.changes.join(", ")));
210 }
211 for r in &result.relationships_added {
212 lines.push(format!("+ {} -> {} -> {}", r.source, r.kind, r.target));
213 }
214 for r in &result.relationships_removed {
215 lines.push(format!("- {} -> {} -> {}", r.source, r.kind, r.target));
216 }
217 lines
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223 use crate::graph::SysmlGraph;
224 use nomograph_core::types::ParseResult;
225 use std::path::PathBuf;
226
227 fn make_element(name: &str, kind: &str) -> SysmlElement {
228 SysmlElement {
229 qualified_name: name.to_string(),
230 kind: kind.to_string(),
231 file_path: PathBuf::from("test.sysml"),
232 span: nomograph_core::types::Span {
233 start_line: 0,
234 start_col: 0,
235 end_line: 0,
236 end_col: 0,
237 },
238 doc: None,
239 attributes: Vec::new(),
240 members: Vec::new(),
241 layer: None,
242 }
243 }
244
245 fn make_rel(source: &str, kind: &str, target: &str) -> SysmlRelationship {
246 SysmlRelationship {
247 source: source.to_string(),
248 target: target.to_string(),
249 kind: kind.to_string(),
250 file_path: PathBuf::from("test.sysml"),
251 span: nomograph_core::types::Span {
252 start_line: 0,
253 start_col: 0,
254 end_line: 0,
255 end_col: 0,
256 },
257 }
258 }
259
260 fn build_graph(
261 elements: Vec<SysmlElement>,
262 relationships: Vec<SysmlRelationship>,
263 ) -> SysmlGraph {
264 let mut graph = SysmlGraph::new();
265 let result = ParseResult {
266 elements,
267 relationships,
268 diagnostics: Vec::new(),
269 };
270 graph.index(vec![result]).unwrap();
271 graph
272 }
273
274 #[test]
275 fn test_diff_no_changes() {
276 let base = build_graph(vec![make_element("A", "part_usage")], vec![]);
277 let head = build_graph(vec![make_element("A", "part_usage")], vec![]);
278 let result = diff_graphs(&base, &head);
279 assert_eq!(result.summary.total_changes, 0);
280 }
281
282 #[test]
283 fn test_diff_element_added() {
284 let base = build_graph(vec![make_element("A", "part_usage")], vec![]);
285 let head = build_graph(
286 vec![
287 make_element("A", "part_usage"),
288 make_element("B", "requirement_usage"),
289 ],
290 vec![],
291 );
292 let result = diff_graphs(&base, &head);
293 assert_eq!(result.summary.elements_added, 1);
294 assert_eq!(result.elements_added[0].qualified_name, "B");
295 }
296
297 #[test]
298 fn test_diff_element_removed() {
299 let base = build_graph(
300 vec![
301 make_element("A", "part_usage"),
302 make_element("B", "requirement_usage"),
303 ],
304 vec![],
305 );
306 let head = build_graph(vec![make_element("A", "part_usage")], vec![]);
307 let result = diff_graphs(&base, &head);
308 assert_eq!(result.summary.elements_removed, 1);
309 assert_eq!(result.elements_removed[0].qualified_name, "B");
310 }
311
312 #[test]
313 fn test_diff_element_modified() {
314 let mut e = make_element("A", "part_usage");
315 e.doc = Some("old doc".to_string());
316 let base = build_graph(vec![e], vec![]);
317
318 let mut e2 = make_element("A", "part_usage");
319 e2.doc = Some("new doc".to_string());
320 let head = build_graph(vec![e2], vec![]);
321
322 let result = diff_graphs(&base, &head);
323 assert_eq!(result.summary.elements_modified, 1);
324 assert!(result.elements_modified[0]
325 .changes
326 .contains(&"doc changed".to_string()));
327 }
328
329 #[test]
330 fn test_diff_relationship_added() {
331 let base = build_graph(vec![make_element("A", "part_usage")], vec![]);
332 let head = build_graph(
333 vec![make_element("A", "part_usage")],
334 vec![make_rel("A", "Satisfy", "B")],
335 );
336 let result = diff_graphs(&base, &head);
337 assert_eq!(result.summary.relationships_added, 1);
338 }
339
340 #[test]
341 fn test_diff_relationship_removed() {
342 let base = build_graph(
343 vec![make_element("A", "part_usage")],
344 vec![make_rel("A", "Satisfy", "B")],
345 );
346 let head = build_graph(vec![make_element("A", "part_usage")], vec![]);
347 let result = diff_graphs(&base, &head);
348 assert_eq!(result.summary.relationships_removed, 1);
349 }
350
351 #[test]
352 fn test_compact_format() {
353 let base = build_graph(vec![make_element("A", "part_usage")], vec![]);
354 let head = build_graph(
355 vec![
356 make_element("A", "part_usage"),
357 make_element("B", "requirement_usage"),
358 ],
359 vec![make_rel("A", "Satisfy", "B")],
360 );
361 let result = diff_graphs(&base, &head);
362 let lines = format_compact(&result);
363 assert!(lines.iter().any(|l| l.starts_with("+ B")));
364 assert!(lines.iter().any(|l| l.contains("Satisfy")));
365 }
366}