ddex_builder/versions/
converter.rs

1use crate::presets::DdexVersion;
2use crate::versions::{ConversionOptions};
3use indexmap::IndexMap;
4use quick_xml::events::{Event, BytesStart, BytesEnd, BytesText};
5use quick_xml::{Reader, Writer};
6use std::io::Cursor;
7
8#[derive(Debug, Clone)]
9pub enum ConversionResult {
10    Success {
11        xml: String,
12        report: ConversionReport,
13    },
14    Failure {
15        error: String,
16        report: ConversionReport,
17    },
18}
19
20#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
21pub struct ConversionReport {
22    pub from_version: DdexVersion,
23    pub to_version: DdexVersion,
24    pub warnings: Vec<ConversionWarning>,
25    pub elements_converted: usize,
26    pub elements_dropped: usize,
27    pub elements_added: usize,
28}
29
30#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
31pub enum ConversionWarningType {
32    ElementRenamed,
33    ElementDropped,
34    ElementAdded,
35    ValidationChanged,
36    NamespaceChanged,
37    FormatMigrated,
38}
39
40#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
41pub struct ConversionWarning {
42    pub warning_type: ConversionWarningType,
43    pub message: String,
44    pub element: Option<String>,
45}
46
47pub struct VersionConverter {
48    conversion_rules: IndexMap<(DdexVersion, DdexVersion), ConversionRules>,
49}
50
51#[derive(Debug, Clone)]
52struct ConversionRules {
53    element_mappings: IndexMap<String, ElementMapping>,
54    namespace_mapping: NamespaceMapping,
55    field_migrations: Vec<FieldMigration>,
56    validation_changes: Vec<ValidationChange>,
57}
58
59#[derive(Debug, Clone)]
60enum ElementMapping {
61    Direct(String),
62    Renamed(String),
63    Split { into: Vec<String>, splitter: fn(&str) -> Vec<String> },
64    Merge { from: Vec<String>, merger: fn(Vec<&str>) -> String },
65    Deprecated { replacement: Option<String>, warning: String },
66    New { default_value: Option<String> },
67}
68
69#[derive(Debug, Clone)]
70struct NamespaceMapping {
71    from: String,
72    to: String,
73    schema_version_from: String,
74    schema_version_to: String,
75}
76
77#[derive(Debug, Clone)]
78struct FieldMigration {
79    element: String,
80    field: String,
81    migration_type: MigrationType,
82}
83
84#[derive(Debug, Clone)]
85enum MigrationType {
86    FormatChange { from_pattern: String, to_pattern: String },
87    ValueMapping(IndexMap<String, String>),
88    ValidationChange { old_rules: Vec<String>, new_rules: Vec<String> },
89}
90
91#[derive(Debug, Clone)]
92struct ValidationChange {
93    element: String,
94    change_type: ValidationChangeType,
95}
96
97#[derive(Debug, Clone)]
98enum ValidationChangeType {
99    RequiredAdded(String),
100    RequiredRemoved(String),
101    OptionalAdded(String),
102    OptionalRemoved(String),
103    FormatChanged { field: String, old_format: String, new_format: String },
104}
105
106impl VersionConverter {
107    pub fn new() -> Self {
108        let mut converter = Self {
109            conversion_rules: IndexMap::new(),
110        };
111        converter.initialize_conversion_rules();
112        converter
113    }
114
115    fn initialize_conversion_rules(&mut self) {
116        self.add_382_to_42_rules();
117        self.add_42_to_43_rules();
118        self.add_43_to_42_rules();
119        self.add_42_to_382_rules();
120    }
121
122    fn add_382_to_42_rules(&mut self) {
123        let mut element_mappings = IndexMap::new();
124        
125        element_mappings.insert(
126            "SoundRecording".to_string(),
127            ElementMapping::Direct("SoundRecording".to_string())
128        );
129        
130        element_mappings.insert(
131            "TechnicalSoundRecordingDetails".to_string(),
132            ElementMapping::Renamed("TechnicalDetails".to_string())
133        );
134        
135        element_mappings.insert(
136            "CommercialModelType".to_string(),
137            ElementMapping::New { default_value: Some("SubscriptionModel".to_string()) }
138        );
139
140        element_mappings.insert(
141            "Territory".to_string(),
142            ElementMapping::Direct("Territory".to_string())
143        );
144
145        let rules = ConversionRules {
146            element_mappings,
147            namespace_mapping: NamespaceMapping {
148                from: "http://ddex.net/xml/ern/382".to_string(),
149                to: "http://ddex.net/xml/ern/42".to_string(),
150                schema_version_from: "ern/382".to_string(),
151                schema_version_to: "ern/42".to_string(),
152            },
153            field_migrations: vec![
154                FieldMigration {
155                    element: "Duration".to_string(),
156                    field: "value".to_string(),
157                    migration_type: MigrationType::FormatChange {
158                        from_pattern: r"^PT\d+S$".to_string(),
159                        to_pattern: r"^PT(\d+H)?(\d+M)?\d+(\.\d+)?S$".to_string(),
160                    },
161                },
162            ],
163            validation_changes: vec![
164                ValidationChange {
165                    element: "SoundRecording".to_string(),
166                    change_type: ValidationChangeType::OptionalAdded("HashSum".to_string()),
167                },
168                ValidationChange {
169                    element: "TechnicalDetails".to_string(),
170                    change_type: ValidationChangeType::OptionalAdded("BitRate".to_string()),
171                },
172            ],
173        };
174
175        self.conversion_rules.insert((DdexVersion::Ern382, DdexVersion::Ern42), rules);
176    }
177
178    fn add_42_to_43_rules(&mut self) {
179        let mut element_mappings = IndexMap::new();
180        
181        element_mappings.insert(
182            "SoundRecording".to_string(),
183            ElementMapping::Direct("SoundRecording".to_string())
184        );
185        
186        element_mappings.insert(
187            "VideoResource".to_string(),
188            ElementMapping::New { default_value: None }
189        );
190        
191        element_mappings.insert(
192            "HashSum".to_string(),
193            ElementMapping::Direct("HashSum".to_string())
194        );
195
196        let rules = ConversionRules {
197            element_mappings,
198            namespace_mapping: NamespaceMapping {
199                from: "http://ddex.net/xml/ern/42".to_string(),
200                to: "http://ddex.net/xml/ern/43".to_string(),
201                schema_version_from: "ern/42".to_string(),
202                schema_version_to: "ern/43".to_string(),
203            },
204            field_migrations: vec![
205                FieldMigration {
206                    element: "ISRC".to_string(),
207                    field: "value".to_string(),
208                    migration_type: MigrationType::ValidationChange {
209                        old_rules: vec![r"^[A-Z]{2}[A-Z0-9]{3}\d{7}$".to_string()],
210                        new_rules: vec![r"^[A-Z]{2}-?[A-Z0-9]{3}-?\d{2}-?\d{5}$".to_string()],
211                    },
212                },
213            ],
214            validation_changes: vec![
215                ValidationChange {
216                    element: "SoundRecording".to_string(),
217                    change_type: ValidationChangeType::RequiredAdded("ProprietaryId".to_string()),
218                },
219                ValidationChange {
220                    element: "VideoResource".to_string(),
221                    change_type: ValidationChangeType::OptionalAdded("Duration".to_string()),
222                },
223            ],
224        };
225
226        self.conversion_rules.insert((DdexVersion::Ern42, DdexVersion::Ern43), rules);
227    }
228
229    fn add_43_to_42_rules(&mut self) {
230        let mut element_mappings = IndexMap::new();
231        
232        element_mappings.insert(
233            "SoundRecording".to_string(),
234            ElementMapping::Direct("SoundRecording".to_string())
235        );
236        
237        element_mappings.insert(
238            "VideoResource".to_string(),
239            ElementMapping::Deprecated { 
240                replacement: None,
241                warning: "VideoResource not supported in ERN 4.2, will be omitted".to_string()
242            }
243        );
244        
245        element_mappings.insert(
246            "HashSum".to_string(),
247            ElementMapping::Direct("HashSum".to_string())
248        );
249
250        let rules = ConversionRules {
251            element_mappings,
252            namespace_mapping: NamespaceMapping {
253                from: "http://ddex.net/xml/ern/43".to_string(),
254                to: "http://ddex.net/xml/ern/42".to_string(),
255                schema_version_from: "ern/43".to_string(),
256                schema_version_to: "ern/42".to_string(),
257            },
258            field_migrations: vec![],
259            validation_changes: vec![
260                ValidationChange {
261                    element: "SoundRecording".to_string(),
262                    change_type: ValidationChangeType::RequiredRemoved("ProprietaryId".to_string()),
263                },
264            ],
265        };
266
267        self.conversion_rules.insert((DdexVersion::Ern43, DdexVersion::Ern42), rules);
268    }
269
270    fn add_42_to_382_rules(&mut self) {
271        let mut element_mappings = IndexMap::new();
272        
273        element_mappings.insert(
274            "SoundRecording".to_string(),
275            ElementMapping::Direct("SoundRecording".to_string())
276        );
277        
278        element_mappings.insert(
279            "TechnicalDetails".to_string(),
280            ElementMapping::Renamed("TechnicalSoundRecordingDetails".to_string())
281        );
282        
283        element_mappings.insert(
284            "CommercialModelType".to_string(),
285            ElementMapping::Deprecated { 
286                replacement: None,
287                warning: "CommercialModelType not supported in ERN 3.8.2, will be omitted".to_string()
288            }
289        );
290
291        let rules = ConversionRules {
292            element_mappings,
293            namespace_mapping: NamespaceMapping {
294                from: "http://ddex.net/xml/ern/42".to_string(),
295                to: "http://ddex.net/xml/ern/382".to_string(),
296                schema_version_from: "ern/42".to_string(),
297                schema_version_to: "ern/382".to_string(),
298            },
299            field_migrations: vec![],
300            validation_changes: vec![
301                ValidationChange {
302                    element: "SoundRecording".to_string(),
303                    change_type: ValidationChangeType::OptionalRemoved("HashSum".to_string()),
304                },
305            ],
306        };
307
308        self.conversion_rules.insert((DdexVersion::Ern42, DdexVersion::Ern382), rules);
309    }
310
311    pub fn convert(&self, xml_content: &str, from_version: DdexVersion, to_version: DdexVersion, options: Option<ConversionOptions>) -> ConversionResult {
312        let options = options.unwrap_or_default();
313        let mut report = ConversionReport {
314            from_version,
315            to_version,
316            warnings: Vec::new(),
317            elements_converted: 0,
318            elements_dropped: 0,
319            elements_added: 0,
320        };
321
322        if from_version == to_version {
323            return ConversionResult::Success {
324                xml: xml_content.to_string(),
325                report,
326            };
327        }
328
329        let conversion_path = self.find_conversion_path(from_version, to_version);
330        match conversion_path {
331            Some(path) => self.execute_conversion_path(xml_content, &path, options, &mut report),
332            None => ConversionResult::Failure {
333                error: format!("No conversion path found from {:?} to {:?}", from_version, to_version),
334                report,
335            }
336        }
337    }
338
339    fn find_conversion_path(&self, from: DdexVersion, to: DdexVersion) -> Option<Vec<DdexVersion>> {
340        if let Some(_) = self.conversion_rules.get(&(from, to)) {
341            return Some(vec![from, to]);
342        }
343
344        // Check for multi-step conversions
345        match (from, to) {
346            (DdexVersion::Ern382, DdexVersion::Ern43) => {
347                Some(vec![DdexVersion::Ern382, DdexVersion::Ern42, DdexVersion::Ern43])
348            }
349            (DdexVersion::Ern43, DdexVersion::Ern382) => {
350                Some(vec![DdexVersion::Ern43, DdexVersion::Ern42, DdexVersion::Ern382])
351            }
352            _ => None,
353        }
354    }
355
356    fn execute_conversion_path(&self, xml_content: &str, path: &[DdexVersion], options: ConversionOptions, report: &mut ConversionReport) -> ConversionResult {
357        let mut current_xml = xml_content.to_string();
358        
359        for window in path.windows(2) {
360            let from = window[0];
361            let to = window[1];
362            
363            match self.convert_single_step(&current_xml, from, to, &options, report) {
364                ConversionResult::Success { xml, report: step_report } => {
365                    current_xml = xml;
366                    report.warnings.extend(step_report.warnings);
367                    report.elements_converted += step_report.elements_converted;
368                    report.elements_dropped += step_report.elements_dropped;
369                    report.elements_added += step_report.elements_added;
370                }
371                ConversionResult::Failure { error, .. } => {
372                    return ConversionResult::Failure { error, report: report.clone() };
373                }
374            }
375        }
376
377        ConversionResult::Success {
378            xml: current_xml,
379            report: report.clone(),
380        }
381    }
382
383    fn convert_single_step(&self, xml_content: &str, from: DdexVersion, to: DdexVersion, options: &ConversionOptions, report: &mut ConversionReport) -> ConversionResult {
384        let rules = match self.conversion_rules.get(&(from, to)) {
385            Some(rules) => rules,
386            None => return ConversionResult::Failure {
387                error: format!("No direct conversion rules from {:?} to {:?}", from, to),
388                report: report.clone(),
389            },
390        };
391
392        match self.transform_xml(xml_content, rules, options) {
393            Ok((transformed_xml, conversion_warnings)) => {
394                report.warnings.extend(conversion_warnings);
395                ConversionResult::Success {
396                    xml: transformed_xml,
397                    report: report.clone(),
398                }
399            }
400            Err(error) => ConversionResult::Failure {
401                error: error.to_string(),
402                report: report.clone(),
403            }
404        }
405    }
406
407    fn transform_xml(&self, xml_content: &str, rules: &ConversionRules, options: &ConversionOptions) -> Result<(String, Vec<ConversionWarning>), Box<dyn std::error::Error>> {
408        let mut reader = Reader::from_str(xml_content);
409        let mut writer = Writer::new(Cursor::new(Vec::new()));
410        let mut warnings = Vec::new();
411        let mut buf = Vec::new();
412        let mut elements_stack = Vec::new();
413        let mut skip_element = false;
414        let mut skip_depth = 0;
415
416        loop {
417            match reader.read_event_into(&mut buf) {
418                Ok(Event::Start(ref e)) => {
419                    let element_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
420                    elements_stack.push(element_name.clone());
421
422                    if skip_element {
423                        skip_depth += 1;
424                        continue;
425                    }
426
427                    match rules.element_mappings.get(&element_name) {
428                        Some(ElementMapping::Direct(new_name)) => {
429                            let mut new_element = BytesStart::new(new_name);
430                            for attr in e.attributes() {
431                                if let Ok(attr) = attr {
432                                    new_element.push_attribute(attr);
433                                }
434                            }
435                            self.update_namespace_attributes(&mut new_element, &rules.namespace_mapping);
436                            writer.write_event(Event::Start(new_element))?;
437                        }
438                        Some(ElementMapping::Renamed(new_name)) => {
439                            let mut new_element = BytesStart::new(new_name);
440                            for attr in e.attributes() {
441                                if let Ok(attr) = attr {
442                                    new_element.push_attribute(attr);
443                                }
444                            }
445                            self.update_namespace_attributes(&mut new_element, &rules.namespace_mapping);
446                            writer.write_event(Event::Start(new_element))?;
447                            warnings.push(ConversionWarning {
448                                warning_type: ConversionWarningType::ElementRenamed,
449                                message: format!("Element '{}' renamed to '{}'", element_name, new_name),
450                                element: Some(element_name),
451                            });
452                        }
453                        Some(ElementMapping::Deprecated { replacement: _, warning }) => {
454                            skip_element = true;
455                            skip_depth = 1;
456                            warnings.push(ConversionWarning {
457                                warning_type: ConversionWarningType::ElementDropped,
458                                message: warning.clone(),
459                                element: Some(element_name),
460                            });
461                        }
462                        Some(ElementMapping::New { .. }) => {
463                            writer.write_event(Event::Start(e.clone()))?;
464                        }
465                        _ => {
466                            let mut cloned_element = e.clone();
467                            self.update_namespace_attributes(&mut cloned_element, &rules.namespace_mapping);
468                            writer.write_event(Event::Start(cloned_element))?;
469                        }
470                    }
471                }
472                Ok(Event::End(ref e)) => {
473                    elements_stack.pop();
474
475                    if skip_element {
476                        skip_depth -= 1;
477                        if skip_depth == 0 {
478                            skip_element = false;
479                        }
480                        continue;
481                    }
482
483                    let element_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
484                    match rules.element_mappings.get(&element_name) {
485                        Some(ElementMapping::Direct(new_name)) => {
486                            writer.write_event(Event::End(BytesEnd::new(new_name)))?;
487                        }
488                        Some(ElementMapping::Renamed(new_name)) => {
489                            writer.write_event(Event::End(BytesEnd::new(new_name)))?;
490                        }
491                        Some(ElementMapping::Deprecated { .. }) => {
492                            // Skip deprecated elements
493                        }
494                        _ => {
495                            writer.write_event(Event::End(e.clone()))?;
496                        }
497                    }
498                }
499                Ok(Event::Text(ref e)) => {
500                    if !skip_element {
501                        writer.write_event(Event::Text(e.clone()))?;
502                    }
503                }
504                Ok(Event::Comment(ref e)) => {
505                    if !skip_element && options.preserve_comments {
506                        writer.write_event(Event::Comment(e.clone()))?;
507                    }
508                }
509                Ok(Event::CData(ref e)) => {
510                    if !skip_element {
511                        writer.write_event(Event::CData(e.clone()))?;
512                    }
513                }
514                Ok(Event::Decl(ref e)) => {
515                    writer.write_event(Event::Decl(e.clone()))?;
516                }
517                Ok(Event::PI(ref e)) => {
518                    if !skip_element {
519                        writer.write_event(Event::PI(e.clone()))?;
520                    }
521                }
522                Ok(Event::DocType(ref e)) => {
523                    writer.write_event(Event::DocType(e.clone()))?;
524                }
525                Ok(Event::Empty(ref e)) => {
526                    if !skip_element {
527                        writer.write_event(Event::Empty(e.clone()))?;
528                    }
529                }
530                Ok(Event::Eof) => break,
531                Err(e) => return Err(format!("Error parsing XML: {}", e).into()),
532            }
533            buf.clear();
534        }
535
536        let result = writer.into_inner().into_inner();
537        let transformed_xml = String::from_utf8(result)?;
538        Ok((transformed_xml, warnings))
539    }
540
541    fn update_namespace_attributes(&self, element: &mut BytesStart, namespace_mapping: &NamespaceMapping) {
542        // Update xmlns attributes to new namespace
543        let mut attrs_to_update = Vec::new();
544        
545        for (i, attr_result) in element.attributes().enumerate() {
546            if let Ok(attr) = attr_result {
547                let key = String::from_utf8_lossy(attr.key.as_ref());
548                let value = String::from_utf8_lossy(&attr.value);
549                
550                if key == "xmlns" && value == namespace_mapping.from {
551                    attrs_to_update.push((i, "xmlns".to_string(), namespace_mapping.to.clone()));
552                } else if key.starts_with("xmlns:") && value == namespace_mapping.from {
553                    attrs_to_update.push((i, key.to_string(), namespace_mapping.to.clone()));
554                }
555            }
556        }
557
558        // Apply namespace updates
559        for (_, key, new_value) in attrs_to_update {
560            element.extend_attributes(std::iter::once((key.as_str(), new_value.as_str())));
561        }
562    }
563
564    pub fn get_supported_conversions(&self) -> Vec<(DdexVersion, DdexVersion)> {
565        self.conversion_rules.keys().cloned().collect()
566    }
567
568    pub fn can_convert(&self, from: DdexVersion, to: DdexVersion) -> bool {
569        self.find_conversion_path(from, to).is_some()
570    }
571}
572
573impl Default for VersionConverter {
574    fn default() -> Self {
575        Self::new()
576    }
577}
578
579#[cfg(test)]
580mod tests {
581    use super::*;
582
583    #[test]
584    fn test_converter_initialization() {
585        let converter = VersionConverter::new();
586        assert!(!converter.conversion_rules.is_empty());
587    }
588
589    #[test]
590    fn test_direct_conversion_path() {
591        let converter = VersionConverter::new();
592        let path = converter.find_conversion_path(DdexVersion::Ern382, DdexVersion::Ern42);
593        assert_eq!(path, Some(vec![DdexVersion::Ern382, DdexVersion::Ern42]));
594    }
595
596    #[test]
597    fn test_multi_step_conversion_path() {
598        let converter = VersionConverter::new();
599        let path = converter.find_conversion_path(DdexVersion::Ern382, DdexVersion::Ern43);
600        assert_eq!(path, Some(vec![DdexVersion::Ern382, DdexVersion::Ern42, DdexVersion::Ern43]));
601    }
602
603    #[test]
604    fn test_same_version_conversion() {
605        let converter = VersionConverter::new();
606        let xml = r#"<?xml version="1.0" encoding="UTF-8"?><test>content</test>"#;
607        let result = converter.convert(xml, DdexVersion::Ern42, DdexVersion::Ern42, None);
608        
609        match result {
610            ConversionResult::Success { xml: result_xml, .. } => {
611                assert_eq!(result_xml, xml);
612            }
613            _ => panic!("Expected successful conversion for same version"),
614        }
615    }
616
617    #[test]
618    fn test_supported_conversions() {
619        let converter = VersionConverter::new();
620        let conversions = converter.get_supported_conversions();
621        assert!(conversions.contains(&(DdexVersion::Ern382, DdexVersion::Ern42)));
622        assert!(conversions.contains(&(DdexVersion::Ern42, DdexVersion::Ern43)));
623        assert!(conversions.contains(&(DdexVersion::Ern43, DdexVersion::Ern42)));
624        assert!(conversions.contains(&(DdexVersion::Ern42, DdexVersion::Ern382)));
625    }
626}