use std::fmt::{self, Display, Formatter};
use serde::Serialize;
use serde_json::Value;
use crate::severity::Severity;
#[derive(Debug, Default)]
pub struct Diff {
items: Vec<DiffItem>,
}
impl Diff {
pub(crate) const fn from_items(items: Vec<DiffItem>) -> Self {
Diff { items }
}
#[must_use]
pub fn items(&self) -> &[DiffItem] {
&self.items
}
#[must_use]
pub fn to_json(&self) -> String {
let items: Vec<JsonDiffItem> = self
.items
.iter()
.map(|i| JsonDiffItem {
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 Diff {
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 DiffItem {
FileAdded {
path: String,
},
FileRemoved {
path: String,
},
FileChanged {
path: String,
diff: Option<String>,
},
LineEndingsChange {
path: String,
},
PermissionChange {
path: String,
old: String,
new: String,
},
NameChange {
old: String,
new: String,
},
VersionChange {
old: String,
new: String,
},
EditionChange {
old: String,
new: String,
},
RustVersionChange {
old: Option<String>,
new: Option<String>,
},
AuthorsChange {
added: Vec<String>,
removed: Vec<String>,
},
DescriptionChange {
old: Option<String>,
new: Option<String>,
},
LicenseChange {
old: Option<String>,
new: Option<String>,
},
LicenseFileChange {
old: Option<String>,
new: Option<String>,
},
ReadmeChange {
old: Option<String>,
new: Option<String>,
},
CategoriesChange {
added: Vec<String>,
removed: Vec<String>,
},
KeywordsChange {
added: Vec<String>,
removed: Vec<String>,
},
RepositoryChange {
old: Option<String>,
new: Option<String>,
},
HomepageChange {
old: Option<String>,
new: Option<String>,
},
DocumentationChange {
old: Option<String>,
new: Option<String>,
},
LinksChange {
old: Option<String>,
new: Option<String>,
},
DefaultRunChange {
old: Option<String>,
new: Option<String>,
},
MetadataChange {
old: serde_json::Value,
new: serde_json::Value,
},
DependencyAdded {
path: String,
value: String,
},
DependencyRemoved {
path: String,
value: String,
},
DependencyUpgraded {
path: String,
old: String,
new: String,
},
DependencyDowngraded {
path: String,
old: String,
new: String,
},
DependencyFeatures {
path: String,
added: Vec<String>,
removed: Vec<String>,
},
DependencyOptionality {
path: String,
old: bool,
new: bool,
},
TargetAdded {
path: String,
target: String,
},
TargetRemoved {
path: String,
target: String,
},
FeatureAdded {
name: String,
},
FeatureRemoved {
name: String,
},
FeatureChanged {
name: String,
added: Vec<String>,
removed: Vec<String>,
},
}
#[derive(Serialize)]
struct JsonDiffItem {
severity: String,
kind: &'static str,
data: Value,
}
impl DiffItem {
#[must_use]
pub const fn severity(&self) -> Severity {
#[allow(clippy::match_same_arms)]
match self {
Self::FileAdded { .. } => Severity::Info,
Self::FileRemoved { .. } => Severity::Info,
Self::FileChanged { .. } => Severity::Info,
Self::LineEndingsChange { .. } => Severity::Warning,
Self::PermissionChange { .. } => Severity::Warning,
Self::NameChange { .. } => Severity::Error,
Self::VersionChange { .. } => Severity::Info,
Self::EditionChange { .. } => Severity::Warning,
Self::RustVersionChange { .. } => Severity::Warning,
Self::AuthorsChange { .. } => Severity::Warning,
Self::DescriptionChange { .. } => Severity::Info,
Self::LicenseChange { .. } => Severity::Warning,
Self::LicenseFileChange { .. } => Severity::Warning,
Self::ReadmeChange { .. } => Severity::Info,
Self::CategoriesChange { .. } => Severity::Info,
Self::KeywordsChange { .. } => Severity::Info,
Self::RepositoryChange { .. } => Severity::Warning,
Self::HomepageChange { .. } => Severity::Info,
Self::DocumentationChange { .. } => Severity::Info,
Self::LinksChange { .. } => Severity::Warning,
Self::DefaultRunChange { .. } => Severity::Info,
Self::MetadataChange { .. } => Severity::Info,
Self::DependencyAdded { .. } => Severity::Info,
Self::DependencyRemoved { .. } => Severity::Info,
Self::DependencyUpgraded { .. } => Severity::Info,
Self::DependencyDowngraded { .. } => Severity::Warning,
Self::DependencyFeatures { .. } => Severity::Info,
Self::DependencyOptionality { .. } => Severity::Info,
Self::TargetAdded { .. } => Severity::Info,
Self::TargetRemoved { .. } => Severity::Info,
Self::FeatureAdded { .. } => Severity::Info,
Self::FeatureRemoved { .. } => Severity::Warning,
Self::FeatureChanged { .. } => Severity::Info,
}
}
#[must_use]
pub const fn kind(&self) -> &'static str {
match self {
Self::FileAdded { .. } => "FileAdded",
Self::FileRemoved { .. } => "FileRemoved",
Self::FileChanged { .. } => "ContentChange",
Self::LineEndingsChange { .. } => "LineEndingsChange",
Self::PermissionChange { .. } => "PermissionChange",
Self::NameChange { .. } => "NameChange",
Self::VersionChange { .. } => "VersionChange",
Self::EditionChange { .. } => "EditionChange",
Self::RustVersionChange { .. } => "RustVersionChange",
Self::AuthorsChange { .. } => "AuthorsChange",
Self::DescriptionChange { .. } => "DescriptionChange",
Self::LicenseChange { .. } => "LicenseChange",
Self::LicenseFileChange { .. } => "LicenseFileChange",
Self::ReadmeChange { .. } => "ReadmeChange",
Self::CategoriesChange { .. } => "CategoriesChange",
Self::KeywordsChange { .. } => "KeywordsChange",
Self::RepositoryChange { .. } => "RepositoryChange",
Self::HomepageChange { .. } => "HomepageChange",
Self::DocumentationChange { .. } => "DocumentationChange",
Self::LinksChange { .. } => "LinksChange",
Self::DefaultRunChange { .. } => "DefaultRunChange",
Self::MetadataChange { .. } => "MetadataChange",
Self::DependencyAdded { .. } => "DependencyAdded",
Self::DependencyRemoved { .. } => "DependencyRemoved",
Self::DependencyUpgraded { .. } => "DependencyUpgraded",
Self::DependencyDowngraded { .. } => "DependencyDowngraded",
Self::DependencyFeatures { .. } => "DependencyFeatures",
Self::DependencyOptionality { .. } => "DependencyOptionality",
Self::TargetAdded { .. } => "TargetAdded",
Self::TargetRemoved { .. } => "TargetRemoved",
Self::FeatureAdded { .. } => "FeatureAdded",
Self::FeatureRemoved { .. } => "FeatureRemoved",
Self::FeatureChanged { .. } => "FeatureChanged",
}
}
#[allow(clippy::too_many_lines)]
#[must_use]
pub fn data(&self) -> Value {
let mut data = serde_json::Map::new();
#[allow(clippy::match_same_arms)]
match self {
Self::FileAdded { path } => {
data.insert(String::from("path"), Value::from(path.clone()));
},
Self::FileRemoved { path } => {
data.insert(String::from("path"), Value::from(path.clone()));
},
Self::FileChanged { path, diff } => {
data.insert(String::from("path"), Value::from(path.clone()));
data.insert(String::from("diff"), Value::from(diff.clone()));
},
Self::LineEndingsChange { path } => {
data.insert(String::from("path"), Value::from(Some(path.clone())));
},
Self::PermissionChange { path, old, new } => {
data.insert(String::from("path"), Value::from(path.clone()));
data.insert(String::from("old"), Value::from(old.clone()));
data.insert(String::from("new"), Value::from(new.clone()));
},
Self::NameChange { old, new } => {
data.insert(String::from("old"), Value::from(old.clone()));
data.insert(String::from("new"), Value::from(new.clone()));
},
Self::VersionChange { old, new } => {
data.insert(String::from("old"), Value::from(old.clone()));
data.insert(String::from("new"), Value::from(new.clone()));
},
Self::EditionChange { old, new } => {
data.insert(String::from("old"), Value::from(old.clone()));
data.insert(String::from("new"), Value::from(new.clone()));
},
Self::RustVersionChange { old, new } => {
data.insert(String::from("old"), Value::from(old.clone()));
data.insert(String::from("new"), Value::from(new.clone()));
},
Self::AuthorsChange { added, removed } => {
data.insert(String::from("added"), Value::from(added.clone()));
data.insert(String::from("removed"), Value::from(removed.clone()));
},
Self::DescriptionChange { old, new } => {
data.insert(String::from("old"), Value::from(old.clone()));
data.insert(String::from("new"), Value::from(new.clone()));
},
Self::LicenseChange { old, new } => {
data.insert(String::from("old"), Value::from(old.clone()));
data.insert(String::from("new"), Value::from(new.clone()));
},
Self::LicenseFileChange { old, new } => {
data.insert(String::from("old"), Value::from(old.clone()));
data.insert(String::from("new"), Value::from(new.clone()));
},
Self::ReadmeChange { old, new } => {
data.insert(String::from("old"), Value::from(old.clone()));
data.insert(String::from("new"), Value::from(new.clone()));
},
Self::CategoriesChange { added, removed } => {
data.insert(String::from("added"), Value::from(added.clone()));
data.insert(String::from("removed"), Value::from(removed.clone()));
},
Self::KeywordsChange { added, removed } => {
data.insert(String::from("added"), Value::from(added.clone()));
data.insert(String::from("removed"), Value::from(removed.clone()));
},
Self::RepositoryChange { old, new } => {
data.insert(String::from("old"), Value::from(old.clone()));
data.insert(String::from("new"), Value::from(new.clone()));
},
Self::HomepageChange { old, new } => {
data.insert(String::from("old"), Value::from(old.clone()));
data.insert(String::from("new"), Value::from(new.clone()));
},
Self::DocumentationChange { old, new } => {
data.insert(String::from("old"), Value::from(old.clone()));
data.insert(String::from("new"), Value::from(new.clone()));
},
Self::LinksChange { old, new } => {
data.insert(String::from("old"), Value::from(old.clone()));
data.insert(String::from("new"), Value::from(new.clone()));
},
Self::DefaultRunChange { old, new } => {
data.insert(String::from("old"), Value::from(old.clone()));
data.insert(String::from("new"), Value::from(new.clone()));
},
Self::MetadataChange { old, new } => {
data.insert(String::from("old"), old.clone());
data.insert(String::from("new"), new.clone());
},
Self::DependencyAdded { path, value } => {
data.insert(String::from("path"), Value::from(path.clone()));
data.insert(String::from("value"), Value::from(value.clone()));
},
Self::DependencyRemoved { path, value } => {
data.insert(String::from("path"), Value::from(path.clone()));
data.insert(String::from("value"), Value::from(value.clone()));
},
Self::DependencyUpgraded { path, old, new } => {
data.insert(String::from("path"), Value::from(path.clone()));
data.insert(String::from("old"), Value::from(old.clone()));
data.insert(String::from("new"), Value::from(new.clone()));
},
Self::DependencyDowngraded { path, old, new } => {
data.insert(String::from("path"), Value::from(path.clone()));
data.insert(String::from("old"), Value::from(old.clone()));
data.insert(String::from("new"), Value::from(new.clone()));
},
Self::DependencyFeatures { path, removed, added } => {
data.insert(String::from("path"), Value::from(path.clone()));
data.insert(String::from("removed"), Value::from(removed.clone()));
data.insert(String::from("added"), Value::from(added.clone()));
},
Self::DependencyOptionality { path, old, new } => {
data.insert(String::from("path"), Value::from(path.clone()));
data.insert(String::from("old"), Value::from(old.to_string()));
data.insert(String::from("new"), Value::from(new.to_string()));
},
Self::TargetAdded { path, target } => {
data.insert(String::from("path"), Value::from(path.clone()));
data.insert(String::from("target"), Value::from(target.clone()));
},
Self::TargetRemoved { path, target } => {
data.insert(String::from("path"), Value::from(path.clone()));
data.insert(String::from("target"), Value::from(target.clone()));
},
Self::FeatureAdded { name } => {
data.insert(String::from("name"), Value::from(name.clone()));
},
Self::FeatureRemoved { name } => {
data.insert(String::from("name"), Value::from(name.clone()));
},
Self::FeatureChanged { name, removed, added } => {
data.insert(String::from("name"), Value::from(name.clone()));
data.insert(String::from("added"), Value::from(added.clone()));
data.insert(String::from("removed"), Value::from(removed.clone()));
},
}
Value::Object(data)
}
}
#[allow(clippy::too_many_lines)]
impl Display for DiffItem {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let sev = self.severity();
match self {
DiffItem::FileAdded { path } => {
writeln!(f, "{sev}: file added at path '{path}'")?;
},
DiffItem::FileRemoved { path } => {
writeln!(f, "{sev}: file removed at path '{path}'")?;
},
DiffItem::FileChanged { path, diff } => {
writeln!(f, "{sev}: file at path '{path}' changed")?;
if let Some(diff) = diff
&& f.alternate()
{
for line in diff.lines() {
writeln!(f, " {line}")?;
}
}
},
DiffItem::LineEndingsChange { path } => {
writeln!(f, "{sev}: file at path '{path}' changed line endings (CRLF / LF)")?;
},
DiffItem::PermissionChange { path, old, new } => {
writeln!(f, "{sev}: file at path '{path}' changed mode from {old} to {new}")?;
},
DiffItem::NameChange { old, new } => {
writeln!(f, "{sev}: crate name changed from '{old}' to '{new}'")?;
},
DiffItem::VersionChange { old, new } => {
writeln!(f, "{sev}: crate version changed from '{old}' to '{new}'")?;
},
DiffItem::EditionChange { old, new } => {
writeln!(f, "{sev}: crate edition changed from '{old}' to '{new}'")?;
},
DiffItem::RustVersionChange { old, new } => {
writeln!(
f,
"{sev}: crate MSRV changed from '{}' to '{}'",
old.as_ref().map_or("(none)", String::as_str),
new.as_ref().map_or("(none)", String::as_str)
)?;
},
DiffItem::AuthorsChange { added, removed } => {
writeln!(f, "{sev}: crate authors changed")?;
if !added.is_empty() {
writeln!(f, " added: {}", added.join(", "))?;
}
if !removed.is_empty() {
writeln!(f, " removed: {}", removed.join(", "))?;
}
},
DiffItem::DescriptionChange { old, new } => {
writeln!(f, "{sev}: crate description changed")?;
writeln!(f, " from: {}", old.as_ref().map_or("(none)", String::as_str))?;
writeln!(f, " to: {}", new.as_ref().map_or("(none)", String::as_str))?;
},
DiffItem::LicenseChange { old, new } => {
writeln!(
f,
"{sev}: crate license changed from '{}' to '{}'",
old.as_ref().map_or("(none)", String::as_str),
new.as_ref().map_or("(none)", String::as_str)
)?;
},
DiffItem::LicenseFileChange { old, new } => {
writeln!(
f,
"{sev}: crate license file changed from '{}' to '{}'",
old.as_ref().map_or("(none)", String::as_str),
new.as_ref().map_or("(none)", String::as_str)
)?;
},
DiffItem::ReadmeChange { old, new } => {
writeln!(
f,
"{sev}: crate readme file changed from '{}' to '{}'",
old.as_ref().map_or("(none)", String::as_str),
new.as_ref().map_or("(none)", String::as_str)
)?;
},
DiffItem::CategoriesChange { added, removed } => {
writeln!(f, "{sev}: crate categories changed")?;
if !added.is_empty() {
writeln!(f, " added: {}", added.join(", "))?;
}
if !removed.is_empty() {
writeln!(f, " removed: {}", removed.join(", "))?;
}
},
DiffItem::KeywordsChange { added, removed } => {
writeln!(f, "{sev}: crate keywords changed")?;
if !added.is_empty() {
writeln!(f, " added: {}", added.join(", "))?;
}
if !removed.is_empty() {
writeln!(f, " removed: {}", removed.join(", "))?;
}
},
DiffItem::RepositoryChange { old, new } => {
writeln!(
f,
"{sev}: crate repository URL file changed from '{}' to '{}'",
old.as_ref().map_or("(none)", String::as_str),
new.as_ref().map_or("(none)", String::as_str)
)?;
},
DiffItem::HomepageChange { old, new } => {
writeln!(
f,
"{sev}: crate homepage URL file changed from '{}' to '{}'",
old.as_ref().map_or("(none)", String::as_str),
new.as_ref().map_or("(none)", String::as_str)
)?;
},
DiffItem::DocumentationChange { old, new } => {
writeln!(
f,
"{sev}: crate documentation URL file changed from '{}' to '{}'",
old.as_ref().map_or("(none)", String::as_str),
new.as_ref().map_or("(none)", String::as_str)
)?;
},
DiffItem::LinksChange { old, new } => {
writeln!(
f,
"{sev}: linked library changed from '{}' to '{}'",
old.as_ref().map_or("(none)", String::as_str),
new.as_ref().map_or("(none)", String::as_str)
)?;
},
DiffItem::DefaultRunChange { old, new } => {
writeln!(
f,
"{sev}: default 'run' target changed from '{}' to '{}'",
old.as_ref().map_or("(none)", String::as_str),
new.as_ref().map_or("(none)", String::as_str)
)?;
},
DiffItem::MetadataChange { old, new } => {
writeln!(f, "{sev}: crate metadata changed:")?;
writeln!(f, " old: {old}")?;
writeln!(f, " new: {new}")?;
},
DiffItem::DependencyAdded { path, value } => {
writeln!(f, "{sev}: crate dependency '{path}' added:")?;
writeln!(f, " {value}")?;
},
DiffItem::DependencyRemoved { path, value } => {
writeln!(f, "{sev}: crate dependency '{path}' removed:")?;
writeln!(f, " {value}")?;
},
DiffItem::DependencyUpgraded { path, old, new } => {
writeln!(f, "{sev}: crate dependency '{path}' upgraded from {old} to {new}")?;
},
DiffItem::DependencyDowngraded { path, old, new } => {
writeln!(f, "{sev}: crate dependency '{path}' downgraded from {old} to {new}")?;
},
DiffItem::DependencyFeatures { path, added, removed } => {
writeln!(f, "{sev}: crate dependency '{path}' features changed:")?;
if !added.is_empty() {
writeln!(f, " added: {}", added.join(", "))?;
}
if !removed.is_empty() {
writeln!(f, " removed: {}", removed.join(", "))?;
}
},
DiffItem::DependencyOptionality { path, old, new } => {
if !*old && *new {
writeln!(f, "{sev}: crate dependency '{path}' is now optional")?;
}
if *old && !*new {
writeln!(f, "{sev}: crate dependency '{path}' is no longer optional")?;
}
},
DiffItem::TargetAdded { path, target } => {
writeln!(f, "{sev}: crate compilation target '{path}' added: {target}")?;
},
DiffItem::TargetRemoved { path, target } => {
writeln!(f, "{sev}: crate compilation target '{path}' removed: {target}")?;
},
DiffItem::FeatureAdded { name } => {
writeln!(f, "{sev}: crate feature added: '{name}'")?;
},
DiffItem::FeatureRemoved { name } => {
writeln!(f, "{sev}: crate feature removed: '{name}'")?;
},
DiffItem::FeatureChanged { name, added, removed } => {
writeln!(f, "{sev}: crate feature '{name}' dependencies changed:")?;
if !added.is_empty() {
writeln!(f, " added: {}", added.join(", "))?;
}
if !removed.is_empty() {
writeln!(f, " removed: {}", removed.join(", "))?;
}
},
}
Ok(())
}
}