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