use super::{
ComponentChange, DependencyChange, LicenseChange, VexStatusChange, VulnerabilityDetail,
};
use crate::model::{CanonicalId, NormalizedSbom};
use std::collections::HashMap;
pub type ComponentMatches = HashMap<CanonicalId, Option<CanonicalId>>;
pub trait ChangeComputer: Send + Sync {
type ChangeSet;
fn compute(
&self,
old: &NormalizedSbom,
new: &NormalizedSbom,
matches: &ComponentMatches,
) -> Self::ChangeSet;
fn name(&self) -> &str;
}
#[derive(Debug, Clone, Default)]
pub struct ComponentChangeSet {
pub added: Vec<ComponentChange>,
pub removed: Vec<ComponentChange>,
pub modified: Vec<ComponentChange>,
}
impl ComponentChangeSet {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.added.is_empty() && self.removed.is_empty() && self.modified.is_empty()
}
#[must_use]
pub fn total(&self) -> usize {
self.added.len() + self.removed.len() + self.modified.len()
}
}
#[derive(Debug, Clone, Default)]
pub struct DependencyChangeSet {
pub added: Vec<DependencyChange>,
pub removed: Vec<DependencyChange>,
}
impl DependencyChangeSet {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.added.is_empty() && self.removed.is_empty()
}
#[must_use]
pub fn total(&self) -> usize {
self.added.len() + self.removed.len()
}
}
#[derive(Debug, Clone, Default)]
pub struct LicenseChangeSet {
pub new_licenses: Vec<LicenseChange>,
pub removed_licenses: Vec<LicenseChange>,
pub component_changes: Vec<(String, String, String)>, }
impl LicenseChangeSet {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.new_licenses.is_empty()
&& self.removed_licenses.is_empty()
&& self.component_changes.is_empty()
}
}
#[derive(Debug, Clone, Default)]
pub struct VulnerabilityChangeSet {
pub introduced: Vec<VulnerabilityDetail>,
pub resolved: Vec<VulnerabilityDetail>,
pub persistent: Vec<VulnerabilityDetail>,
pub vex_changes: Vec<VexStatusChange>,
}
impl VulnerabilityChangeSet {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.introduced.is_empty() && self.resolved.is_empty()
}
pub fn sort_by_severity(&mut self) {
let severity_order = |s: &str| match s {
"Critical" => 0,
"High" => 1,
"Medium" => 2,
"Low" => 3,
_ => 4,
};
self.introduced
.sort_by(|a, b| severity_order(&a.severity).cmp(&severity_order(&b.severity)));
self.resolved
.sort_by(|a, b| severity_order(&a.severity).cmp(&severity_order(&b.severity)));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_component_change_set_empty() {
let set = ComponentChangeSet::new();
assert!(set.is_empty());
assert_eq!(set.total(), 0);
}
#[test]
fn test_dependency_change_set_empty() {
let set = DependencyChangeSet::new();
assert!(set.is_empty());
assert_eq!(set.total(), 0);
}
#[test]
fn test_license_change_set_empty() {
let set = LicenseChangeSet::new();
assert!(set.is_empty());
}
#[test]
fn test_vulnerability_change_set_empty() {
let set = VulnerabilityChangeSet::new();
assert!(set.is_empty());
}
}