use crate::error::BuildError;
use crate::presets::DdexVersion;
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
mod converter;
mod ern_382;
mod ern_42;
mod ern_43;
pub mod ern382 {
pub use super::ern_382::*;
}
pub mod ern42 {
pub use super::ern_42::*;
}
pub mod ern43 {
pub use super::ern_43::*;
}
pub use ern_43::{builders, get_version_spec, validation};
pub use ern_382::get_namespace_mappings as get_namespace_mappings_382;
pub use ern_42::get_namespace_mappings as get_namespace_mappings_42;
pub use ern_43::get_namespace_mappings as get_namespace_mappings_43;
pub use converter::{
ConversionReport as ConverterReport, ConversionResult as ConverterResult,
ConversionWarning as ConverterWarning, ConversionWarningType, VersionConverter,
};
pub use ern_382::get_xml_template as get_xml_template_382;
pub use ern_42::get_xml_template as get_xml_template_42;
pub use ern_43::get_xml_template as get_xml_template_43;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VersionSpec {
pub version: DdexVersion,
pub namespace: String,
pub schema_location: Option<String>,
pub message_schema_version_id: String,
pub supported_message_types: Vec<String>,
pub element_mappings: IndexMap<String, String>,
pub required_elements: Vec<String>,
pub deprecated_elements: Vec<String>,
pub new_elements: Vec<String>,
pub namespace_prefixes: IndexMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct ConversionResult {
pub converted_xml: String,
pub source_version: DdexVersion,
pub target_version: DdexVersion,
pub report: ConversionReport,
pub metadata: ConversionMetadata,
}
#[derive(Debug, Clone)]
pub struct ConversionReport {
pub conversions: Vec<ElementConversion>,
pub warnings: Vec<ConversionWarning>,
pub errors: Vec<ConversionError>,
pub unconvertible_elements: Vec<String>,
pub data_loss_warnings: Vec<String>,
pub compatibility_notes: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct ElementConversion {
pub source_path: String,
pub target_path: String,
pub conversion_type: ConversionType,
pub notes: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ConversionType {
DirectMapping,
Renamed {
old_name: String,
new_name: String,
},
Restructured {
description: String,
},
Added {
default_value: Option<String>,
},
Removed {
reason: String,
},
Moved {
old_path: String,
new_path: String,
},
Transformed {
description: String,
},
}
#[derive(Debug, Clone)]
pub struct ConversionWarning {
pub code: String,
pub message: String,
pub element_path: Option<String>,
pub suggestion: Option<String>,
pub impact: ImpactLevel,
}
#[derive(Debug, Clone)]
pub struct ConversionError {
pub code: String,
pub message: String,
pub element_path: String,
pub fallback: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ImpactLevel {
Low,
Medium,
High,
Critical,
}
#[derive(Debug, Clone)]
pub struct ConversionMetadata {
pub converted_at: chrono::DateTime<chrono::Utc>,
pub conversion_time: std::time::Duration,
pub elements_processed: usize,
pub warning_count: usize,
pub error_count: usize,
pub fidelity_percentage: f64,
}
#[derive(Debug, Clone)]
pub struct VersionDetection {
pub detected_version: DdexVersion,
pub confidence: f64,
pub clues: Vec<DetectionClue>,
pub ambiguities: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct DetectionClue {
pub clue_type: ClueType,
pub evidence: String,
pub weight: f64,
}
#[derive(Debug, Clone)]
pub enum ClueType {
Namespace,
SchemaLocation,
MessageSchemaVersionId,
VersionSpecificElement,
StructuralPattern,
NamespacePrefix,
}
#[derive(Debug, Clone)]
pub struct CompatibilityMatrix {
pub conversion_paths: Vec<ConversionPath>,
pub feature_compatibility: IndexMap<String, FeatureSupport>,
pub recommended_strategies: Vec<ConversionStrategy>,
}
#[derive(Debug, Clone)]
pub struct ConversionPath {
pub from: DdexVersion,
pub to: DdexVersion,
pub difficulty: ConversionDifficulty,
pub fidelity: f64,
pub major_changes: Vec<String>,
pub production_ready: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ConversionDifficulty {
Trivial,
Moderate,
Complex,
Challenging,
}
#[derive(Debug, Clone)]
pub struct FeatureSupport {
pub feature: String,
pub ern_382: SupportLevel,
pub ern_42: SupportLevel,
pub ern_43: SupportLevel,
pub migration_notes: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum SupportLevel {
Full,
Partial,
None,
Deprecated,
New,
}
#[derive(Debug, Clone)]
pub struct ConversionStrategy {
pub name: String,
pub description: String,
pub scenarios: Vec<String>,
pub steps: Vec<String>,
pub outcomes: Vec<String>,
pub risk_level: RiskLevel,
}
#[derive(Debug, Clone, PartialEq)]
pub enum RiskLevel {
Low,
Medium,
High,
VeryHigh,
}
#[derive(Debug, Clone)]
pub struct VersionManager {
version_specs: IndexMap<DdexVersion, VersionSpec>,
compatibility: CompatibilityMatrix,
_default_options: ConversionOptions,
}
#[derive(Debug, Clone)]
pub struct ConversionOptions {
pub allow_lossy: bool,
pub detailed_reports: bool,
pub preserve_unknown: bool,
pub add_metadata: bool,
pub preserve_comments: bool,
pub validation_level: ValidationLevel,
pub custom_mappings: IndexMap<String, String>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ValidationLevel {
None,
Basic,
Schema,
Full,
}
impl Default for ConversionOptions {
fn default() -> Self {
Self {
allow_lossy: false,
detailed_reports: true,
preserve_unknown: false,
add_metadata: true,
preserve_comments: false,
validation_level: ValidationLevel::Schema,
custom_mappings: IndexMap::new(),
}
}
}
impl VersionManager {
pub fn new() -> Self {
Self {
version_specs: Self::load_default_specs(),
compatibility: Self::build_compatibility_matrix(),
_default_options: ConversionOptions::default(),
}
}
pub fn get_version_spec(&self, version: DdexVersion) -> Option<&VersionSpec> {
self.version_specs.get(&version)
}
pub fn detect_version(&self, xml_content: &str) -> Result<VersionDetection, BuildError> {
let mut clues = Vec::new();
let mut version_scores = IndexMap::new();
for version in [DdexVersion::Ern382, DdexVersion::Ern42, DdexVersion::Ern43] {
version_scores.insert(version, 0.0);
}
if let Some(namespace) = self.extract_namespace(xml_content) {
clues.push(DetectionClue {
clue_type: ClueType::Namespace,
evidence: namespace.clone(),
weight: 0.8,
});
for (version, spec) in &self.version_specs {
if spec.namespace == namespace {
*version_scores.get_mut(version).unwrap() += 0.8;
}
}
}
if let Some(schema_version) = self.extract_message_schema_version(xml_content) {
clues.push(DetectionClue {
clue_type: ClueType::MessageSchemaVersionId,
evidence: schema_version.clone(),
weight: 0.9,
});
for (version, spec) in &self.version_specs {
if spec.message_schema_version_id == schema_version {
*version_scores.get_mut(version).unwrap() += 0.9;
}
}
}
for (version, spec) in &self.version_specs {
for element in &spec.new_elements {
if xml_content.contains(&format!("<{}", element)) {
clues.push(DetectionClue {
clue_type: ClueType::VersionSpecificElement,
evidence: element.clone(),
weight: 0.6,
});
*version_scores.get_mut(version).unwrap() += 0.6;
}
}
}
let (detected_version, confidence) = version_scores
.into_iter()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
.unwrap();
let normalized_confidence = (confidence / 2.5_f64).min(1.0_f64);
Ok(VersionDetection {
detected_version,
confidence: normalized_confidence,
clues,
ambiguities: Vec::new(), })
}
pub fn is_conversion_supported(&self, from: DdexVersion, to: DdexVersion) -> bool {
self.compatibility
.conversion_paths
.iter()
.any(|path| path.from == from && path.to == to)
}
pub fn get_conversion_path(
&self,
from: DdexVersion,
to: DdexVersion,
) -> Option<&ConversionPath> {
self.compatibility
.conversion_paths
.iter()
.find(|path| path.from == from && path.to == to)
}
pub fn get_feature_compatibility(&self, feature: &str) -> Option<&FeatureSupport> {
self.compatibility.feature_compatibility.get(feature)
}
pub fn get_recommended_strategy(
&self,
from: DdexVersion,
to: DdexVersion,
) -> Option<&ConversionStrategy> {
let scenario = format!("{:?} to {:?}", from, to);
self.compatibility
.recommended_strategies
.iter()
.find(|strategy| strategy.scenarios.contains(&scenario))
}
fn load_default_specs() -> IndexMap<DdexVersion, VersionSpec> {
let mut specs = IndexMap::new();
specs.insert(DdexVersion::Ern382, ern_382::get_version_spec());
specs.insert(DdexVersion::Ern42, ern_42::get_version_spec());
specs.insert(DdexVersion::Ern43, ern_43::get_version_spec());
specs
}
fn build_compatibility_matrix() -> CompatibilityMatrix {
let conversion_paths = vec![
ConversionPath {
from: DdexVersion::Ern382,
to: DdexVersion::Ern42,
difficulty: ConversionDifficulty::Moderate,
fidelity: 0.85,
major_changes: vec![
"Namespace migration".to_string(),
"Element structure updates".to_string(),
"New optional elements".to_string(),
],
production_ready: true,
},
ConversionPath {
from: DdexVersion::Ern42,
to: DdexVersion::Ern43,
difficulty: ConversionDifficulty::Trivial,
fidelity: 0.95,
major_changes: vec![
"Minor element additions".to_string(),
"Enhanced validation rules".to_string(),
],
production_ready: true,
},
ConversionPath {
from: DdexVersion::Ern382,
to: DdexVersion::Ern43,
difficulty: ConversionDifficulty::Complex,
fidelity: 0.80,
major_changes: vec![
"Major namespace changes".to_string(),
"Significant structural updates".to_string(),
"New required elements".to_string(),
],
production_ready: true,
},
ConversionPath {
from: DdexVersion::Ern43,
to: DdexVersion::Ern42,
difficulty: ConversionDifficulty::Moderate,
fidelity: 0.90,
major_changes: vec![
"Remove newer elements".to_string(),
"Downgrade validation rules".to_string(),
],
production_ready: true,
},
ConversionPath {
from: DdexVersion::Ern42,
to: DdexVersion::Ern382,
difficulty: ConversionDifficulty::Challenging,
fidelity: 0.75,
major_changes: vec![
"Legacy namespace mapping".to_string(),
"Remove modern elements".to_string(),
"Structural downgrade".to_string(),
],
production_ready: false,
},
ConversionPath {
from: DdexVersion::Ern43,
to: DdexVersion::Ern382,
difficulty: ConversionDifficulty::Challenging,
fidelity: 0.70,
major_changes: vec![
"Major structural downgrade".to_string(),
"Significant feature removal".to_string(),
"Legacy compatibility layer".to_string(),
],
production_ready: false,
},
];
let feature_compatibility = Self::build_feature_compatibility();
let recommended_strategies = Self::build_recommended_strategies();
CompatibilityMatrix {
conversion_paths,
feature_compatibility,
recommended_strategies,
}
}
fn build_feature_compatibility() -> IndexMap<String, FeatureSupport> {
let mut features = IndexMap::new();
features.insert(
"ResourceReference".to_string(),
FeatureSupport {
feature: "Resource Reference Elements".to_string(),
ern_382: SupportLevel::Partial,
ern_42: SupportLevel::Full,
ern_43: SupportLevel::Full,
migration_notes: Some("Enhanced in 4.2 with better linking".to_string()),
},
);
features.insert(
"DetailedDealTerms".to_string(),
FeatureSupport {
feature: "Detailed Deal Terms".to_string(),
ern_382: SupportLevel::None,
ern_42: SupportLevel::Partial,
ern_43: SupportLevel::Full,
migration_notes: Some("New detailed terms structure in 4.2+".to_string()),
},
);
features.insert(
"EnhancedMetadata".to_string(),
FeatureSupport {
feature: "Enhanced Metadata Fields".to_string(),
ern_382: SupportLevel::None,
ern_42: SupportLevel::None,
ern_43: SupportLevel::New,
migration_notes: Some("Completely new in 4.3".to_string()),
},
);
features.insert(
"DeprecatedElements".to_string(),
FeatureSupport {
feature: "Legacy Deprecated Elements".to_string(),
ern_382: SupportLevel::Full,
ern_42: SupportLevel::Deprecated,
ern_43: SupportLevel::None,
migration_notes: Some("Removed in 4.3, use modern equivalents".to_string()),
},
);
features
}
fn build_recommended_strategies() -> Vec<ConversionStrategy> {
vec![
ConversionStrategy {
name: "Conservative Upgrade".to_string(),
description: "Step-by-step version upgrade with validation".to_string(),
scenarios: vec!["Ern382 to Ern43".to_string()],
steps: vec![
"Validate source ERN 3.8.2 message".to_string(),
"Convert 3.8.2 → 4.2 with warnings".to_string(),
"Validate intermediate 4.2 message".to_string(),
"Convert 4.2 → 4.3 with enhancements".to_string(),
"Final validation and report".to_string(),
],
outcomes: vec![
"High-fidelity conversion".to_string(),
"Detailed conversion report".to_string(),
"Step-by-step validation".to_string(),
],
risk_level: RiskLevel::Low,
},
ConversionStrategy {
name: "Direct Upgrade".to_string(),
description: "Direct conversion between versions".to_string(),
scenarios: vec!["Ern42 to Ern43".to_string()],
steps: vec![
"Validate source message".to_string(),
"Apply direct conversion mappings".to_string(),
"Add new optional elements".to_string(),
"Validate target message".to_string(),
],
outcomes: vec![
"Fast conversion".to_string(),
"Minimal data transformation".to_string(),
],
risk_level: RiskLevel::Low,
},
]
}
fn extract_namespace(&self, xml_content: &str) -> Option<String> {
let re = regex::Regex::new(r#"xmlns="([^"]+)""#).ok()?;
re.captures(xml_content)?
.get(1)
.map(|m| m.as_str().to_string())
}
fn extract_message_schema_version(&self, xml_content: &str) -> Option<String> {
let re = regex::Regex::new(r#"MessageSchemaVersionId="([^"]+)""#).ok()?;
re.captures(xml_content)?
.get(1)
.map(|m| m.as_str().to_string())
}
}
impl Default for VersionManager {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for ConversionDifficulty {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConversionDifficulty::Trivial => write!(f, "Trivial"),
ConversionDifficulty::Moderate => write!(f, "Moderate"),
ConversionDifficulty::Complex => write!(f, "Complex"),
ConversionDifficulty::Challenging => write!(f, "Challenging"),
}
}
}
impl std::fmt::Display for SupportLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SupportLevel::Full => write!(f, "Full"),
SupportLevel::Partial => write!(f, "Partial"),
SupportLevel::None => write!(f, "None"),
SupportLevel::Deprecated => write!(f, "Deprecated"),
SupportLevel::New => write!(f, "New"),
}
}
}
impl std::fmt::Display for ImpactLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ImpactLevel::Low => write!(f, "Low"),
ImpactLevel::Medium => write!(f, "Medium"),
ImpactLevel::High => write!(f, "High"),
ImpactLevel::Critical => write!(f, "Critical"),
}
}
}
pub mod utils {
use super::*;
pub fn supported_versions() -> Vec<DdexVersion> {
vec![DdexVersion::Ern382, DdexVersion::Ern42, DdexVersion::Ern43]
}
pub fn is_legacy_version(version: DdexVersion) -> bool {
matches!(version, DdexVersion::Ern382)
}
pub fn is_modern_version(version: DdexVersion) -> bool {
matches!(version, DdexVersion::Ern43)
}
pub fn get_version_release_date(version: DdexVersion) -> chrono::NaiveDate {
match version {
DdexVersion::Ern382 => chrono::NaiveDate::from_ymd_opt(2018, 5, 1).unwrap(),
DdexVersion::Ern42 => chrono::NaiveDate::from_ymd_opt(2020, 8, 15).unwrap(),
DdexVersion::Ern43 => chrono::NaiveDate::from_ymd_opt(2023, 3, 1).unwrap(),
DdexVersion::Ern41 => chrono::NaiveDate::from_ymd_opt(2019, 11, 15).unwrap(),
}
}
pub fn get_version_description(version: DdexVersion) -> String {
match version {
DdexVersion::Ern382 => "Legacy version with basic features".to_string(),
DdexVersion::Ern42 => "Intermediate version with enhanced features".to_string(),
DdexVersion::Ern43 => "Current version with full feature set".to_string(),
DdexVersion::Ern41 => "Early 4.x version".to_string(),
}
}
}