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();
#[expect(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, PartialEq)]
#[non_exhaustive]
pub enum ReportItem {
MissingRepositoryUrl,
InvalidRepoUrl {
repo: String,
},
InvalidGitRef {
repo: String,
rev: String,
},
MissingVcsInfo,
NoPathInVcsInfo,
NotFoundInRepo {
repo: String,
name: String,
},
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 {
#[expect(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::Warning,
Self::NotFoundInRepo { .. } => 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::NotFoundInRepo { .. } => "NotFoundInRepository",
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();
#[expect(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::NotFoundInRepo { repo, name } => {
data.insert(String::from("url"), Value::from(repo.clone()));
data.insert(String::from("name"), Value::from(name.clone()));
},
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)
}
#[must_use]
pub fn message(&self) -> Cow<'static, str> {
match self {
Self::MissingRepositoryUrl => Into::into("missing repository URL in crate metadata"),
Self::InvalidRepoUrl { repo } => format!("invalid repository URL: '{repo}'").into(),
Self::InvalidGitRef { repo, rev } => format!("invalid git ref '{rev}' for repository at '{repo}'").into(),
Self::MissingVcsInfo => Into::into("missing '.cargo_vcs_info.json' in published crate"),
Self::NoPathInVcsInfo => Into::into("no path specified in '.cargo_vcs_info.json'"),
Self::NotFoundInRepo { repo, name } => {
format!("crate '{name}' cannot be found in repository at '{repo}'").into()
},
Self::DirtyRepository => Into::into("crate was published from a \"dirty\" repository"),
Self::MetadataMismatch { field, krate, urepo } => {
let kmd = krate.as_ref().map_or("(none)", String::as_str);
let umd = urepo.as_ref().map_or("(none)", String::as_str);
format!("metadata mismatch: '{field}' differs between crate ({kmd}) and repository ({umd})").into()
},
Self::BrokenSymlinkInCrate { path } => format!("broken symbolic link in crate at path '{path}'").into(),
Self::BrokenSymlinkInRepo { path } => format!("broken symbolic link in repository at path '{path}'").into(),
Self::InvalidSymlinkInCrate { path } => format!("invalid symbolic link in crate at path '{path}'").into(),
Self::InvalidSymlinkInRepo { path } => {
format!("invalid symbolic link in repository at path '{path}'").into()
},
Self::MissingFile { path } => {
format!("file present in crate missing from repository at path '{path}'").into()
},
Self::ContentMismatch { path, .. } => {
format!("contents of file at path '{path}' differ between crate and repository").into()
},
Self::LineEndings { path } => {
format!("contents of file at path '{path}' use different line endings (CRLF / LF)").into()
},
Self::Permissions { path, krate, urepo } => {
format!("file at path '{path}' has different modes in crate ({krate}) and repository ({urepo})").into()
},
}
}
#[must_use]
pub fn extra(&self) -> Option<String> {
if let Self::ContentMismatch { diff, .. } = self
&& let Some(diff) = diff
{
Some(diff.clone())
} else {
None
}
}
}
impl Display for ReportItem {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if f.alternate() {
writeln!(f, "{}: {}", self.severity(), self.message())?;
if let Some(extra) = self.extra() {
for line in extra.lines() {
writeln!(f, " {line}")?;
}
writeln!(f)?;
}
Ok(())
} else {
writeln!(f, "{}: {}", self.severity(), self.message())
}
}
}