1use crate::error::BuildError;
28use crate::presets::DdexVersion;
29use indexmap::IndexMap;
30use serde::{Deserialize, Serialize};
31
32mod ern_382;
33mod ern_42;
34mod ern_43;
35mod converter;
36
37pub use ern_382::*;
38pub use ern_42::*;
39pub use ern_43::*;
40pub use converter::{VersionConverter, ConversionResult as ConverterResult, ConversionReport as ConverterReport, ConversionWarning as ConverterWarning, ConversionWarningType};
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct VersionSpec {
45 pub version: DdexVersion,
47 pub namespace: String,
49 pub schema_location: Option<String>,
51 pub message_schema_version_id: String,
53 pub supported_message_types: Vec<String>,
55 pub element_mappings: IndexMap<String, String>,
57 pub required_elements: Vec<String>,
59 pub deprecated_elements: Vec<String>,
61 pub new_elements: Vec<String>,
63 pub namespace_prefixes: IndexMap<String, String>,
65}
66
67#[derive(Debug, Clone)]
69pub struct ConversionResult {
70 pub converted_xml: String,
72 pub source_version: DdexVersion,
74 pub target_version: DdexVersion,
76 pub report: ConversionReport,
78 pub metadata: ConversionMetadata,
80}
81
82#[derive(Debug, Clone)]
84pub struct ConversionReport {
85 pub conversions: Vec<ElementConversion>,
87 pub warnings: Vec<ConversionWarning>,
89 pub errors: Vec<ConversionError>,
91 pub unconvertible_elements: Vec<String>,
93 pub data_loss_warnings: Vec<String>,
95 pub compatibility_notes: Vec<String>,
97}
98
99#[derive(Debug, Clone)]
101pub struct ElementConversion {
102 pub source_path: String,
104 pub target_path: String,
106 pub conversion_type: ConversionType,
108 pub notes: Option<String>,
110}
111
112#[derive(Debug, Clone, PartialEq)]
114pub enum ConversionType {
115 DirectMapping,
117 Renamed { old_name: String, new_name: String },
119 Restructured { description: String },
121 Added { default_value: Option<String> },
123 Removed { reason: String },
125 Moved { old_path: String, new_path: String },
127 Transformed { description: String },
129}
130
131#[derive(Debug, Clone)]
133pub struct ConversionWarning {
134 pub code: String,
136 pub message: String,
138 pub element_path: Option<String>,
140 pub suggestion: Option<String>,
142 pub impact: ImpactLevel,
144}
145
146#[derive(Debug, Clone)]
148pub struct ConversionError {
149 pub code: String,
151 pub message: String,
153 pub element_path: String,
155 pub fallback: Option<String>,
157}
158
159#[derive(Debug, Clone, PartialEq)]
161pub enum ImpactLevel {
162 Low,
164 Medium,
166 High,
168 Critical,
170}
171
172#[derive(Debug, Clone)]
174pub struct ConversionMetadata {
175 pub converted_at: chrono::DateTime<chrono::Utc>,
177 pub conversion_time: std::time::Duration,
179 pub elements_processed: usize,
181 pub warning_count: usize,
183 pub error_count: usize,
185 pub fidelity_percentage: f64,
187}
188
189#[derive(Debug, Clone)]
191pub struct VersionDetection {
192 pub detected_version: DdexVersion,
194 pub confidence: f64,
196 pub clues: Vec<DetectionClue>,
198 pub ambiguities: Vec<String>,
200}
201
202#[derive(Debug, Clone)]
204pub struct DetectionClue {
205 pub clue_type: ClueType,
207 pub evidence: String,
209 pub weight: f64,
211}
212
213#[derive(Debug, Clone)]
215pub enum ClueType {
216 Namespace,
218 SchemaLocation,
220 MessageSchemaVersionId,
222 VersionSpecificElement,
224 StructuralPattern,
226 NamespacePrefix,
228}
229
230#[derive(Debug, Clone)]
232pub struct CompatibilityMatrix {
233 pub conversion_paths: Vec<ConversionPath>,
235 pub feature_compatibility: IndexMap<String, FeatureSupport>,
237 pub recommended_strategies: Vec<ConversionStrategy>,
239}
240
241#[derive(Debug, Clone)]
243pub struct ConversionPath {
244 pub from: DdexVersion,
246 pub to: DdexVersion,
248 pub difficulty: ConversionDifficulty,
250 pub fidelity: f64,
252 pub major_changes: Vec<String>,
254 pub production_ready: bool,
256}
257
258#[derive(Debug, Clone, PartialEq)]
260pub enum ConversionDifficulty {
261 Trivial,
263 Moderate,
265 Complex,
267 Challenging,
269}
270
271#[derive(Debug, Clone)]
273pub struct FeatureSupport {
274 pub feature: String,
276 pub ern_382: SupportLevel,
278 pub ern_42: SupportLevel,
280 pub ern_43: SupportLevel,
282 pub migration_notes: Option<String>,
284}
285
286#[derive(Debug, Clone, PartialEq)]
288pub enum SupportLevel {
289 Full,
291 Partial,
293 None,
295 Deprecated,
297 New,
299}
300
301#[derive(Debug, Clone)]
303pub struct ConversionStrategy {
304 pub name: String,
306 pub description: String,
308 pub scenarios: Vec<String>,
310 pub steps: Vec<String>,
312 pub outcomes: Vec<String>,
314 pub risk_level: RiskLevel,
316}
317
318#[derive(Debug, Clone, PartialEq)]
320pub enum RiskLevel {
321 Low,
323 Medium,
325 High,
327 VeryHigh,
329}
330
331#[derive(Debug, Clone)]
333pub struct VersionManager {
334 version_specs: IndexMap<DdexVersion, VersionSpec>,
336 compatibility: CompatibilityMatrix,
338 default_options: ConversionOptions,
340}
341
342#[derive(Debug, Clone)]
344pub struct ConversionOptions {
345 pub allow_lossy: bool,
347 pub detailed_reports: bool,
349 pub preserve_unknown: bool,
351 pub add_metadata: bool,
353 pub preserve_comments: bool,
355 pub validation_level: ValidationLevel,
357 pub custom_mappings: IndexMap<String, String>,
359}
360
361#[derive(Debug, Clone, PartialEq)]
363pub enum ValidationLevel {
364 None,
366 Basic,
368 Schema,
370 Full,
372}
373
374impl Default for ConversionOptions {
375 fn default() -> Self {
376 Self {
377 allow_lossy: false,
378 detailed_reports: true,
379 preserve_unknown: false,
380 add_metadata: true,
381 preserve_comments: false,
382 validation_level: ValidationLevel::Schema,
383 custom_mappings: IndexMap::new(),
384 }
385 }
386}
387
388impl VersionManager {
389 pub fn new() -> Self {
391 Self {
392 version_specs: Self::load_default_specs(),
393 compatibility: Self::build_compatibility_matrix(),
394 default_options: ConversionOptions::default(),
395 }
396 }
397
398 pub fn get_version_spec(&self, version: DdexVersion) -> Option<&VersionSpec> {
400 self.version_specs.get(&version)
401 }
402
403 pub fn detect_version(&self, xml_content: &str) -> Result<VersionDetection, BuildError> {
405 let mut clues = Vec::new();
406 let mut version_scores = IndexMap::new();
407
408 for version in [DdexVersion::Ern382, DdexVersion::Ern42, DdexVersion::Ern43] {
410 version_scores.insert(version, 0.0);
411 }
412
413 if let Some(namespace) = self.extract_namespace(xml_content) {
415 clues.push(DetectionClue {
416 clue_type: ClueType::Namespace,
417 evidence: namespace.clone(),
418 weight: 0.8,
419 });
420
421 for (version, spec) in &self.version_specs {
423 if spec.namespace == namespace {
424 *version_scores.get_mut(version).unwrap() += 0.8;
425 }
426 }
427 }
428
429 if let Some(schema_version) = self.extract_message_schema_version(xml_content) {
431 clues.push(DetectionClue {
432 clue_type: ClueType::MessageSchemaVersionId,
433 evidence: schema_version.clone(),
434 weight: 0.9,
435 });
436
437 for (version, spec) in &self.version_specs {
439 if spec.message_schema_version_id == schema_version {
440 *version_scores.get_mut(version).unwrap() += 0.9;
441 }
442 }
443 }
444
445 for (version, spec) in &self.version_specs {
447 for element in &spec.new_elements {
448 if xml_content.contains(&format!("<{}", element)) {
449 clues.push(DetectionClue {
450 clue_type: ClueType::VersionSpecificElement,
451 evidence: element.clone(),
452 weight: 0.6,
453 });
454 *version_scores.get_mut(version).unwrap() += 0.6;
455 }
456 }
457 }
458
459 let (detected_version, confidence) = version_scores
461 .into_iter()
462 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
463 .unwrap();
464
465 let normalized_confidence = (confidence / 2.5_f64).min(1.0_f64); Ok(VersionDetection {
468 detected_version,
469 confidence: normalized_confidence,
470 clues,
471 ambiguities: Vec::new(), })
473 }
474
475 pub fn is_conversion_supported(&self, from: DdexVersion, to: DdexVersion) -> bool {
477 self.compatibility.conversion_paths.iter()
478 .any(|path| path.from == from && path.to == to)
479 }
480
481 pub fn get_conversion_path(&self, from: DdexVersion, to: DdexVersion) -> Option<&ConversionPath> {
483 self.compatibility.conversion_paths.iter()
484 .find(|path| path.from == from && path.to == to)
485 }
486
487 pub fn get_feature_compatibility(&self, feature: &str) -> Option<&FeatureSupport> {
489 self.compatibility.feature_compatibility.get(feature)
490 }
491
492 pub fn get_recommended_strategy(&self, from: DdexVersion, to: DdexVersion) -> Option<&ConversionStrategy> {
494 let scenario = format!("{:?} to {:?}", from, to);
495 self.compatibility.recommended_strategies.iter()
496 .find(|strategy| strategy.scenarios.contains(&scenario))
497 }
498
499 fn load_default_specs() -> IndexMap<DdexVersion, VersionSpec> {
502 let mut specs = IndexMap::new();
503
504 specs.insert(DdexVersion::Ern382, ern_382::get_version_spec());
505 specs.insert(DdexVersion::Ern42, ern_42::get_version_spec());
506 specs.insert(DdexVersion::Ern43, ern_43::get_version_spec());
507
508 specs
509 }
510
511 fn build_compatibility_matrix() -> CompatibilityMatrix {
512 let conversion_paths = vec![
513 ConversionPath {
515 from: DdexVersion::Ern382,
516 to: DdexVersion::Ern42,
517 difficulty: ConversionDifficulty::Moderate,
518 fidelity: 0.85,
519 major_changes: vec![
520 "Namespace migration".to_string(),
521 "Element structure updates".to_string(),
522 "New optional elements".to_string(),
523 ],
524 production_ready: true,
525 },
526 ConversionPath {
527 from: DdexVersion::Ern42,
528 to: DdexVersion::Ern43,
529 difficulty: ConversionDifficulty::Trivial,
530 fidelity: 0.95,
531 major_changes: vec![
532 "Minor element additions".to_string(),
533 "Enhanced validation rules".to_string(),
534 ],
535 production_ready: true,
536 },
537 ConversionPath {
538 from: DdexVersion::Ern382,
539 to: DdexVersion::Ern43,
540 difficulty: ConversionDifficulty::Complex,
541 fidelity: 0.80,
542 major_changes: vec![
543 "Major namespace changes".to_string(),
544 "Significant structural updates".to_string(),
545 "New required elements".to_string(),
546 ],
547 production_ready: true,
548 },
549 ConversionPath {
551 from: DdexVersion::Ern43,
552 to: DdexVersion::Ern42,
553 difficulty: ConversionDifficulty::Moderate,
554 fidelity: 0.90,
555 major_changes: vec![
556 "Remove newer elements".to_string(),
557 "Downgrade validation rules".to_string(),
558 ],
559 production_ready: true,
560 },
561 ConversionPath {
562 from: DdexVersion::Ern42,
563 to: DdexVersion::Ern382,
564 difficulty: ConversionDifficulty::Challenging,
565 fidelity: 0.75,
566 major_changes: vec![
567 "Legacy namespace mapping".to_string(),
568 "Remove modern elements".to_string(),
569 "Structural downgrade".to_string(),
570 ],
571 production_ready: false,
572 },
573 ConversionPath {
574 from: DdexVersion::Ern43,
575 to: DdexVersion::Ern382,
576 difficulty: ConversionDifficulty::Challenging,
577 fidelity: 0.70,
578 major_changes: vec![
579 "Major structural downgrade".to_string(),
580 "Significant feature removal".to_string(),
581 "Legacy compatibility layer".to_string(),
582 ],
583 production_ready: false,
584 },
585 ];
586
587 let feature_compatibility = Self::build_feature_compatibility();
588 let recommended_strategies = Self::build_recommended_strategies();
589
590 CompatibilityMatrix {
591 conversion_paths,
592 feature_compatibility,
593 recommended_strategies,
594 }
595 }
596
597 fn build_feature_compatibility() -> IndexMap<String, FeatureSupport> {
598 let mut features = IndexMap::new();
599
600 features.insert("ResourceReference".to_string(), FeatureSupport {
601 feature: "Resource Reference Elements".to_string(),
602 ern_382: SupportLevel::Partial,
603 ern_42: SupportLevel::Full,
604 ern_43: SupportLevel::Full,
605 migration_notes: Some("Enhanced in 4.2 with better linking".to_string()),
606 });
607
608 features.insert("DetailedDealTerms".to_string(), FeatureSupport {
609 feature: "Detailed Deal Terms".to_string(),
610 ern_382: SupportLevel::None,
611 ern_42: SupportLevel::Partial,
612 ern_43: SupportLevel::Full,
613 migration_notes: Some("New detailed terms structure in 4.2+".to_string()),
614 });
615
616 features.insert("EnhancedMetadata".to_string(), FeatureSupport {
617 feature: "Enhanced Metadata Fields".to_string(),
618 ern_382: SupportLevel::None,
619 ern_42: SupportLevel::None,
620 ern_43: SupportLevel::New,
621 migration_notes: Some("Completely new in 4.3".to_string()),
622 });
623
624 features.insert("DeprecatedElements".to_string(), FeatureSupport {
625 feature: "Legacy Deprecated Elements".to_string(),
626 ern_382: SupportLevel::Full,
627 ern_42: SupportLevel::Deprecated,
628 ern_43: SupportLevel::None,
629 migration_notes: Some("Removed in 4.3, use modern equivalents".to_string()),
630 });
631
632 features
633 }
634
635 fn build_recommended_strategies() -> Vec<ConversionStrategy> {
636 vec![
637 ConversionStrategy {
638 name: "Conservative Upgrade".to_string(),
639 description: "Step-by-step version upgrade with validation".to_string(),
640 scenarios: vec!["Ern382 to Ern43".to_string()],
641 steps: vec![
642 "Validate source ERN 3.8.2 message".to_string(),
643 "Convert 3.8.2 → 4.2 with warnings".to_string(),
644 "Validate intermediate 4.2 message".to_string(),
645 "Convert 4.2 → 4.3 with enhancements".to_string(),
646 "Final validation and report".to_string(),
647 ],
648 outcomes: vec![
649 "High-fidelity conversion".to_string(),
650 "Detailed conversion report".to_string(),
651 "Step-by-step validation".to_string(),
652 ],
653 risk_level: RiskLevel::Low,
654 },
655 ConversionStrategy {
656 name: "Direct Upgrade".to_string(),
657 description: "Direct conversion between versions".to_string(),
658 scenarios: vec!["Ern42 to Ern43".to_string()],
659 steps: vec![
660 "Validate source message".to_string(),
661 "Apply direct conversion mappings".to_string(),
662 "Add new optional elements".to_string(),
663 "Validate target message".to_string(),
664 ],
665 outcomes: vec![
666 "Fast conversion".to_string(),
667 "Minimal data transformation".to_string(),
668 ],
669 risk_level: RiskLevel::Low,
670 },
671 ]
672 }
673
674 fn extract_namespace(&self, xml_content: &str) -> Option<String> {
675 let re = regex::Regex::new(r#"xmlns="([^"]+)""#).ok()?;
677 re.captures(xml_content)?
678 .get(1)
679 .map(|m| m.as_str().to_string())
680 }
681
682 fn extract_message_schema_version(&self, xml_content: &str) -> Option<String> {
683 let re = regex::Regex::new(r#"MessageSchemaVersionId="([^"]+)""#).ok()?;
684 re.captures(xml_content)?
685 .get(1)
686 .map(|m| m.as_str().to_string())
687 }
688}
689
690impl Default for VersionManager {
691 fn default() -> Self {
692 Self::new()
693 }
694}
695
696
697impl std::fmt::Display for ConversionDifficulty {
698 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
699 match self {
700 ConversionDifficulty::Trivial => write!(f, "Trivial"),
701 ConversionDifficulty::Moderate => write!(f, "Moderate"),
702 ConversionDifficulty::Complex => write!(f, "Complex"),
703 ConversionDifficulty::Challenging => write!(f, "Challenging"),
704 }
705 }
706}
707
708impl std::fmt::Display for SupportLevel {
709 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
710 match self {
711 SupportLevel::Full => write!(f, "Full"),
712 SupportLevel::Partial => write!(f, "Partial"),
713 SupportLevel::None => write!(f, "None"),
714 SupportLevel::Deprecated => write!(f, "Deprecated"),
715 SupportLevel::New => write!(f, "New"),
716 }
717 }
718}
719
720impl std::fmt::Display for ImpactLevel {
721 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
722 match self {
723 ImpactLevel::Low => write!(f, "Low"),
724 ImpactLevel::Medium => write!(f, "Medium"),
725 ImpactLevel::High => write!(f, "High"),
726 ImpactLevel::Critical => write!(f, "Critical"),
727 }
728 }
729}
730
731pub mod utils {
733 use super::*;
734
735 pub fn supported_versions() -> Vec<DdexVersion> {
737 vec![DdexVersion::Ern382, DdexVersion::Ern42, DdexVersion::Ern43]
738 }
739
740 pub fn is_legacy_version(version: DdexVersion) -> bool {
742 matches!(version, DdexVersion::Ern382)
743 }
744
745 pub fn is_modern_version(version: DdexVersion) -> bool {
747 matches!(version, DdexVersion::Ern43)
748 }
749
750 pub fn get_version_release_date(version: DdexVersion) -> chrono::NaiveDate {
752 match version {
753 DdexVersion::Ern382 => chrono::NaiveDate::from_ymd_opt(2018, 5, 1).unwrap(),
754 DdexVersion::Ern42 => chrono::NaiveDate::from_ymd_opt(2020, 8, 15).unwrap(),
755 DdexVersion::Ern43 => chrono::NaiveDate::from_ymd_opt(2023, 3, 1).unwrap(),
756 DdexVersion::Ern41 => chrono::NaiveDate::from_ymd_opt(2019, 11, 15).unwrap(),
757 }
758 }
759
760 pub fn get_version_description(version: DdexVersion) -> String {
762 match version {
763 DdexVersion::Ern382 => "Legacy version with basic features".to_string(),
764 DdexVersion::Ern42 => "Intermediate version with enhanced features".to_string(),
765 DdexVersion::Ern43 => "Current version with full feature set".to_string(),
766 DdexVersion::Ern41 => "Early 4.x version".to_string(),
767 }
768 }
769}