1use serde::{Serialize, Deserialize};
4use indexmap::IndexMap;
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 { from: DiffPath, to: DiffPath },
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ChangeSet {
25 pub changes: Vec<SemanticChange>,
27
28 pub summary: ChangeSummary,
30
31 pub metadata: IndexMap<String, String>,
33
34 pub timestamp: chrono::DateTime<chrono::Utc>,
36}
37
38impl ChangeSet {
39 pub fn new() -> Self {
41 Self {
42 changes: Vec::new(),
43 summary: ChangeSummary::default(),
44 metadata: IndexMap::new(),
45 timestamp: chrono::Utc::now(),
46 }
47 }
48
49 pub fn add_change(&mut self, change: SemanticChange) {
51 match change.change_type {
53 ChangeType::ElementAdded | ChangeType::AttributeAdded => {
54 self.summary.additions += 1;
55 },
56 ChangeType::ElementRemoved | ChangeType::AttributeRemoved => {
57 self.summary.deletions += 1;
58 },
59 ChangeType::ElementModified | ChangeType::AttributeModified |
60 ChangeType::TextModified | ChangeType::ElementRenamed => {
61 self.summary.modifications += 1;
62 },
63 ChangeType::ElementMoved => {
64 self.summary.moves += 1;
65 },
66 }
67
68 if change.is_critical {
69 self.summary.critical_changes += 1;
70 }
71
72 self.changes.push(change);
73 self.summary.total_changes = self.changes.len();
74 }
75
76 pub fn has_changes(&self) -> bool {
78 !self.changes.is_empty()
79 }
80
81 pub fn critical_changes(&self) -> Vec<&SemanticChange> {
83 self.changes.iter().filter(|c| c.is_critical).collect()
84 }
85
86 pub fn changes_by_type(&self, change_type: ChangeType) -> Vec<&SemanticChange> {
88 self.changes.iter().filter(|c| c.change_type == change_type).collect()
89 }
90
91 pub fn impact_level(&self) -> ImpactLevel {
93 if self.summary.critical_changes > 0 {
94 ImpactLevel::High
95 } else if self.summary.total_changes > 10 {
96 ImpactLevel::Medium
97 } else if self.summary.total_changes > 0 {
98 ImpactLevel::Low
99 } else {
100 ImpactLevel::None
101 }
102 }
103}
104
105impl Default for ChangeSet {
106 fn default() -> Self {
107 Self::new()
108 }
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct SemanticChange {
114 pub path: DiffPath,
116
117 pub change_type: ChangeType,
119
120 pub old_value: Option<String>,
122
123 pub new_value: Option<String>,
125
126 pub is_critical: bool,
128
129 pub description: String,
131}
132
133#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
135pub struct DiffPath {
136 pub segments: Vec<PathSegment>,
138}
139
140impl DiffPath {
141 pub fn root() -> Self {
143 Self { segments: Vec::new() }
144 }
145
146 pub fn element(name: &str) -> Self {
148 Self {
149 segments: vec![PathSegment::Element(name.to_string())]
150 }
151 }
152
153 pub fn with_element(&self, name: &str) -> Self {
155 let mut segments = self.segments.clone();
156 segments.push(PathSegment::Element(name.to_string()));
157 Self { segments }
158 }
159
160 pub fn with_attribute(&self, name: &str) -> Self {
162 let mut segments = self.segments.clone();
163 segments.push(PathSegment::Attribute(name.to_string()));
164 Self { segments }
165 }
166
167 pub fn with_text(&self) -> Self {
169 let mut segments = self.segments.clone();
170 segments.push(PathSegment::Text);
171 Self { segments }
172 }
173
174 pub fn with_index(&self, index: usize) -> Self {
176 let mut segments = self.segments.clone();
177 segments.push(PathSegment::Index(index));
178 Self { segments }
179 }
180
181 pub fn to_string(&self) -> String {
183 if self.segments.is_empty() {
184 return "/".to_string();
185 }
186
187 let mut path = String::new();
188 for segment in &self.segments {
189 path.push('/');
190 match segment {
191 PathSegment::Element(name) => path.push_str(name),
192 PathSegment::Attribute(name) => {
193 path.push('@');
194 path.push_str(name);
195 },
196 PathSegment::Text => path.push_str("#text"),
197 PathSegment::Index(idx) => path.push_str(&format!("[{}]", idx)),
198 }
199 }
200 path
201 }
202}
203
204impl fmt::Display for DiffPath {
205 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206 write!(f, "{}", self.to_string())
207 }
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
212pub enum PathSegment {
213 Element(String),
215 Attribute(String),
217 Text,
219 Index(usize),
221}
222
223#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
225pub enum ChangeType {
226 ElementAdded,
228 ElementRemoved,
230 ElementModified,
232 ElementRenamed,
234 ElementMoved,
236 AttributeAdded,
238 AttributeRemoved,
240 AttributeModified,
242 TextModified,
244}
245
246impl fmt::Display for ChangeType {
247 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248 let s = match self {
249 ChangeType::ElementAdded => "Element Added",
250 ChangeType::ElementRemoved => "Element Removed",
251 ChangeType::ElementModified => "Element Modified",
252 ChangeType::ElementRenamed => "Element Renamed",
253 ChangeType::ElementMoved => "Element Moved",
254 ChangeType::AttributeAdded => "Attribute Added",
255 ChangeType::AttributeRemoved => "Attribute Removed",
256 ChangeType::AttributeModified => "Attribute Modified",
257 ChangeType::TextModified => "Text Modified",
258 };
259 write!(f, "{}", s)
260 }
261}
262
263#[derive(Debug, Clone, Default, Serialize, Deserialize)]
265pub struct ChangeSummary {
266 pub total_changes: usize,
268 pub additions: usize,
270 pub deletions: usize,
272 pub modifications: usize,
274 pub moves: usize,
276 pub critical_changes: usize,
278}
279
280impl ChangeSummary {
281 pub fn has_changes(&self) -> bool {
283 self.total_changes > 0
284 }
285
286 pub fn summary_string(&self) -> String {
288 if !self.has_changes() {
289 return "No changes".to_string();
290 }
291
292 let mut parts = Vec::new();
293
294 if self.additions > 0 {
295 parts.push(format!("{} added", self.additions));
296 }
297 if self.deletions > 0 {
298 parts.push(format!("{} deleted", self.deletions));
299 }
300 if self.modifications > 0 {
301 parts.push(format!("{} modified", self.modifications));
302 }
303 if self.moves > 0 {
304 parts.push(format!("{} moved", self.moves));
305 }
306
307 let summary = parts.join(", ");
308
309 if self.critical_changes > 0 {
310 format!("{} ({} critical)", summary, self.critical_changes)
311 } else {
312 summary
313 }
314 }
315}
316
317#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
319pub enum ImpactLevel {
320 None,
322 Low,
324 Medium,
326 High,
328}
329
330impl fmt::Display for ImpactLevel {
331 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
332 let s = match self {
333 ImpactLevel::None => "None",
334 ImpactLevel::Low => "Low",
335 ImpactLevel::Medium => "Medium",
336 ImpactLevel::High => "High",
337 };
338 write!(f, "{}", s)
339 }
340}
341
342#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct ChangeContext {
345 pub entity_type: Option<String>,
347
348 pub entity_id: Option<String>,
350
351 pub business_context: Option<String>,
353
354 pub technical_context: IndexMap<String, String>,
356}
357
358#[derive(Debug, Clone, Serialize, Deserialize)]
360pub struct ChangeSignificance {
361 pub critical_fields: Vec<String>,
363
364 pub ignored_fields: Vec<String>,
366
367 pub numeric_tolerance: f64,
369
370 pub ignore_order: bool,
372}
373
374impl Default for ChangeSignificance {
375 fn default() -> Self {
376 Self {
377 critical_fields: vec![
378 "CommercialModelType".to_string(),
379 "TerritoryCode".to_string(),
380 "Price".to_string(),
381 "ValidityPeriod".to_string(),
382 "ReleaseDate".to_string(),
383 "UPC".to_string(),
384 "ISRC".to_string(),
385 ],
386 ignored_fields: vec![
387 "MessageId".to_string(),
388 "MessageCreatedDateTime".to_string(),
389 ],
390 numeric_tolerance: 0.01,
391 ignore_order: true,
392 }
393 }
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399
400 #[test]
401 fn test_diff_path() {
402 let path = DiffPath::root()
403 .with_element("Release")
404 .with_attribute("ReleaseId");
405
406 assert_eq!(path.to_string(), "/Release/@ReleaseId");
407 }
408
409 #[test]
410 fn test_changeset() {
411 let mut changeset = ChangeSet::new();
412
413 changeset.add_change(SemanticChange {
414 path: DiffPath::element("Test"),
415 change_type: ChangeType::ElementAdded,
416 old_value: None,
417 new_value: Some("new".to_string()),
418 is_critical: true,
419 description: "Test change".to_string(),
420 });
421
422 assert!(changeset.has_changes());
423 assert_eq!(changeset.summary.total_changes, 1);
424 assert_eq!(changeset.summary.critical_changes, 1);
425 assert_eq!(changeset.impact_level(), ImpactLevel::High);
426 }
427
428 #[test]
429 fn test_change_summary() {
430 let mut summary = ChangeSummary::default();
431 assert!(!summary.has_changes());
432 assert_eq!(summary.summary_string(), "No changes");
433
434 summary.additions = 2;
435 summary.modifications = 1;
436 summary.critical_changes = 1;
437 summary.total_changes = 3;
438
439 assert!(summary.has_changes());
440 let summary_str = summary.summary_string();
441 assert!(summary_str.contains("2 added"));
442 assert!(summary_str.contains("1 modified"));
443 assert!(summary_str.contains("1 critical"));
444 }
445}