rta-core 0.3.0

Core analyzers and report model for the Rust Technical Audit Toolkit
Documentation
use crate::model::{AuditReport, RiskFinding, Severity};

pub fn render_json(report: &AuditReport) -> String {
    format!(
        concat!(
            "{{\n",
            "  \"repository_path\": {},\n",
            "  \"overall_score\": {},\n",
            "  \"overview\": {},\n",
            "  \"dependencies\": {},\n",
            "  \"code_quality\": {},\n",
            "  \"architecture\": {},\n",
            "  \"testing\": {},\n",
            "  \"risks\": {}\n",
            "}}\n"
        ),
        quoted(&report.repository_path),
        report.overall_score,
        overview_json(report),
        dependencies_json(report),
        code_quality_json(report),
        architecture_json(report),
        testing_json(report),
        risks_json(report)
    )
}

pub fn render_scorecard_json(report: &AuditReport) -> String {
    let risk_counts = RiskCounts::from_report(report);
    format!(
        concat!(
            "{{\n",
            "  \"schema_version\": \"rta.scorecard.v2\",\n",
            "  \"repository_path\": {},\n",
            "  \"overall_score\": {},\n",
            "  \"scores\": {{\n",
            "    \"dependency_health\": {},\n",
            "    \"code_quality\": {},\n",
            "    \"architecture\": {},\n",
            "    \"testing\": {},\n",
            "    \"risk_posture\": {}\n",
            "  }},\n",
            "  \"metrics\": {{\n",
            "    \"crate_count\": {},\n",
            "    \"package_count\": {},\n",
            "    \"direct_dependencies\": {},\n",
            "    \"lines_of_rust_code\": {},\n",
            "    \"rust_modules\": {},\n",
            "    \"function_count\": {},\n",
            "    \"average_function_size\": {:.1},\n",
            "    \"unit_test_files\": {},\n",
            "    \"integration_test_files\": {},\n",
            "    \"test_function_count\": {}\n",
            "  }},\n",
            "  \"risk_findings\": {{\n",
            "    \"total\": {},\n",
            "    \"high\": {},\n",
            "    \"medium\": {},\n",
            "    \"low\": {}\n",
            "  }}\n",
            "}}\n"
        ),
        quoted(&report.repository_path),
        report.overall_score,
        report.dependencies.score,
        report.code_quality.score,
        report.architecture.score,
        report.testing.score,
        report.risks.score,
        report.overview.crate_count,
        report.overview.package_count,
        report.dependencies.direct_dependencies,
        report.code_quality.lines_of_code,
        report.code_quality.module_count,
        report.code_quality.function_count,
        report.code_quality.average_function_size,
        report.testing.unit_test_files,
        report.testing.integration_test_files,
        report.testing.test_function_count,
        report.risks.findings.len(),
        risk_counts.high,
        risk_counts.medium,
        risk_counts.low
    )
}

#[derive(Debug, Default)]
struct RiskCounts {
    high: usize,
    medium: usize,
    low: usize,
}

impl RiskCounts {
    fn from_report(report: &AuditReport) -> Self {
        let mut counts = Self::default();
        for finding in &report.risks.findings {
            match finding.severity {
                Severity::High => counts.high += 1,
                Severity::Medium => counts.medium += 1,
                Severity::Low => counts.low += 1,
            }
        }
        counts
    }
}

fn overview_json(report: &AuditReport) -> String {
    let languages = report
        .overview
        .languages
        .iter()
        .map(|language| {
            format!(
                "{{\"language\":{},\"files\":{},\"lines\":{}}}",
                quoted(&language.language),
                language.files,
                language.lines
            )
        })
        .collect::<Vec<_>>()
        .join(",");
    format!(
        "{{\"crate_count\":{},\"package_count\":{},\"workspace_members\":{},\"total_files\":{},\"total_bytes\":{},\"languages\":[{}],\"cargo_configs\":{},\"summary\":{}}}",
        report.overview.crate_count,
        report.overview.package_count,
        string_array(&report.overview.workspace_members),
        report.overview.total_files,
        report.overview.total_bytes,
        languages,
        string_array(&report.overview.cargo_configs),
        quoted(&report.overview.summary)
    )
}

fn dependencies_json(report: &AuditReport) -> String {
    format!(
        "{{\"total_dependencies\":{},\"direct_dependencies\":{},\"critical_dependencies\":{},\"outdated_indicators\":{},\"maintenance_risks\":{},\"score\":{}}}",
        report.dependencies.total_dependencies,
        report.dependencies.direct_dependencies,
        string_array(&report.dependencies.critical_dependencies),
        string_array(&report.dependencies.outdated_indicators),
        string_array(&report.dependencies.maintenance_risks),
        report.dependencies.score
    )
}

fn code_quality_json(report: &AuditReport) -> String {
    format!(
        "{{\"lines_of_code\":{},\"module_count\":{},\"function_count\":{},\"average_function_size\":{:.1},\"complexity_indicators\":{},\"large_modules\":{},\"god_module_candidates\":{},\"score\":{}}}",
        report.code_quality.lines_of_code,
        report.code_quality.module_count,
        report.code_quality.function_count,
        report.code_quality.average_function_size,
        string_array(&report.code_quality.complexity_indicators),
        string_array(&report.code_quality.large_modules),
        string_array(&report.code_quality.god_module_candidates),
        report.code_quality.score
    )
}

fn architecture_json(report: &AuditReport) -> String {
    format!(
        "{{\"detected_layers\":{},\"domain_boundaries\":{},\"module_centralization_risks\":{},\"circular_dependencies\":{},\"architecture_style\":{},\"separation_of_concerns\":{},\"score\":{}}}",
        string_array(&report.architecture.detected_layers),
        string_array(&report.architecture.domain_boundaries),
        string_array(&report.architecture.module_centralization_risks),
        string_array(&report.architecture.circular_dependencies),
        quoted(&report.architecture.architecture_style),
        quoted(&report.architecture.separation_of_concerns),
        report.architecture.score
    )
}

fn testing_json(report: &AuditReport) -> String {
    format!(
        "{{\"has_tests\":{},\"unit_test_files\":{},\"integration_test_files\":{},\"test_function_count\":{},\"testing_structure\":{},\"score\":{}}}",
        report.testing.has_tests,
        report.testing.unit_test_files,
        report.testing.integration_test_files,
        report.testing.test_function_count,
        quoted(&report.testing.testing_structure),
        report.testing.score
    )
}

fn risks_json(report: &AuditReport) -> String {
    let findings = report
        .risks
        .findings
        .iter()
        .map(risk_json)
        .collect::<Vec<_>>()
        .join(",");
    format!(
        "{{\"score\":{},\"findings\":[{}]}}",
        report.risks.score, findings
    )
}

fn risk_json(finding: &RiskFinding) -> String {
    format!(
        "{{\"severity\":{},\"title\":{},\"evidence\":{},\"recommendation\":{}}}",
        quoted(finding.severity.as_str()),
        quoted(&finding.title),
        quoted(&finding.evidence),
        quoted(&finding.recommendation)
    )
}

fn string_array(values: &[String]) -> String {
    let values = values
        .iter()
        .map(|value| quoted(value))
        .collect::<Vec<_>>()
        .join(",");
    format!("[{values}]")
}

fn quoted(value: &str) -> String {
    let mut out = String::from("\"");
    for ch in value.chars() {
        match ch {
            '"' => out.push_str("\\\""),
            '\\' => out.push_str("\\\\"),
            '\n' => out.push_str("\\n"),
            '\r' => out.push_str("\\r"),
            '\t' => out.push_str("\\t"),
            ch if ch.is_control() => out.push(' '),
            ch => out.push(ch),
        }
    }
    out.push('"');
    out
}

#[cfg(test)]
mod tests {
    use crate::model::{
        ArchitectureAssessment, AuditReport, CodeQuality, DependencyHealth, Overview, RiskReport,
        TestingMaturity,
    };

    use super::{quoted, render_scorecard_json};

    #[test]
    fn escapes_json_strings() {
        assert_eq!(quoted("a\"b\\c"), "\"a\\\"b\\\\c\"");
    }

    #[test]
    fn renders_scorecard_schema_version() {
        let report = AuditReport {
            repository_path: "/tmp/repo".into(),
            overview: Overview {
                crate_count: 1,
                package_count: 1,
                workspace_members: Vec::new(),
                total_files: 1,
                total_bytes: 1,
                languages: Vec::new(),
                cargo_configs: Vec::new(),
                summary: "single package".into(),
            },
            dependencies: DependencyHealth {
                total_dependencies: 2,
                direct_dependencies: 2,
                critical_dependencies: Vec::new(),
                outdated_indicators: Vec::new(),
                maintenance_risks: Vec::new(),
                score: 90,
            },
            code_quality: CodeQuality {
                lines_of_code: 100,
                module_count: 2,
                function_count: 5,
                average_function_size: 20.0,
                complexity_indicators: Vec::new(),
                large_modules: Vec::new(),
                god_module_candidates: Vec::new(),
                score: 80,
            },
            architecture: ArchitectureAssessment {
                detected_layers: Vec::new(),
                domain_boundaries: Vec::new(),
                module_centralization_risks: Vec::new(),
                circular_dependencies: Vec::new(),
                architecture_style: "compact".into(),
                separation_of_concerns: "clear".into(),
                score: 75,
            },
            testing: TestingMaturity {
                has_tests: true,
                unit_test_files: 1,
                integration_test_files: 0,
                test_function_count: 2,
                testing_structure: "Unit tests detected.".into(),
                score: 70,
            },
            risks: RiskReport {
                findings: Vec::new(),
                score: 100,
            },
            overall_score: 82,
        };

        let rendered = render_scorecard_json(&report);

        assert!(rendered.contains("\"schema_version\": \"rta.scorecard.v2\""));
        assert!(rendered.contains("\"overall_score\": 82"));
        assert!(rendered.contains("\"risk_findings\""));
    }
}