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}