Skip to main content

sysml_core/
badge.rs

1use crate::SysmlGraph;
2use nomograph_core::traits::KnowledgeGraph;
3use nomograph_core::types::CheckType;
4
5pub struct BadgeData {
6    pub label: String,
7    pub value: String,
8    pub color: String,
9    pub completeness: f64,
10    pub elements: usize,
11    pub relationships: usize,
12    pub findings: usize,
13}
14
15pub fn compute_badge_data(graph: &SysmlGraph) -> BadgeData {
16    let elements = graph.element_count();
17    let relationships = graph.relationship_count();
18
19    let all_checks = vec![
20        CheckType::OrphanRequirements,
21        CheckType::UnverifiedRequirements,
22        CheckType::MissingVerification,
23        CheckType::UnconnectedPorts,
24        CheckType::DanglingReferences,
25    ];
26
27    let mut total_findings = 0;
28    let mut orphan_count = 0;
29    let mut unverified_count = 0;
30
31    for ct in &all_checks {
32        let findings = graph.check(ct.clone());
33        let count = findings.len();
34        total_findings += count;
35        match ct {
36            CheckType::OrphanRequirements => orphan_count = count,
37            CheckType::UnverifiedRequirements => unverified_count = count,
38            _ => {}
39        }
40    }
41
42    let total_requirements = graph
43        .elements()
44        .iter()
45        .filter(|e| e.kind.to_lowercase().contains("requirement"))
46        .count();
47
48    let completeness = if total_requirements > 0 {
49        let gap = (orphan_count + unverified_count).min(total_requirements);
50        1.0 - (gap as f64 / total_requirements as f64)
51    } else {
52        1.0
53    };
54
55    let pct = (completeness * 100.0).round() as u32;
56    let value = format!("{pct}% ({elements}E / {relationships}R)");
57
58    let color = if total_findings == 0 {
59        "#4c1".to_string()
60    } else if completeness >= 0.8 {
61        "#dfb317".to_string()
62    } else if completeness >= 0.5 {
63        "#fe7d37".to_string()
64    } else {
65        "#e05d44".to_string()
66    };
67
68    BadgeData {
69        label: "model health".to_string(),
70        value,
71        color,
72        completeness,
73        elements,
74        relationships,
75        findings: total_findings,
76    }
77}
78
79pub fn render_svg(data: &BadgeData) -> String {
80    let label = &data.label;
81    let value = &data.value;
82    let color = &data.color;
83
84    let label_width = label.len() as u32 * 7 + 12;
85    let value_width = value.len() as u32 * 7 + 12;
86    let total_width = label_width + value_width;
87
88    let label_x = label_width as f32 / 2.0;
89    let value_x = label_width as f32 + value_width as f32 / 2.0;
90
91    format!(
92        r##"<svg xmlns="http://www.w3.org/2000/svg" width="{total_width}" height="20" role="img" aria-label="{label}: {value}">
93  <title>{label}: {value}</title>
94  <linearGradient id="s" x2="0" y2="100%">
95    <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
96    <stop offset="1" stop-opacity=".1"/>
97  </linearGradient>
98  <clipPath id="r"><rect width="{total_width}" height="20" rx="3" fill="#fff"/></clipPath>
99  <g clip-path="url(#r)">
100    <rect width="{label_width}" height="20" fill="#555"/>
101    <rect x="{label_width}" width="{value_width}" height="20" fill="{color}"/>
102    <rect width="{total_width}" height="20" fill="url(#s)"/>
103  </g>
104  <g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
105    <text x="{label_x}" y="15" fill="#010101" fill-opacity=".3">{label}</text>
106    <text x="{label_x}" y="14" fill="#fff">{label}</text>
107    <text x="{value_x}" y="15" fill="#010101" fill-opacity=".3">{value}</text>
108    <text x="{value_x}" y="14" fill="#fff">{value}</text>
109  </g>
110</svg>"##
111    )
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn test_render_svg_contains_label_and_value() {
120        let data = BadgeData {
121            label: "model health".to_string(),
122            value: "85% (100E / 200R)".to_string(),
123            color: "#dfb317".to_string(),
124            completeness: 0.85,
125            elements: 100,
126            relationships: 200,
127            findings: 3,
128        };
129        let svg = render_svg(&data);
130        assert!(svg.contains("<svg"));
131        assert!(svg.contains("model health"));
132        assert!(svg.contains("85% (100E / 200R)"));
133        assert!(svg.contains("#dfb317"));
134    }
135
136    #[test]
137    fn test_color_thresholds() {
138        let green = BadgeData {
139            label: "x".into(),
140            value: "x".into(),
141            color: String::new(),
142            completeness: 1.0,
143            elements: 0,
144            relationships: 0,
145            findings: 0,
146        };
147        assert_eq!(compute_color(1.0, 0), "#4c1");
148        assert_eq!(compute_color(0.9, 2), "#dfb317");
149        assert_eq!(compute_color(0.6, 5), "#fe7d37");
150        assert_eq!(compute_color(0.3, 10), "#e05d44");
151        let _ = green;
152    }
153
154    fn compute_color(completeness: f64, findings: usize) -> &'static str {
155        if findings == 0 {
156            "#4c1"
157        } else if completeness >= 0.8 {
158            "#dfb317"
159        } else if completeness >= 0.5 {
160            "#fe7d37"
161        } else {
162            "#e05d44"
163        }
164    }
165
166    #[test]
167    fn test_svg_is_valid_xml() {
168        let data = BadgeData {
169            label: "model health".to_string(),
170            value: "100% (50E / 80R)".to_string(),
171            color: "#4c1".to_string(),
172            completeness: 1.0,
173            elements: 50,
174            relationships: 80,
175            findings: 0,
176        };
177        let svg = render_svg(&data);
178        assert!(svg.starts_with("<svg"));
179        assert!(svg.ends_with("</svg>"));
180        assert!(svg.contains("</g>"));
181    }
182
183    #[test]
184    fn test_badge_data_from_eve_model() {
185        use nomograph_core::traits::KnowledgeGraph;
186        let results = crate::graph::tests::parse_all_eve();
187        let mut graph = crate::SysmlGraph::new();
188        graph.index(results).unwrap();
189        let data = compute_badge_data(&graph);
190        assert!(data.elements > 0);
191        assert!(data.relationships > 0);
192        assert!(data.completeness >= 0.0);
193        assert!(data.completeness <= 1.0);
194        assert!(!data.value.is_empty());
195
196        let svg = render_svg(&data);
197        assert!(svg.contains("model health"));
198        assert!(svg.contains(&format!("{}E", data.elements)));
199    }
200}