1use crate::error::BuildError;
28use crate::presets::DdexVersion;
29use indexmap::IndexMap;
30use serde::{Deserialize, Serialize};
31
32mod converter;
33mod ern_382;
34mod ern_42;
35mod ern_43;
36
37pub mod ern382 {
40 pub use super::ern_382::*;
41}
42
43pub mod ern42 {
45 pub use super::ern_42::*;
46}
47
48pub mod ern43 {
50 pub use super::ern_43::*;
51}
52
53pub use ern_43::{builders, get_version_spec, validation};
55
56pub use ern_382::get_namespace_mappings as get_namespace_mappings_382;
58pub use ern_42::get_namespace_mappings as get_namespace_mappings_42;
59pub use ern_43::get_namespace_mappings as get_namespace_mappings_43;
60
61pub use converter::{
62 ConversionReport as ConverterReport, ConversionResult as ConverterResult,
63 ConversionWarning as ConverterWarning, ConversionWarningType, VersionConverter,
64};
65pub use ern_382::get_xml_template as get_xml_template_382;
66pub use ern_42::get_xml_template as get_xml_template_42;
67pub use ern_43::get_xml_template as get_xml_template_43;
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct VersionSpec {
72 pub version: DdexVersion,
74 pub namespace: String,
76 pub schema_location: Option<String>,
78 pub message_schema_version_id: String,
80 pub supported_message_types: Vec<String>,
82 pub element_mappings: IndexMap<String, String>,
84 pub required_elements: Vec<String>,
86 pub deprecated_elements: Vec<String>,
88 pub new_elements: Vec<String>,
90 pub namespace_prefixes: IndexMap<String, String>,
92}
93
94#[derive(Debug, Clone)]
96pub struct ConversionResult {
97 pub converted_xml: String,
99 pub source_version: DdexVersion,
101 pub target_version: DdexVersion,
103 pub report: ConversionReport,
105 pub metadata: ConversionMetadata,
107}
108
109#[derive(Debug, Clone)]
111pub struct ConversionReport {
112 pub conversions: Vec<ElementConversion>,
114 pub warnings: Vec<ConversionWarning>,
116 pub errors: Vec<ConversionError>,
118 pub unconvertible_elements: Vec<String>,
120 pub data_loss_warnings: Vec<String>,
122 pub compatibility_notes: Vec<String>,
124}
125
126#[derive(Debug, Clone)]
128pub struct ElementConversion {
129 pub source_path: String,
131 pub target_path: String,
133 pub conversion_type: ConversionType,
135 pub notes: Option<String>,
137}
138
139#[derive(Debug, Clone, PartialEq)]
141pub enum ConversionType {
142 DirectMapping,
144 Renamed {
146 old_name: String,
148 new_name: String,
150 },
151 Restructured {
153 description: String,
155 },
156 Added {
158 default_value: Option<String>,
160 },
161 Removed {
163 reason: String,
165 },
166 Moved {
168 old_path: String,
170 new_path: String,
172 },
173 Transformed {
175 description: String,
177 },
178}
179
180#[derive(Debug, Clone)]
182pub struct ConversionWarning {
183 pub code: String,
185 pub message: String,
187 pub element_path: Option<String>,
189 pub suggestion: Option<String>,
191 pub impact: ImpactLevel,
193}
194
195#[derive(Debug, Clone)]
197pub struct ConversionError {
198 pub code: String,
200 pub message: String,
202 pub element_path: String,
204 pub fallback: Option<String>,
206}
207
208#[derive(Debug, Clone, PartialEq)]
210pub enum ImpactLevel {
211 Low,
213 Medium,
215 High,
217 Critical,
219}
220
221#[derive(Debug, Clone)]
223pub struct ConversionMetadata {
224 pub converted_at: chrono::DateTime<chrono::Utc>,
226 pub conversion_time: std::time::Duration,
228 pub elements_processed: usize,
230 pub warning_count: usize,
232 pub error_count: usize,
234 pub fidelity_percentage: f64,
236}
237
238#[derive(Debug, Clone)]
240pub struct VersionDetection {
241 pub detected_version: DdexVersion,
243 pub confidence: f64,
245 pub clues: Vec<DetectionClue>,
247 pub ambiguities: Vec<String>,
249}
250
251#[derive(Debug, Clone)]
253pub struct DetectionClue {
254 pub clue_type: ClueType,
256 pub evidence: String,
258 pub weight: f64,
260}
261
262#[derive(Debug, Clone)]
264pub enum ClueType {
265 Namespace,
267 SchemaLocation,
269 MessageSchemaVersionId,
271 VersionSpecificElement,
273 StructuralPattern,
275 NamespacePrefix,
277}
278
279#[derive(Debug, Clone)]
281pub struct CompatibilityMatrix {
282 pub conversion_paths: Vec<ConversionPath>,
284 pub feature_compatibility: IndexMap<String, FeatureSupport>,
286 pub recommended_strategies: Vec<ConversionStrategy>,
288}
289
290#[derive(Debug, Clone)]
292pub struct ConversionPath {
293 pub from: DdexVersion,
295 pub to: DdexVersion,
297 pub difficulty: ConversionDifficulty,
299 pub fidelity: f64,
301 pub major_changes: Vec<String>,
303 pub production_ready: bool,
305}
306
307#[derive(Debug, Clone, PartialEq)]
309pub enum ConversionDifficulty {
310 Trivial,
312 Moderate,
314 Complex,
316 Challenging,
318}
319
320#[derive(Debug, Clone)]
322pub struct FeatureSupport {
323 pub feature: String,
325 pub ern_382: SupportLevel,
327 pub ern_42: SupportLevel,
329 pub ern_43: SupportLevel,
331 pub migration_notes: Option<String>,
333}
334
335#[derive(Debug, Clone, PartialEq)]
337pub enum SupportLevel {
338 Full,
340 Partial,
342 None,
344 Deprecated,
346 New,
348}
349
350#[derive(Debug, Clone)]
352pub struct ConversionStrategy {
353 pub name: String,
355 pub description: String,
357 pub scenarios: Vec<String>,
359 pub steps: Vec<String>,
361 pub outcomes: Vec<String>,
363 pub risk_level: RiskLevel,
365}
366
367#[derive(Debug, Clone, PartialEq)]
369pub enum RiskLevel {
370 Low,
372 Medium,
374 High,
376 VeryHigh,
378}
379
380#[derive(Debug, Clone)]
382pub struct VersionManager {
383 version_specs: IndexMap<DdexVersion, VersionSpec>,
385 compatibility: CompatibilityMatrix,
387 _default_options: ConversionOptions,
389}
390
391#[derive(Debug, Clone)]
393pub struct ConversionOptions {
394 pub allow_lossy: bool,
396 pub detailed_reports: bool,
398 pub preserve_unknown: bool,
400 pub add_metadata: bool,
402 pub preserve_comments: bool,
404 pub validation_level: ValidationLevel,
406 pub custom_mappings: IndexMap<String, String>,
408}
409
410#[derive(Debug, Clone, PartialEq)]
412pub enum ValidationLevel {
413 None,
415 Basic,
417 Schema,
419 Full,
421}
422
423impl Default for ConversionOptions {
424 fn default() -> Self {
425 Self {
426 allow_lossy: false,
427 detailed_reports: true,
428 preserve_unknown: false,
429 add_metadata: true,
430 preserve_comments: false,
431 validation_level: ValidationLevel::Schema,
432 custom_mappings: IndexMap::new(),
433 }
434 }
435}
436
437impl VersionManager {
438 pub fn new() -> Self {
440 Self {
441 version_specs: Self::load_default_specs(),
442 compatibility: Self::build_compatibility_matrix(),
443 _default_options: ConversionOptions::default(),
444 }
445 }
446
447 pub fn get_version_spec(&self, version: DdexVersion) -> Option<&VersionSpec> {
449 self.version_specs.get(&version)
450 }
451
452 pub fn detect_version(&self, xml_content: &str) -> Result<VersionDetection, BuildError> {
454 let mut clues = Vec::new();
455 let mut version_scores = IndexMap::new();
456
457 for version in [DdexVersion::Ern382, DdexVersion::Ern42, DdexVersion::Ern43] {
459 version_scores.insert(version, 0.0);
460 }
461
462 if let Some(namespace) = self.extract_namespace(xml_content) {
464 clues.push(DetectionClue {
465 clue_type: ClueType::Namespace,
466 evidence: namespace.clone(),
467 weight: 0.8,
468 });
469
470 for (version, spec) in &self.version_specs {
472 if spec.namespace == namespace {
473 *version_scores.get_mut(version).unwrap() += 0.8;
474 }
475 }
476 }
477
478 if let Some(schema_version) = self.extract_message_schema_version(xml_content) {
480 clues.push(DetectionClue {
481 clue_type: ClueType::MessageSchemaVersionId,
482 evidence: schema_version.clone(),
483 weight: 0.9,
484 });
485
486 for (version, spec) in &self.version_specs {
488 if spec.message_schema_version_id == schema_version {
489 *version_scores.get_mut(version).unwrap() += 0.9;
490 }
491 }
492 }
493
494 for (version, spec) in &self.version_specs {
496 for element in &spec.new_elements {
497 if xml_content.contains(&format!("<{}", element)) {
498 clues.push(DetectionClue {
499 clue_type: ClueType::VersionSpecificElement,
500 evidence: element.clone(),
501 weight: 0.6,
502 });
503 *version_scores.get_mut(version).unwrap() += 0.6;
504 }
505 }
506 }
507
508 let (detected_version, confidence) = version_scores
510 .into_iter()
511 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
512 .unwrap();
513
514 let normalized_confidence = (confidence / 2.5_f64).min(1.0_f64); Ok(VersionDetection {
517 detected_version,
518 confidence: normalized_confidence,
519 clues,
520 ambiguities: Vec::new(), })
522 }
523
524 pub fn is_conversion_supported(&self, from: DdexVersion, to: DdexVersion) -> bool {
526 self.compatibility
527 .conversion_paths
528 .iter()
529 .any(|path| path.from == from && path.to == to)
530 }
531
532 pub fn get_conversion_path(
534 &self,
535 from: DdexVersion,
536 to: DdexVersion,
537 ) -> Option<&ConversionPath> {
538 self.compatibility
539 .conversion_paths
540 .iter()
541 .find(|path| path.from == from && path.to == to)
542 }
543
544 pub fn get_feature_compatibility(&self, feature: &str) -> Option<&FeatureSupport> {
546 self.compatibility.feature_compatibility.get(feature)
547 }
548
549 pub fn get_recommended_strategy(
551 &self,
552 from: DdexVersion,
553 to: DdexVersion,
554 ) -> Option<&ConversionStrategy> {
555 let scenario = format!("{:?} to {:?}", from, to);
556 self.compatibility
557 .recommended_strategies
558 .iter()
559 .find(|strategy| strategy.scenarios.contains(&scenario))
560 }
561
562 fn load_default_specs() -> IndexMap<DdexVersion, VersionSpec> {
565 let mut specs = IndexMap::new();
566
567 specs.insert(DdexVersion::Ern382, ern_382::get_version_spec());
568 specs.insert(DdexVersion::Ern42, ern_42::get_version_spec());
569 specs.insert(DdexVersion::Ern43, ern_43::get_version_spec());
570
571 specs
572 }
573
574 fn build_compatibility_matrix() -> CompatibilityMatrix {
575 let conversion_paths = vec![
576 ConversionPath {
578 from: DdexVersion::Ern382,
579 to: DdexVersion::Ern42,
580 difficulty: ConversionDifficulty::Moderate,
581 fidelity: 0.85,
582 major_changes: vec![
583 "Namespace migration".to_string(),
584 "Element structure updates".to_string(),
585 "New optional elements".to_string(),
586 ],
587 production_ready: true,
588 },
589 ConversionPath {
590 from: DdexVersion::Ern42,
591 to: DdexVersion::Ern43,
592 difficulty: ConversionDifficulty::Trivial,
593 fidelity: 0.95,
594 major_changes: vec![
595 "Minor element additions".to_string(),
596 "Enhanced validation rules".to_string(),
597 ],
598 production_ready: true,
599 },
600 ConversionPath {
601 from: DdexVersion::Ern382,
602 to: DdexVersion::Ern43,
603 difficulty: ConversionDifficulty::Complex,
604 fidelity: 0.80,
605 major_changes: vec![
606 "Major namespace changes".to_string(),
607 "Significant structural updates".to_string(),
608 "New required elements".to_string(),
609 ],
610 production_ready: true,
611 },
612 ConversionPath {
614 from: DdexVersion::Ern43,
615 to: DdexVersion::Ern42,
616 difficulty: ConversionDifficulty::Moderate,
617 fidelity: 0.90,
618 major_changes: vec![
619 "Remove newer elements".to_string(),
620 "Downgrade validation rules".to_string(),
621 ],
622 production_ready: true,
623 },
624 ConversionPath {
625 from: DdexVersion::Ern42,
626 to: DdexVersion::Ern382,
627 difficulty: ConversionDifficulty::Challenging,
628 fidelity: 0.75,
629 major_changes: vec![
630 "Legacy namespace mapping".to_string(),
631 "Remove modern elements".to_string(),
632 "Structural downgrade".to_string(),
633 ],
634 production_ready: false,
635 },
636 ConversionPath {
637 from: DdexVersion::Ern43,
638 to: DdexVersion::Ern382,
639 difficulty: ConversionDifficulty::Challenging,
640 fidelity: 0.70,
641 major_changes: vec![
642 "Major structural downgrade".to_string(),
643 "Significant feature removal".to_string(),
644 "Legacy compatibility layer".to_string(),
645 ],
646 production_ready: false,
647 },
648 ];
649
650 let feature_compatibility = Self::build_feature_compatibility();
651 let recommended_strategies = Self::build_recommended_strategies();
652
653 CompatibilityMatrix {
654 conversion_paths,
655 feature_compatibility,
656 recommended_strategies,
657 }
658 }
659
660 fn build_feature_compatibility() -> IndexMap<String, FeatureSupport> {
661 let mut features = IndexMap::new();
662
663 features.insert(
664 "ResourceReference".to_string(),
665 FeatureSupport {
666 feature: "Resource Reference Elements".to_string(),
667 ern_382: SupportLevel::Partial,
668 ern_42: SupportLevel::Full,
669 ern_43: SupportLevel::Full,
670 migration_notes: Some("Enhanced in 4.2 with better linking".to_string()),
671 },
672 );
673
674 features.insert(
675 "DetailedDealTerms".to_string(),
676 FeatureSupport {
677 feature: "Detailed Deal Terms".to_string(),
678 ern_382: SupportLevel::None,
679 ern_42: SupportLevel::Partial,
680 ern_43: SupportLevel::Full,
681 migration_notes: Some("New detailed terms structure in 4.2+".to_string()),
682 },
683 );
684
685 features.insert(
686 "EnhancedMetadata".to_string(),
687 FeatureSupport {
688 feature: "Enhanced Metadata Fields".to_string(),
689 ern_382: SupportLevel::None,
690 ern_42: SupportLevel::None,
691 ern_43: SupportLevel::New,
692 migration_notes: Some("Completely new in 4.3".to_string()),
693 },
694 );
695
696 features.insert(
697 "DeprecatedElements".to_string(),
698 FeatureSupport {
699 feature: "Legacy Deprecated Elements".to_string(),
700 ern_382: SupportLevel::Full,
701 ern_42: SupportLevel::Deprecated,
702 ern_43: SupportLevel::None,
703 migration_notes: Some("Removed in 4.3, use modern equivalents".to_string()),
704 },
705 );
706
707 features
708 }
709
710 fn build_recommended_strategies() -> Vec<ConversionStrategy> {
711 vec![
712 ConversionStrategy {
713 name: "Conservative Upgrade".to_string(),
714 description: "Step-by-step version upgrade with validation".to_string(),
715 scenarios: vec!["Ern382 to Ern43".to_string()],
716 steps: vec![
717 "Validate source ERN 3.8.2 message".to_string(),
718 "Convert 3.8.2 → 4.2 with warnings".to_string(),
719 "Validate intermediate 4.2 message".to_string(),
720 "Convert 4.2 → 4.3 with enhancements".to_string(),
721 "Final validation and report".to_string(),
722 ],
723 outcomes: vec![
724 "High-fidelity conversion".to_string(),
725 "Detailed conversion report".to_string(),
726 "Step-by-step validation".to_string(),
727 ],
728 risk_level: RiskLevel::Low,
729 },
730 ConversionStrategy {
731 name: "Direct Upgrade".to_string(),
732 description: "Direct conversion between versions".to_string(),
733 scenarios: vec!["Ern42 to Ern43".to_string()],
734 steps: vec![
735 "Validate source message".to_string(),
736 "Apply direct conversion mappings".to_string(),
737 "Add new optional elements".to_string(),
738 "Validate target message".to_string(),
739 ],
740 outcomes: vec![
741 "Fast conversion".to_string(),
742 "Minimal data transformation".to_string(),
743 ],
744 risk_level: RiskLevel::Low,
745 },
746 ]
747 }
748
749 fn extract_namespace(&self, xml_content: &str) -> Option<String> {
750 let re = regex::Regex::new(r#"xmlns="([^"]+)""#).ok()?;
752 re.captures(xml_content)?
753 .get(1)
754 .map(|m| m.as_str().to_string())
755 }
756
757 fn extract_message_schema_version(&self, xml_content: &str) -> Option<String> {
758 let re = regex::Regex::new(r#"MessageSchemaVersionId="([^"]+)""#).ok()?;
759 re.captures(xml_content)?
760 .get(1)
761 .map(|m| m.as_str().to_string())
762 }
763}
764
765impl Default for VersionManager {
766 fn default() -> Self {
767 Self::new()
768 }
769}
770
771impl std::fmt::Display for ConversionDifficulty {
772 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
773 match self {
774 ConversionDifficulty::Trivial => write!(f, "Trivial"),
775 ConversionDifficulty::Moderate => write!(f, "Moderate"),
776 ConversionDifficulty::Complex => write!(f, "Complex"),
777 ConversionDifficulty::Challenging => write!(f, "Challenging"),
778 }
779 }
780}
781
782impl std::fmt::Display for SupportLevel {
783 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
784 match self {
785 SupportLevel::Full => write!(f, "Full"),
786 SupportLevel::Partial => write!(f, "Partial"),
787 SupportLevel::None => write!(f, "None"),
788 SupportLevel::Deprecated => write!(f, "Deprecated"),
789 SupportLevel::New => write!(f, "New"),
790 }
791 }
792}
793
794impl std::fmt::Display for ImpactLevel {
795 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
796 match self {
797 ImpactLevel::Low => write!(f, "Low"),
798 ImpactLevel::Medium => write!(f, "Medium"),
799 ImpactLevel::High => write!(f, "High"),
800 ImpactLevel::Critical => write!(f, "Critical"),
801 }
802 }
803}
804
805pub mod utils {
807 use super::*;
808
809 pub fn supported_versions() -> Vec<DdexVersion> {
811 vec![DdexVersion::Ern382, DdexVersion::Ern42, DdexVersion::Ern43]
812 }
813
814 pub fn is_legacy_version(version: DdexVersion) -> bool {
816 matches!(version, DdexVersion::Ern382)
817 }
818
819 pub fn is_modern_version(version: DdexVersion) -> bool {
821 matches!(version, DdexVersion::Ern43)
822 }
823
824 pub fn get_version_release_date(version: DdexVersion) -> chrono::NaiveDate {
826 match version {
827 DdexVersion::Ern382 => chrono::NaiveDate::from_ymd_opt(2018, 5, 1).unwrap(),
828 DdexVersion::Ern42 => chrono::NaiveDate::from_ymd_opt(2020, 8, 15).unwrap(),
829 DdexVersion::Ern43 => chrono::NaiveDate::from_ymd_opt(2023, 3, 1).unwrap(),
830 DdexVersion::Ern41 => chrono::NaiveDate::from_ymd_opt(2019, 11, 15).unwrap(),
831 }
832 }
833
834 pub fn get_version_description(version: DdexVersion) -> String {
836 match version {
837 DdexVersion::Ern382 => "Legacy version with basic features".to_string(),
838 DdexVersion::Ern42 => "Intermediate version with enhanced features".to_string(),
839 DdexVersion::Ern43 => "Current version with full feature set".to_string(),
840 DdexVersion::Ern41 => "Early 4.x version".to_string(),
841 }
842 }
843}