use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DiffResult {
Unchanged,
Added,
Modified,
Removed,
Moved {
from: DiffPath,
to: DiffPath,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChangeSet {
pub changes: Vec<SemanticChange>,
pub summary: ChangeSummary,
pub metadata: IndexMap<String, String>,
pub timestamp: chrono::DateTime<chrono::Utc>,
}
impl ChangeSet {
pub fn new() -> Self {
Self {
changes: Vec::new(),
summary: ChangeSummary::default(),
metadata: IndexMap::new(),
timestamp: chrono::Utc::now(),
}
}
pub fn add_change(&mut self, change: SemanticChange) {
match change.change_type {
ChangeType::ElementAdded | ChangeType::AttributeAdded => {
self.summary.additions += 1;
}
ChangeType::ElementRemoved | ChangeType::AttributeRemoved => {
self.summary.deletions += 1;
}
ChangeType::ElementModified
| ChangeType::AttributeModified
| ChangeType::TextModified
| ChangeType::ElementRenamed => {
self.summary.modifications += 1;
}
ChangeType::ElementMoved => {
self.summary.moves += 1;
}
}
if change.is_critical {
self.summary.critical_changes += 1;
}
self.changes.push(change);
self.summary.total_changes = self.changes.len();
}
pub fn has_changes(&self) -> bool {
!self.changes.is_empty()
}
pub fn critical_changes(&self) -> Vec<&SemanticChange> {
self.changes.iter().filter(|c| c.is_critical).collect()
}
pub fn changes_by_type(&self, change_type: ChangeType) -> Vec<&SemanticChange> {
self.changes
.iter()
.filter(|c| c.change_type == change_type)
.collect()
}
pub fn impact_level(&self) -> ImpactLevel {
if self.summary.critical_changes > 0 {
ImpactLevel::High
} else if self.summary.total_changes > 10 {
ImpactLevel::Medium
} else if self.summary.total_changes > 0 {
ImpactLevel::Low
} else {
ImpactLevel::None
}
}
}
impl Default for ChangeSet {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SemanticChange {
pub path: DiffPath,
pub change_type: ChangeType,
pub old_value: Option<String>,
pub new_value: Option<String>,
pub is_critical: bool,
pub description: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct DiffPath {
pub segments: Vec<PathSegment>,
}
impl DiffPath {
pub fn root() -> Self {
Self {
segments: Vec::new(),
}
}
pub fn element(name: &str) -> Self {
Self {
segments: vec![PathSegment::Element(name.to_string())],
}
}
pub fn with_element(&self, name: &str) -> Self {
let mut segments = self.segments.clone();
segments.push(PathSegment::Element(name.to_string()));
Self { segments }
}
pub fn with_attribute(&self, name: &str) -> Self {
let mut segments = self.segments.clone();
segments.push(PathSegment::Attribute(name.to_string()));
Self { segments }
}
pub fn with_text(&self) -> Self {
let mut segments = self.segments.clone();
segments.push(PathSegment::Text);
Self { segments }
}
pub fn with_index(&self, index: usize) -> Self {
let mut segments = self.segments.clone();
segments.push(PathSegment::Index(index));
Self { segments }
}
pub fn to_string(&self) -> String {
if self.segments.is_empty() {
return "/".to_string();
}
let mut path = String::new();
for segment in &self.segments {
path.push('/');
match segment {
PathSegment::Element(name) => path.push_str(name),
PathSegment::Attribute(name) => {
path.push('@');
path.push_str(name);
}
PathSegment::Text => path.push_str("#text"),
PathSegment::Index(idx) => path.push_str(&format!("[{}]", idx)),
}
}
path
}
}
impl fmt::Display for DiffPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_string())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum PathSegment {
Element(String),
Attribute(String),
Text,
Index(usize),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ChangeType {
ElementAdded,
ElementRemoved,
ElementModified,
ElementRenamed,
ElementMoved,
AttributeAdded,
AttributeRemoved,
AttributeModified,
TextModified,
}
impl fmt::Display for ChangeType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
ChangeType::ElementAdded => "Element Added",
ChangeType::ElementRemoved => "Element Removed",
ChangeType::ElementModified => "Element Modified",
ChangeType::ElementRenamed => "Element Renamed",
ChangeType::ElementMoved => "Element Moved",
ChangeType::AttributeAdded => "Attribute Added",
ChangeType::AttributeRemoved => "Attribute Removed",
ChangeType::AttributeModified => "Attribute Modified",
ChangeType::TextModified => "Text Modified",
};
write!(f, "{}", s)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ChangeSummary {
pub total_changes: usize,
pub additions: usize,
pub deletions: usize,
pub modifications: usize,
pub moves: usize,
pub critical_changes: usize,
}
impl ChangeSummary {
pub fn has_changes(&self) -> bool {
self.total_changes > 0
}
pub fn summary_string(&self) -> String {
if !self.has_changes() {
return "No changes".to_string();
}
let mut parts = Vec::new();
if self.additions > 0 {
parts.push(format!("{} added", self.additions));
}
if self.deletions > 0 {
parts.push(format!("{} deleted", self.deletions));
}
if self.modifications > 0 {
parts.push(format!("{} modified", self.modifications));
}
if self.moves > 0 {
parts.push(format!("{} moved", self.moves));
}
let summary = parts.join(", ");
if self.critical_changes > 0 {
format!("{} ({} critical)", summary, self.critical_changes)
} else {
summary
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ImpactLevel {
None,
Low,
Medium,
High,
}
impl fmt::Display for ImpactLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
ImpactLevel::None => "None",
ImpactLevel::Low => "Low",
ImpactLevel::Medium => "Medium",
ImpactLevel::High => "High",
};
write!(f, "{}", s)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChangeContext {
pub entity_type: Option<String>,
pub entity_id: Option<String>,
pub business_context: Option<String>,
pub technical_context: IndexMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChangeSignificance {
pub critical_fields: Vec<String>,
pub ignored_fields: Vec<String>,
pub numeric_tolerance: f64,
pub ignore_order: bool,
}
impl Default for ChangeSignificance {
fn default() -> Self {
Self {
critical_fields: vec![
"CommercialModelType".to_string(),
"TerritoryCode".to_string(),
"Price".to_string(),
"ValidityPeriod".to_string(),
"ReleaseDate".to_string(),
"UPC".to_string(),
"ISRC".to_string(),
],
ignored_fields: vec![
"MessageId".to_string(),
"MessageCreatedDateTime".to_string(),
],
numeric_tolerance: 0.01,
ignore_order: true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_diff_path() {
let path = DiffPath::root()
.with_element("Release")
.with_attribute("ReleaseId");
assert_eq!(path.to_string(), "/Release/@ReleaseId");
}
#[test]
fn test_changeset() {
let mut changeset = ChangeSet::new();
changeset.add_change(SemanticChange {
path: DiffPath::element("Test"),
change_type: ChangeType::ElementAdded,
old_value: None,
new_value: Some("new".to_string()),
is_critical: true,
description: "Test change".to_string(),
});
assert!(changeset.has_changes());
assert_eq!(changeset.summary.total_changes, 1);
assert_eq!(changeset.summary.critical_changes, 1);
assert_eq!(changeset.impact_level(), ImpactLevel::High);
}
#[test]
fn test_change_summary() {
let mut summary = ChangeSummary::default();
assert!(!summary.has_changes());
assert_eq!(summary.summary_string(), "No changes");
summary.additions = 2;
summary.modifications = 1;
summary.critical_changes = 1;
summary.total_changes = 3;
assert!(summary.has_changes());
let summary_str = summary.summary_string();
assert!(summary_str.contains("2 added"));
assert!(summary_str.contains("1 modified"));
assert!(summary_str.contains("1 critical"));
}
}