1use indexmap::IndexMap;
4use serde::{Deserialize, Serialize};
5use std::fmt;
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9pub enum DiffResult {
10    Unchanged,
12    Added,
14    Modified,
16    Removed,
18    Moved {
20        from: DiffPath,
22        to: DiffPath,
24    },
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct ChangeSet {
30    pub changes: Vec<SemanticChange>,
32
33    pub summary: ChangeSummary,
35
36    pub metadata: IndexMap<String, String>,
38
39    pub timestamp: chrono::DateTime<chrono::Utc>,
41}
42
43impl ChangeSet {
44    pub fn new() -> Self {
46        Self {
47            changes: Vec::new(),
48            summary: ChangeSummary::default(),
49            metadata: IndexMap::new(),
50            timestamp: chrono::Utc::now(),
51        }
52    }
53
54    pub fn add_change(&mut self, change: SemanticChange) {
56        match change.change_type {
58            ChangeType::ElementAdded | ChangeType::AttributeAdded => {
59                self.summary.additions += 1;
60            }
61            ChangeType::ElementRemoved | ChangeType::AttributeRemoved => {
62                self.summary.deletions += 1;
63            }
64            ChangeType::ElementModified
65            | ChangeType::AttributeModified
66            | ChangeType::TextModified
67            | ChangeType::ElementRenamed => {
68                self.summary.modifications += 1;
69            }
70            ChangeType::ElementMoved => {
71                self.summary.moves += 1;
72            }
73        }
74
75        if change.is_critical {
76            self.summary.critical_changes += 1;
77        }
78
79        self.changes.push(change);
80        self.summary.total_changes = self.changes.len();
81    }
82
83    pub fn has_changes(&self) -> bool {
85        !self.changes.is_empty()
86    }
87
88    pub fn critical_changes(&self) -> Vec<&SemanticChange> {
90        self.changes.iter().filter(|c| c.is_critical).collect()
91    }
92
93    pub fn changes_by_type(&self, change_type: ChangeType) -> Vec<&SemanticChange> {
95        self.changes
96            .iter()
97            .filter(|c| c.change_type == change_type)
98            .collect()
99    }
100
101    pub fn impact_level(&self) -> ImpactLevel {
103        if self.summary.critical_changes > 0 {
104            ImpactLevel::High
105        } else if self.summary.total_changes > 10 {
106            ImpactLevel::Medium
107        } else if self.summary.total_changes > 0 {
108            ImpactLevel::Low
109        } else {
110            ImpactLevel::None
111        }
112    }
113}
114
115impl Default for ChangeSet {
116    fn default() -> Self {
117        Self::new()
118    }
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct SemanticChange {
124    pub path: DiffPath,
126
127    pub change_type: ChangeType,
129
130    pub old_value: Option<String>,
132
133    pub new_value: Option<String>,
135
136    pub is_critical: bool,
138
139    pub description: String,
141}
142
143#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
145pub struct DiffPath {
146    pub segments: Vec<PathSegment>,
148}
149
150impl DiffPath {
151    pub fn root() -> Self {
153        Self {
154            segments: Vec::new(),
155        }
156    }
157
158    pub fn element(name: &str) -> Self {
160        Self {
161            segments: vec![PathSegment::Element(name.to_string())],
162        }
163    }
164
165    pub fn with_element(&self, name: &str) -> Self {
167        let mut segments = self.segments.clone();
168        segments.push(PathSegment::Element(name.to_string()));
169        Self { segments }
170    }
171
172    pub fn with_attribute(&self, name: &str) -> Self {
174        let mut segments = self.segments.clone();
175        segments.push(PathSegment::Attribute(name.to_string()));
176        Self { segments }
177    }
178
179    pub fn with_text(&self) -> Self {
181        let mut segments = self.segments.clone();
182        segments.push(PathSegment::Text);
183        Self { segments }
184    }
185
186    pub fn with_index(&self, index: usize) -> Self {
188        let mut segments = self.segments.clone();
189        segments.push(PathSegment::Index(index));
190        Self { segments }
191    }
192
193    pub fn to_string(&self) -> String {
195        if self.segments.is_empty() {
196            return "/".to_string();
197        }
198
199        let mut path = String::new();
200        for segment in &self.segments {
201            path.push('/');
202            match segment {
203                PathSegment::Element(name) => path.push_str(name),
204                PathSegment::Attribute(name) => {
205                    path.push('@');
206                    path.push_str(name);
207                }
208                PathSegment::Text => path.push_str("#text"),
209                PathSegment::Index(idx) => path.push_str(&format!("[{}]", idx)),
210            }
211        }
212        path
213    }
214}
215
216impl fmt::Display for DiffPath {
217    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218        write!(f, "{}", self.to_string())
219    }
220}
221
222#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
224pub enum PathSegment {
225    Element(String),
227    Attribute(String),
229    Text,
231    Index(usize),
233}
234
235#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
237pub enum ChangeType {
238    ElementAdded,
240    ElementRemoved,
242    ElementModified,
244    ElementRenamed,
246    ElementMoved,
248    AttributeAdded,
250    AttributeRemoved,
252    AttributeModified,
254    TextModified,
256}
257
258impl fmt::Display for ChangeType {
259    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260        let s = match self {
261            ChangeType::ElementAdded => "Element Added",
262            ChangeType::ElementRemoved => "Element Removed",
263            ChangeType::ElementModified => "Element Modified",
264            ChangeType::ElementRenamed => "Element Renamed",
265            ChangeType::ElementMoved => "Element Moved",
266            ChangeType::AttributeAdded => "Attribute Added",
267            ChangeType::AttributeRemoved => "Attribute Removed",
268            ChangeType::AttributeModified => "Attribute Modified",
269            ChangeType::TextModified => "Text Modified",
270        };
271        write!(f, "{}", s)
272    }
273}
274
275#[derive(Debug, Clone, Default, Serialize, Deserialize)]
277pub struct ChangeSummary {
278    pub total_changes: usize,
280    pub additions: usize,
282    pub deletions: usize,
284    pub modifications: usize,
286    pub moves: usize,
288    pub critical_changes: usize,
290}
291
292impl ChangeSummary {
293    pub fn has_changes(&self) -> bool {
295        self.total_changes > 0
296    }
297
298    pub fn summary_string(&self) -> String {
300        if !self.has_changes() {
301            return "No changes".to_string();
302        }
303
304        let mut parts = Vec::new();
305
306        if self.additions > 0 {
307            parts.push(format!("{} added", self.additions));
308        }
309        if self.deletions > 0 {
310            parts.push(format!("{} deleted", self.deletions));
311        }
312        if self.modifications > 0 {
313            parts.push(format!("{} modified", self.modifications));
314        }
315        if self.moves > 0 {
316            parts.push(format!("{} moved", self.moves));
317        }
318
319        let summary = parts.join(", ");
320
321        if self.critical_changes > 0 {
322            format!("{} ({} critical)", summary, self.critical_changes)
323        } else {
324            summary
325        }
326    }
327}
328
329#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
331pub enum ImpactLevel {
332    None,
334    Low,
336    Medium,
338    High,
340}
341
342impl fmt::Display for ImpactLevel {
343    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
344        let s = match self {
345            ImpactLevel::None => "None",
346            ImpactLevel::Low => "Low",
347            ImpactLevel::Medium => "Medium",
348            ImpactLevel::High => "High",
349        };
350        write!(f, "{}", s)
351    }
352}
353
354#[derive(Debug, Clone, Serialize, Deserialize)]
356pub struct ChangeContext {
357    pub entity_type: Option<String>,
359
360    pub entity_id: Option<String>,
362
363    pub business_context: Option<String>,
365
366    pub technical_context: IndexMap<String, String>,
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize)]
372pub struct ChangeSignificance {
373    pub critical_fields: Vec<String>,
375
376    pub ignored_fields: Vec<String>,
378
379    pub numeric_tolerance: f64,
381
382    pub ignore_order: bool,
384}
385
386impl Default for ChangeSignificance {
387    fn default() -> Self {
388        Self {
389            critical_fields: vec![
390                "CommercialModelType".to_string(),
391                "TerritoryCode".to_string(),
392                "Price".to_string(),
393                "ValidityPeriod".to_string(),
394                "ReleaseDate".to_string(),
395                "UPC".to_string(),
396                "ISRC".to_string(),
397            ],
398            ignored_fields: vec![
399                "MessageId".to_string(),
400                "MessageCreatedDateTime".to_string(),
401            ],
402            numeric_tolerance: 0.01,
403            ignore_order: true,
404        }
405    }
406}
407
408#[cfg(test)]
409mod tests {
410    use super::*;
411
412    #[test]
413    fn test_diff_path() {
414        let path = DiffPath::root()
415            .with_element("Release")
416            .with_attribute("ReleaseId");
417
418        assert_eq!(path.to_string(), "/Release/@ReleaseId");
419    }
420
421    #[test]
422    fn test_changeset() {
423        let mut changeset = ChangeSet::new();
424
425        changeset.add_change(SemanticChange {
426            path: DiffPath::element("Test"),
427            change_type: ChangeType::ElementAdded,
428            old_value: None,
429            new_value: Some("new".to_string()),
430            is_critical: true,
431            description: "Test change".to_string(),
432        });
433
434        assert!(changeset.has_changes());
435        assert_eq!(changeset.summary.total_changes, 1);
436        assert_eq!(changeset.summary.critical_changes, 1);
437        assert_eq!(changeset.impact_level(), ImpactLevel::High);
438    }
439
440    #[test]
441    fn test_change_summary() {
442        let mut summary = ChangeSummary::default();
443        assert!(!summary.has_changes());
444        assert_eq!(summary.summary_string(), "No changes");
445
446        summary.additions = 2;
447        summary.modifications = 1;
448        summary.critical_changes = 1;
449        summary.total_changes = 3;
450
451        assert!(summary.has_changes());
452        let summary_str = summary.summary_string();
453        assert!(summary_str.contains("2 added"));
454        assert!(summary_str.contains("1 modified"));
455        assert!(summary_str.contains("1 critical"));
456    }
457}