use std::borrow::Cow;
use std::fmt::{self, Display, Formatter};
use serde::Serialize;
use serde_json::Value;
use crate::severity::Severity;
#[derive(Debug, Default)]
pub struct Report {
items: Vec<ReportItem>,
}
impl Report {
pub(crate) const fn from_items(items: Vec<ReportItem>) -> Self {
Report { items }
}
#[must_use]
pub fn items(&self) -> &[ReportItem] {
&self.items
}
#[must_use]
pub fn to_json(&self) -> String {
let items: Vec<JsonReportItem> = self
.items
.iter()
.map(|i| JsonReportItem {
severity: i.severity().to_string(),
kind: i.kind(),
data: i.data(),
})
.collect();
#[allow(clippy::expect_used)]
serde_json::to_string_pretty(&items).expect("Failed to serialize report as JSON.")
}
}
impl Display for Report {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for item in self.items() {
if f.alternate() {
write!(f, "{item:#}")?;
} else {
write!(f, "{item}")?;
}
}
Ok(())
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum ReportItem {
MissingRepositoryUrl,
InvalidRepoUrl {
repo: String,
},
InvalidGitRef {
repo: String,
rev: String,
},
MissingVcsInfo,
NoPathInVcsInfo,
DirtyRepository,
MetadataMismatch {
field: Cow<'static, str>,
krate: Option<String>,
urepo: Option<String>,
},
BrokenSymlinkInCrate {
path: String,
},
BrokenSymlinkInRepo {
path: String,
},
InvalidSymlinkInCrate {
path: String,
},
InvalidSymlinkInRepo {
path: String,
},
MissingFile {
path: String,
},
ContentMismatch {
path: String,
diff: Option<String>,
},
LineEndings {
path: String,
},
Permissions {
path: String,
krate: String,
urepo: String,
},
}
impl ReportItem {
pub(crate) fn metadata_mismatch<F: Into<Cow<'static, str>>>(
field: F,
krate: Option<String>,
urepo: Option<String>,
) -> Self {
ReportItem::MetadataMismatch {
field: field.into(),
krate,
urepo,
}
}
}
#[derive(Serialize)]
struct JsonReportItem {
severity: String,
kind: &'static str,
data: Value,
}
impl ReportItem {
#[must_use]
pub const fn severity(&self) -> Severity {
#[allow(clippy::match_same_arms)]
match self {
Self::MissingRepositoryUrl => Severity::Fatal,
Self::InvalidRepoUrl { .. } => Severity::Fatal,
Self::InvalidGitRef { .. } => Severity::Fatal,
Self::MissingVcsInfo => Severity::Fatal,
Self::NoPathInVcsInfo => Severity::Fatal,
Self::DirtyRepository => Severity::Warning,
Self::MetadataMismatch { .. } => Severity::Error,
Self::BrokenSymlinkInCrate { .. } => Severity::Warning,
Self::BrokenSymlinkInRepo { .. } => Severity::Warning,
Self::InvalidSymlinkInCrate { .. } => Severity::Error,
Self::InvalidSymlinkInRepo { .. } => Severity::Error,
Self::MissingFile { .. } => Severity::Error,
Self::ContentMismatch { .. } => Severity::Error,
Self::LineEndings { .. } => Severity::Warning,
Self::Permissions { .. } => Severity::Warning,
}
}
#[must_use]
pub const fn kind(&self) -> &'static str {
match self {
Self::MissingRepositoryUrl => "MissingRepositoryUrl",
Self::InvalidRepoUrl { .. } => "InvalidRepoUrl",
Self::InvalidGitRef { .. } => "InvalidGitRef",
Self::MissingVcsInfo => "MissingVcsInfo",
Self::NoPathInVcsInfo => "NoPathInVcsInfo",
Self::DirtyRepository => "DirtyRepository",
Self::MetadataMismatch { .. } => "MetadataMismatch",
Self::BrokenSymlinkInCrate { .. } => "BrokenSymlinkInCrate",
Self::BrokenSymlinkInRepo { .. } => "BrokenSymlinkInRepo",
Self::InvalidSymlinkInCrate { .. } => "InvalidSymlinkInCrate",
Self::InvalidSymlinkInRepo { .. } => "InvalidSymlinkInRepo",
Self::MissingFile { .. } => "MissingFile",
Self::ContentMismatch { .. } => "ContentMismatch",
Self::LineEndings { .. } => "LineEndings",
Self::Permissions { .. } => "Permissions",
}
}
#[must_use]
pub fn data(&self) -> Value {
let mut data = serde_json::Map::new();
#[allow(clippy::match_same_arms)]
match self {
Self::MissingRepositoryUrl => {},
Self::InvalidRepoUrl { repo } => {
data.insert(String::from("url"), Value::from(repo.clone()));
},
Self::InvalidGitRef { repo, rev } => {
data.insert(String::from("url"), Value::from(repo.clone()));
data.insert(String::from("ref"), Value::from(rev.clone()));
},
Self::MissingVcsInfo => {},
Self::NoPathInVcsInfo => {},
Self::DirtyRepository => {},
Self::MetadataMismatch { field, krate, urepo } => {
data.insert(String::from("field"), Value::from(String::from(field.as_ref())));
data.insert(String::from("crate"), Value::from(krate.clone()));
data.insert(String::from("urepo"), Value::from(urepo.clone()));
},
Self::BrokenSymlinkInCrate { path } => {
data.insert(String::from("path"), Value::from(path.clone()));
},
Self::BrokenSymlinkInRepo { path } => {
data.insert(String::from("path"), Value::from(path.clone()));
},
Self::InvalidSymlinkInCrate { path } => {
data.insert(String::from("path"), Value::from(path.clone()));
},
Self::InvalidSymlinkInRepo { path } => {
data.insert(String::from("path"), Value::from(path.clone()));
},
Self::MissingFile { path } => {
data.insert(String::from("path"), Value::from(path.clone()));
},
Self::ContentMismatch { path, diff } => {
data.insert(String::from("path"), Value::from(path.clone()));
data.insert(String::from("diff"), Value::from(diff.clone()));
},
Self::LineEndings { path } => {
data.insert(String::from("path"), Value::from(path.clone()));
},
Self::Permissions { path, krate, urepo } => {
data.insert(String::from("path"), Value::from(path.clone()));
data.insert(String::from("mode-in-crate"), Value::from(krate.clone()));
data.insert(String::from("mode-in-repo"), Value::from(urepo.clone()));
},
}
Value::Object(data)
}
}
impl Display for ReportItem {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let sev = self.severity();
match self {
ReportItem::MissingRepositoryUrl => {
writeln!(f, "{sev}: missing repository URL in crate metadata")?;
},
ReportItem::InvalidRepoUrl { repo } => {
writeln!(f, "{sev}: invalid repository URL: '{repo}'")?;
},
ReportItem::InvalidGitRef { repo, rev } => {
writeln!(f, "{sev}: invalid git ref '{rev}' for repository at '{repo}'")?;
},
ReportItem::MissingVcsInfo => {
writeln!(f, "{sev}: missing '.cargo_vcs_info.json' in published crate")?;
},
ReportItem::NoPathInVcsInfo => {
writeln!(f, "{sev}: no path specified in '.cargo_vcs_info.json'")?;
},
ReportItem::DirtyRepository => {
writeln!(f, "{sev}: crate was published from a \"dirty\" repository")?;
},
ReportItem::MetadataMismatch { field, krate, urepo } => {
writeln!(f, "{sev}: metadata mismatch between crate and repository at '{field}':")?;
writeln!(f, " crate: {}", krate.as_ref().map_or("(none)", String::as_str))?;
writeln!(f, " repo: {}", urepo.as_ref().map_or("(none)", String::as_str))?;
},
ReportItem::BrokenSymlinkInCrate { path } => {
writeln!(f, "{sev}: broken symbolic link in crate at path '{path}'")?;
},
ReportItem::BrokenSymlinkInRepo { path } => {
writeln!(f, "{sev}: broken symbolic link in repository at path '{path}'")?;
},
ReportItem::InvalidSymlinkInCrate { path } => {
writeln!(f, "{sev}: invalid symbolic link in crate at path '{path}'")?;
},
ReportItem::InvalidSymlinkInRepo { path } => {
writeln!(f, "{sev}: invalid symbolic link in repository at path '{path}'")?;
},
ReportItem::MissingFile { path } => {
writeln!(
f,
"{sev}: file present in crate missing from repository at path '{path}'"
)?;
},
ReportItem::ContentMismatch { path, diff } => {
writeln!(
f,
"{sev}: contents of file at path '{path}' differ between crate and repository"
)?;
if let Some(diff) = diff
&& f.alternate()
{
for line in diff.lines() {
writeln!(f, " {line}")?;
}
}
},
ReportItem::LineEndings { path } => {
writeln!(
f,
"{sev}: contents of file at path '{path}' use different line endings (CRLF / LF)"
)?;
},
ReportItem::Permissions { path, krate, urepo } => {
writeln!(
f,
"{sev}: file at path '{path}' has different modes in crate ({krate}) and repository ({urepo})"
)?;
},
}
Ok(())
}
}