Skip to main content

rta_core/
json.rs

1use crate::model::{AuditReport, RiskFinding, Severity};
2
3pub fn render_json(report: &AuditReport) -> String {
4    format!(
5        concat!(
6            "{{\n",
7            "  \"repository_path\": {},\n",
8            "  \"overall_score\": {},\n",
9            "  \"overview\": {},\n",
10            "  \"dependencies\": {},\n",
11            "  \"code_quality\": {},\n",
12            "  \"architecture\": {},\n",
13            "  \"testing\": {},\n",
14            "  \"risks\": {}\n",
15            "}}\n"
16        ),
17        quoted(&report.repository_path),
18        report.overall_score,
19        overview_json(report),
20        dependencies_json(report),
21        code_quality_json(report),
22        architecture_json(report),
23        testing_json(report),
24        risks_json(report)
25    )
26}
27
28pub fn render_scorecard_json(report: &AuditReport) -> String {
29    let risk_counts = RiskCounts::from_report(report);
30    format!(
31        concat!(
32            "{{\n",
33            "  \"schema_version\": \"rta.scorecard.v2\",\n",
34            "  \"repository_path\": {},\n",
35            "  \"overall_score\": {},\n",
36            "  \"scores\": {{\n",
37            "    \"dependency_health\": {},\n",
38            "    \"code_quality\": {},\n",
39            "    \"architecture\": {},\n",
40            "    \"testing\": {},\n",
41            "    \"risk_posture\": {}\n",
42            "  }},\n",
43            "  \"metrics\": {{\n",
44            "    \"crate_count\": {},\n",
45            "    \"package_count\": {},\n",
46            "    \"direct_dependencies\": {},\n",
47            "    \"lines_of_rust_code\": {},\n",
48            "    \"rust_modules\": {},\n",
49            "    \"function_count\": {},\n",
50            "    \"average_function_size\": {:.1},\n",
51            "    \"unit_test_files\": {},\n",
52            "    \"integration_test_files\": {},\n",
53            "    \"test_function_count\": {}\n",
54            "  }},\n",
55            "  \"risk_findings\": {{\n",
56            "    \"total\": {},\n",
57            "    \"high\": {},\n",
58            "    \"medium\": {},\n",
59            "    \"low\": {}\n",
60            "  }}\n",
61            "}}\n"
62        ),
63        quoted(&report.repository_path),
64        report.overall_score,
65        report.dependencies.score,
66        report.code_quality.score,
67        report.architecture.score,
68        report.testing.score,
69        report.risks.score,
70        report.overview.crate_count,
71        report.overview.package_count,
72        report.dependencies.direct_dependencies,
73        report.code_quality.lines_of_code,
74        report.code_quality.module_count,
75        report.code_quality.function_count,
76        report.code_quality.average_function_size,
77        report.testing.unit_test_files,
78        report.testing.integration_test_files,
79        report.testing.test_function_count,
80        report.risks.findings.len(),
81        risk_counts.high,
82        risk_counts.medium,
83        risk_counts.low
84    )
85}
86
87#[derive(Debug, Default)]
88struct RiskCounts {
89    high: usize,
90    medium: usize,
91    low: usize,
92}
93
94impl RiskCounts {
95    fn from_report(report: &AuditReport) -> Self {
96        let mut counts = Self::default();
97        for finding in &report.risks.findings {
98            match finding.severity {
99                Severity::High => counts.high += 1,
100                Severity::Medium => counts.medium += 1,
101                Severity::Low => counts.low += 1,
102            }
103        }
104        counts
105    }
106}
107
108fn overview_json(report: &AuditReport) -> String {
109    let languages = report
110        .overview
111        .languages
112        .iter()
113        .map(|language| {
114            format!(
115                "{{\"language\":{},\"files\":{},\"lines\":{}}}",
116                quoted(&language.language),
117                language.files,
118                language.lines
119            )
120        })
121        .collect::<Vec<_>>()
122        .join(",");
123    format!(
124        "{{\"crate_count\":{},\"package_count\":{},\"workspace_members\":{},\"total_files\":{},\"total_bytes\":{},\"languages\":[{}],\"cargo_configs\":{},\"summary\":{}}}",
125        report.overview.crate_count,
126        report.overview.package_count,
127        string_array(&report.overview.workspace_members),
128        report.overview.total_files,
129        report.overview.total_bytes,
130        languages,
131        string_array(&report.overview.cargo_configs),
132        quoted(&report.overview.summary)
133    )
134}
135
136fn dependencies_json(report: &AuditReport) -> String {
137    format!(
138        "{{\"total_dependencies\":{},\"direct_dependencies\":{},\"critical_dependencies\":{},\"outdated_indicators\":{},\"maintenance_risks\":{},\"score\":{}}}",
139        report.dependencies.total_dependencies,
140        report.dependencies.direct_dependencies,
141        string_array(&report.dependencies.critical_dependencies),
142        string_array(&report.dependencies.outdated_indicators),
143        string_array(&report.dependencies.maintenance_risks),
144        report.dependencies.score
145    )
146}
147
148fn code_quality_json(report: &AuditReport) -> String {
149    format!(
150        "{{\"lines_of_code\":{},\"module_count\":{},\"function_count\":{},\"average_function_size\":{:.1},\"complexity_indicators\":{},\"large_modules\":{},\"god_module_candidates\":{},\"score\":{}}}",
151        report.code_quality.lines_of_code,
152        report.code_quality.module_count,
153        report.code_quality.function_count,
154        report.code_quality.average_function_size,
155        string_array(&report.code_quality.complexity_indicators),
156        string_array(&report.code_quality.large_modules),
157        string_array(&report.code_quality.god_module_candidates),
158        report.code_quality.score
159    )
160}
161
162fn architecture_json(report: &AuditReport) -> String {
163    format!(
164        "{{\"detected_layers\":{},\"domain_boundaries\":{},\"module_centralization_risks\":{},\"circular_dependencies\":{},\"architecture_style\":{},\"separation_of_concerns\":{},\"score\":{}}}",
165        string_array(&report.architecture.detected_layers),
166        string_array(&report.architecture.domain_boundaries),
167        string_array(&report.architecture.module_centralization_risks),
168        string_array(&report.architecture.circular_dependencies),
169        quoted(&report.architecture.architecture_style),
170        quoted(&report.architecture.separation_of_concerns),
171        report.architecture.score
172    )
173}
174
175fn testing_json(report: &AuditReport) -> String {
176    format!(
177        "{{\"has_tests\":{},\"unit_test_files\":{},\"integration_test_files\":{},\"test_function_count\":{},\"testing_structure\":{},\"score\":{}}}",
178        report.testing.has_tests,
179        report.testing.unit_test_files,
180        report.testing.integration_test_files,
181        report.testing.test_function_count,
182        quoted(&report.testing.testing_structure),
183        report.testing.score
184    )
185}
186
187fn risks_json(report: &AuditReport) -> String {
188    let findings = report
189        .risks
190        .findings
191        .iter()
192        .map(risk_json)
193        .collect::<Vec<_>>()
194        .join(",");
195    format!(
196        "{{\"score\":{},\"findings\":[{}]}}",
197        report.risks.score, findings
198    )
199}
200
201fn risk_json(finding: &RiskFinding) -> String {
202    format!(
203        "{{\"severity\":{},\"title\":{},\"evidence\":{},\"recommendation\":{}}}",
204        quoted(finding.severity.as_str()),
205        quoted(&finding.title),
206        quoted(&finding.evidence),
207        quoted(&finding.recommendation)
208    )
209}
210
211fn string_array(values: &[String]) -> String {
212    let values = values
213        .iter()
214        .map(|value| quoted(value))
215        .collect::<Vec<_>>()
216        .join(",");
217    format!("[{values}]")
218}
219
220fn quoted(value: &str) -> String {
221    let mut out = String::from("\"");
222    for ch in value.chars() {
223        match ch {
224            '"' => out.push_str("\\\""),
225            '\\' => out.push_str("\\\\"),
226            '\n' => out.push_str("\\n"),
227            '\r' => out.push_str("\\r"),
228            '\t' => out.push_str("\\t"),
229            ch if ch.is_control() => out.push(' '),
230            ch => out.push(ch),
231        }
232    }
233    out.push('"');
234    out
235}
236
237#[cfg(test)]
238mod tests {
239    use crate::model::{
240        ArchitectureAssessment, AuditReport, CodeQuality, DependencyHealth, Overview, RiskReport,
241        TestingMaturity,
242    };
243
244    use super::{quoted, render_scorecard_json};
245
246    #[test]
247    fn escapes_json_strings() {
248        assert_eq!(quoted("a\"b\\c"), "\"a\\\"b\\\\c\"");
249    }
250
251    #[test]
252    fn renders_scorecard_schema_version() {
253        let report = AuditReport {
254            repository_path: "/tmp/repo".into(),
255            overview: Overview {
256                crate_count: 1,
257                package_count: 1,
258                workspace_members: Vec::new(),
259                total_files: 1,
260                total_bytes: 1,
261                languages: Vec::new(),
262                cargo_configs: Vec::new(),
263                summary: "single package".into(),
264            },
265            dependencies: DependencyHealth {
266                total_dependencies: 2,
267                direct_dependencies: 2,
268                critical_dependencies: Vec::new(),
269                outdated_indicators: Vec::new(),
270                maintenance_risks: Vec::new(),
271                score: 90,
272            },
273            code_quality: CodeQuality {
274                lines_of_code: 100,
275                module_count: 2,
276                function_count: 5,
277                average_function_size: 20.0,
278                complexity_indicators: Vec::new(),
279                large_modules: Vec::new(),
280                god_module_candidates: Vec::new(),
281                score: 80,
282            },
283            architecture: ArchitectureAssessment {
284                detected_layers: Vec::new(),
285                domain_boundaries: Vec::new(),
286                module_centralization_risks: Vec::new(),
287                circular_dependencies: Vec::new(),
288                architecture_style: "compact".into(),
289                separation_of_concerns: "clear".into(),
290                score: 75,
291            },
292            testing: TestingMaturity {
293                has_tests: true,
294                unit_test_files: 1,
295                integration_test_files: 0,
296                test_function_count: 2,
297                testing_structure: "Unit tests detected.".into(),
298                score: 70,
299            },
300            risks: RiskReport {
301                findings: Vec::new(),
302                score: 100,
303            },
304            overall_score: 82,
305        };
306
307        let rendered = render_scorecard_json(&report);
308
309        assert!(rendered.contains("\"schema_version\": \"rta.scorecard.v2\""));
310        assert!(rendered.contains("\"overall_score\": 82"));
311        assert!(rendered.contains("\"risk_findings\""));
312    }
313}