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 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(¤t_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 }
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 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 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}