1use super::severity_str;
9use crate::report::CheckReport;
10use serde::Serialize;
11use std::collections::BTreeMap;
12
13pub const SCHEMA_VERSION: u32 = 1;
15
16#[derive(Debug, Serialize, serde::Deserialize, PartialEq)]
18pub struct ReportDocument {
19 pub schema_version: u32,
21 pub findings: Vec<FindingDocument>,
23 pub summary: BTreeMap<String, usize>,
25}
26
27#[derive(Debug, Serialize, serde::Deserialize, PartialEq)]
29pub struct FindingDocument {
30 pub ecosystem: String,
32 pub manifest_path: String,
34 pub dependency_name: Option<String>,
36 pub requirement: Option<String>,
38 pub category: String,
40 pub severity: String,
42 pub range: RangeDocument,
44 pub message: String,
46}
47
48#[derive(Debug, Serialize, serde::Deserialize, PartialEq)]
50pub struct RangeDocument {
51 pub start: PositionDocument,
53 pub end: PositionDocument,
55}
56
57#[derive(Debug, Serialize, serde::Deserialize, PartialEq)]
59pub struct PositionDocument {
60 pub line: u32,
62 pub character: u32,
64}
65
66#[must_use]
79pub fn to_document(report: &CheckReport) -> ReportDocument {
80 let findings = report
81 .findings
82 .iter()
83 .map(|finding| FindingDocument {
84 ecosystem: finding.ecosystem.id().to_string(),
85 manifest_path: finding.manifest_path.display().to_string(),
86 dependency_name: finding.dependency_name.clone(),
87 requirement: finding.requirement.clone(),
88 category: finding.category.as_str().to_string(),
89 severity: severity_str(finding.severity).to_string(),
90 range: RangeDocument {
91 start: PositionDocument {
92 line: finding.range.start.line,
93 character: finding.range.start.character,
94 },
95 end: PositionDocument {
96 line: finding.range.end.line,
97 character: finding.range.end.character,
98 },
99 },
100 message: finding.message.clone(),
101 })
102 .collect();
103
104 let summary = report
105 .summary()
106 .into_iter()
107 .map(|(category, count)| (category.as_str().to_string(), count))
108 .collect();
109
110 ReportDocument {
111 schema_version: SCHEMA_VERSION,
112 findings,
113 summary,
114 }
115}
116
117pub fn render(report: &CheckReport) -> Result<String, serde_json::Error> {
124 serde_json::to_string_pretty(&to_document(report))
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use crate::report::{Category, CheckFinding};
131 use deps_core::EcosystemId;
132 use deps_core::diagnostic::Severity;
133 use deps_core::position::{Position, Range};
134 use std::path::PathBuf;
135
136 fn finding() -> CheckFinding {
137 CheckFinding {
138 ecosystem: EcosystemId::Cargo,
139 manifest_path: PathBuf::from("Cargo.toml"),
140 dependency_name: Some("serde".to_string()),
141 requirement: Some("1.0".to_string()),
142 category: Category::Outdated,
143 code: None,
144 advisory_url: None,
145 advisory_severity: None,
146 severity: Severity::Hint,
147 range: Range::new(Position::new(4, 0), Position::new(4, 10)),
148 message: "Newer version available: 1.1.0".to_string(),
149 }
150 }
151
152 #[test]
153 fn test_to_document_empty_report() {
154 let document = to_document(&CheckReport::default());
155 assert_eq!(document.schema_version, SCHEMA_VERSION);
156 assert!(document.findings.is_empty());
157 assert!(document.summary.is_empty());
158 }
159
160 #[test]
161 fn test_to_document_maps_finding_fields() {
162 let document = to_document(&CheckReport {
163 findings: vec![finding()],
164 });
165 let f = &document.findings[0];
166 assert_eq!(f.ecosystem, "cargo");
167 assert_eq!(f.manifest_path, "Cargo.toml");
168 assert_eq!(f.dependency_name.as_deref(), Some("serde"));
169 assert_eq!(f.requirement.as_deref(), Some("1.0"));
170 assert_eq!(f.category, "outdated");
171 assert_eq!(f.severity, "hint");
172 assert_eq!(f.range.start.line, 4);
173 assert_eq!(document.summary.get("outdated"), Some(&1));
174 }
175
176 #[test]
177 fn test_render_round_trips_through_serde_json() {
178 let report = CheckReport {
179 findings: vec![finding()],
180 };
181 let rendered = render(&report).expect("render must succeed");
182 let parsed: ReportDocument = serde_json::from_str(&rendered).expect("must round-trip");
183 assert_eq!(parsed, to_document(&report));
184 }
185
186 #[test]
187 fn test_render_includes_schema_version_field() {
188 let rendered = render(&CheckReport::default()).expect("render must succeed");
189 assert!(rendered.contains("\"schema_version\": 1"));
190 }
191
192 #[test]
197 fn test_to_document_snapshot() {
198 let mut other = finding();
199 other.category = Category::Vulnerable;
200 other.severity = Severity::Error;
201 other.manifest_path = PathBuf::from("package.json");
202 other.dependency_name = None;
203 other.requirement = None;
204 other.message = "GHSA-xxxx-yyyy-zzzz: example advisory".to_string();
205
206 let document = to_document(&CheckReport {
207 findings: vec![finding(), other],
208 });
209 insta::assert_json_snapshot!(document);
210 }
211}