use super::severity_str;
use crate::report::CheckReport;
use serde::Serialize;
use std::collections::BTreeMap;
pub const SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Serialize, serde::Deserialize, PartialEq)]
pub struct ReportDocument {
pub schema_version: u32,
pub findings: Vec<FindingDocument>,
pub summary: BTreeMap<String, usize>,
}
#[derive(Debug, Serialize, serde::Deserialize, PartialEq)]
pub struct FindingDocument {
pub ecosystem: String,
pub manifest_path: String,
pub dependency_name: Option<String>,
pub requirement: Option<String>,
pub category: String,
pub severity: String,
pub range: RangeDocument,
pub message: String,
}
#[derive(Debug, Serialize, serde::Deserialize, PartialEq)]
pub struct RangeDocument {
pub start: PositionDocument,
pub end: PositionDocument,
}
#[derive(Debug, Serialize, serde::Deserialize, PartialEq)]
pub struct PositionDocument {
pub line: u32,
pub character: u32,
}
#[must_use]
pub fn to_document(report: &CheckReport) -> ReportDocument {
let findings = report
.findings
.iter()
.map(|finding| FindingDocument {
ecosystem: finding.ecosystem.id().to_string(),
manifest_path: finding.manifest_path.display().to_string(),
dependency_name: finding.dependency_name.clone(),
requirement: finding.requirement.clone(),
category: finding.category.as_str().to_string(),
severity: severity_str(finding.severity).to_string(),
range: RangeDocument {
start: PositionDocument {
line: finding.range.start.line,
character: finding.range.start.character,
},
end: PositionDocument {
line: finding.range.end.line,
character: finding.range.end.character,
},
},
message: finding.message.clone(),
})
.collect();
let summary = report
.summary()
.into_iter()
.map(|(category, count)| (category.as_str().to_string(), count))
.collect();
ReportDocument {
schema_version: SCHEMA_VERSION,
findings,
summary,
}
}
pub fn render(report: &CheckReport) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(&to_document(report))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::report::{Category, CheckFinding};
use deps_core::EcosystemId;
use deps_core::diagnostic::Severity;
use deps_core::position::{Position, Range};
use std::path::PathBuf;
fn finding() -> CheckFinding {
CheckFinding {
ecosystem: EcosystemId::Cargo,
manifest_path: PathBuf::from("Cargo.toml"),
dependency_name: Some("serde".to_string()),
requirement: Some("1.0".to_string()),
category: Category::Outdated,
code: None,
advisory_url: None,
advisory_severity: None,
severity: Severity::Hint,
range: Range::new(Position::new(4, 0), Position::new(4, 10)),
message: "Newer version available: 1.1.0".to_string(),
}
}
#[test]
fn test_to_document_empty_report() {
let document = to_document(&CheckReport::default());
assert_eq!(document.schema_version, SCHEMA_VERSION);
assert!(document.findings.is_empty());
assert!(document.summary.is_empty());
}
#[test]
fn test_to_document_maps_finding_fields() {
let document = to_document(&CheckReport {
findings: vec![finding()],
});
let f = &document.findings[0];
assert_eq!(f.ecosystem, "cargo");
assert_eq!(f.manifest_path, "Cargo.toml");
assert_eq!(f.dependency_name.as_deref(), Some("serde"));
assert_eq!(f.requirement.as_deref(), Some("1.0"));
assert_eq!(f.category, "outdated");
assert_eq!(f.severity, "hint");
assert_eq!(f.range.start.line, 4);
assert_eq!(document.summary.get("outdated"), Some(&1));
}
#[test]
fn test_render_round_trips_through_serde_json() {
let report = CheckReport {
findings: vec![finding()],
};
let rendered = render(&report).expect("render must succeed");
let parsed: ReportDocument = serde_json::from_str(&rendered).expect("must round-trip");
assert_eq!(parsed, to_document(&report));
}
#[test]
fn test_render_includes_schema_version_field() {
let rendered = render(&CheckReport::default()).expect("render must succeed");
assert!(rendered.contains("\"schema_version\": 1"));
}
#[test]
fn test_to_document_snapshot() {
let mut other = finding();
other.category = Category::Vulnerable;
other.severity = Severity::Error;
other.manifest_path = PathBuf::from("package.json");
other.dependency_name = None;
other.requirement = None;
other.message = "GHSA-xxxx-yyyy-zzzz: example advisory".to_string();
let document = to_document(&CheckReport {
findings: vec![finding(), other],
});
insta::assert_json_snapshot!(document);
}
}