Skip to main content

sysml_core/
diff.rs

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}