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}