use crate::builder::MessageHeaderRequest;
use crate::diff::types::{ChangeSet, ChangeType, SemanticChange};
use crate::diff::DiffEngine;
use crate::error::BuildError;
use chrono::{DateTime, Utc};
use indexmap::{IndexMap, IndexSet};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateReleaseMessage {
pub header: MessageHeaderRequest,
pub update_list: Vec<UpdateOperation>,
pub resource_updates: IndexMap<String, ResourceUpdate>,
pub release_updates: IndexMap<String, ReleaseUpdate>,
pub deal_updates: IndexMap<String, DealUpdate>,
pub update_metadata: UpdateMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateOperation {
pub operation_id: String,
pub action: UpdateAction,
pub target_path: String,
pub entity_type: EntityType,
pub entity_id: String,
pub old_value: Option<String>,
pub new_value: Option<String>,
pub is_critical: bool,
pub description: String,
pub dependencies: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum UpdateAction {
Add,
Delete,
Replace,
Move,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum EntityType {
Resource,
Release,
Deal,
Party,
Metadata,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceUpdate {
pub resource_id: String,
pub resource_reference: String,
pub action: UpdateAction,
pub resource_data: Option<ResourceData>,
pub technical_updates: Vec<TechnicalUpdate>,
pub metadata_updates: IndexMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReleaseUpdate {
pub release_id: String,
pub release_reference: String,
pub action: UpdateAction,
pub release_data: Option<ReleaseData>,
pub track_updates: Vec<TrackUpdate>,
pub resource_reference_updates: Vec<ReferenceUpdate>,
pub metadata_updates: IndexMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DealUpdate {
pub deal_id: String,
pub deal_reference: String,
pub action: UpdateAction,
pub deal_data: Option<DealData>,
pub terms_updates: Vec<TermsUpdate>,
}
#[derive(Debug, Clone)]
pub struct UpdatedResource {
pub resource_type: String,
pub title: String,
pub artist: String,
pub isrc: Option<String>,
pub duration: Option<String>,
pub file_path: Option<String>,
pub technical_details: Option<TechnicalDetails>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceData {
pub resource_type: String,
pub title: String,
pub artist: String,
pub isrc: Option<String>,
pub duration: Option<String>,
pub file_path: Option<String>,
pub technical_details: Option<TechnicalDetails>,
}
#[derive(Debug, Clone)]
pub struct UpdatedRelease {
pub release_type: String,
pub title: String,
pub artist: String,
pub label: Option<String>,
pub upc: Option<String>,
pub release_date: Option<String>,
pub genre: Option<String>,
pub resource_references: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReleaseData {
pub release_type: String,
pub title: String,
pub artist: String,
pub label: Option<String>,
pub upc: Option<String>,
pub release_date: Option<String>,
pub genre: Option<String>,
pub resource_references: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DealData {
pub commercial_model_type: String,
pub territory_codes: Vec<String>,
pub start_date: Option<String>,
pub end_date: Option<String>,
pub price: Option<PriceData>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TechnicalUpdate {
pub field_name: String,
pub old_value: Option<String>,
pub new_value: Option<String>,
pub update_action: UpdateAction,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrackUpdate {
pub track_id: String,
pub action: UpdateAction,
pub old_resource_reference: Option<String>,
pub new_resource_reference: Option<String>,
pub position_change: Option<PositionChange>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReferenceUpdate {
pub old_reference: String,
pub new_reference: String,
pub reference_type: String,
pub update_reason: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TermsUpdate {
pub field_name: String,
pub old_value: Option<String>,
pub new_value: Option<String>,
pub effective_date: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PositionChange {
pub old_position: usize,
pub new_position: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TechnicalDetails {
pub file_name: Option<String>,
pub codec_type: Option<String>,
pub bit_rate: Option<String>,
pub sample_rate: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PriceData {
pub amount: String,
pub currency_code: String,
pub price_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateMetadata {
pub original_message_id: String,
pub original_message_version: Option<String>,
pub original_message_timestamp: Option<DateTime<Utc>>,
pub update_created_timestamp: DateTime<Utc>,
pub update_sequence: u64,
pub total_operations: usize,
pub impact_level: String,
pub validation_status: ValidationStatus,
pub custom_metadata: IndexMap<String, String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ValidationStatus {
Validated,
WarningsOnly,
Invalid,
Pending,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateConfig {
pub include_non_critical: bool,
pub max_operations_per_update: usize,
pub validate_references: bool,
pub optimize_references: bool,
pub excluded_fields: IndexSet<String>,
pub update_priorities: IndexMap<String, u8>,
}
impl Default for UpdateConfig {
fn default() -> Self {
let mut excluded_fields = IndexSet::new();
excluded_fields.insert("MessageId".to_string());
excluded_fields.insert("MessageCreatedDateTime".to_string());
Self {
include_non_critical: true,
max_operations_per_update: 1000,
validate_references: true,
optimize_references: true,
excluded_fields,
update_priorities: IndexMap::new(),
}
}
}
pub struct UpdateGenerator {
config: UpdateConfig,
diff_engine: DiffEngine,
operation_counter: u64,
}
impl UpdateGenerator {
pub fn new() -> Self {
Self {
config: UpdateConfig::default(),
diff_engine: DiffEngine::new(),
operation_counter: 0,
}
}
pub fn new_with_config(config: UpdateConfig) -> Self {
Self {
config,
diff_engine: DiffEngine::new(),
operation_counter: 0,
}
}
pub fn create_update(
&mut self,
original_xml: &str,
updated_xml: &str,
original_message_id: &str,
) -> Result<UpdateReleaseMessage, BuildError> {
let original_ast = self.parse_xml_to_ast(original_xml)?;
let updated_ast = self.parse_xml_to_ast(updated_xml)?;
let changeset = self.diff_engine.diff(&original_ast, &updated_ast)?;
let update_operations = self.changeset_to_operations(&changeset)?;
let (resource_updates, release_updates, deal_updates) =
self.group_operations_by_entity(&update_operations)?;
let metadata =
self.create_update_metadata(original_message_id, &update_operations, &changeset);
let header = self.create_update_header(original_message_id, &metadata);
let update_message = UpdateReleaseMessage {
header,
update_list: update_operations,
resource_updates,
release_updates,
deal_updates,
update_metadata: metadata,
};
self.validate_update(&update_message)?;
Ok(update_message)
}
pub fn apply_update(
&self,
base_xml: &str,
update: &UpdateReleaseMessage,
) -> Result<String, BuildError> {
let mut base_ast = self.parse_xml_to_ast(base_xml)?;
let ordered_operations = self.order_operations_by_dependencies(&update.update_list)?;
for operation in &ordered_operations {
self.apply_operation_to_ast(&mut base_ast, operation)?;
}
self.apply_resource_updates(&mut base_ast, &update.resource_updates)?;
self.apply_release_updates(&mut base_ast, &update.release_updates)?;
self.apply_deal_updates(&mut base_ast, &update.deal_updates)?;
self.ast_to_xml(&base_ast)
}
pub fn validate_update(
&self,
update: &UpdateReleaseMessage,
) -> Result<ValidationStatus, BuildError> {
let mut errors = Vec::new();
let mut warnings = Vec::new();
for operation in &update.update_list {
if let Err(e) = self.validate_operation(operation, update) {
errors.push(format!("Operation {}: {}", operation.operation_id, e));
}
}
if self.config.validate_references {
if let Err(e) = self.validate_references(update) {
errors.push(format!("Reference validation: {}", e));
}
}
if let Err(e) = self.validate_dependencies(&update.update_list) {
errors.push(format!("Dependency validation: {}", e));
}
let conflicts = self.detect_conflicts(&update.update_list)?;
if !conflicts.is_empty() {
warnings.push(format!("Found {} potential conflicts", conflicts.len()));
}
if !errors.is_empty() {
Err(BuildError::ValidationFailed { errors })
} else if !warnings.is_empty() {
Ok(ValidationStatus::WarningsOnly)
} else {
Ok(ValidationStatus::Validated)
}
}
fn parse_xml_to_ast(&self, xml: &str) -> Result<crate::ast::AST, BuildError> {
let root = crate::ast::Element::new("NewReleaseMessage").with_text(xml);
Ok(crate::ast::AST {
root,
namespaces: IndexMap::new(),
schema_location: None,
})
}
fn changeset_to_operations(
&mut self,
changeset: &ChangeSet,
) -> Result<Vec<UpdateOperation>, BuildError> {
let mut operations = Vec::new();
for change in &changeset.changes {
let operation = self.semantic_change_to_operation(change)?;
operations.push(operation);
}
Ok(operations)
}
fn semantic_change_to_operation(
&mut self,
change: &SemanticChange,
) -> Result<UpdateOperation, BuildError> {
self.operation_counter += 1;
let action = match change.change_type {
ChangeType::ElementAdded | ChangeType::AttributeAdded => UpdateAction::Add,
ChangeType::ElementRemoved | ChangeType::AttributeRemoved => UpdateAction::Delete,
ChangeType::ElementMoved => UpdateAction::Move,
_ => UpdateAction::Replace,
};
let entity_type = self.determine_entity_type(&change.path);
let entity_id = self.extract_entity_id(&change.path)?;
Ok(UpdateOperation {
operation_id: format!("OP{:06}", self.operation_counter),
action,
target_path: change.path.to_string(),
entity_type,
entity_id,
old_value: change.old_value.clone(),
new_value: change.new_value.clone(),
is_critical: change.is_critical,
description: change.description.clone(),
dependencies: Vec::new(), })
}
fn determine_entity_type(&self, path: &crate::diff::types::DiffPath) -> EntityType {
let path_str = path.to_string().to_lowercase();
if path_str.contains("resource") {
EntityType::Resource
} else if path_str.contains("release") {
EntityType::Release
} else if path_str.contains("deal") {
EntityType::Deal
} else if path_str.contains("party") {
EntityType::Party
} else {
EntityType::Metadata
}
}
fn extract_entity_id(&self, path: &crate::diff::types::DiffPath) -> Result<String, BuildError> {
let path_str = path.to_string();
if let Some(id_start) = path_str.find("Id=") {
let id_part = &path_str[id_start + 3..];
if let Some(id_end) = id_part.find(&[']', '/', '@'][..]) {
Ok(id_part[..id_end].to_string())
} else {
Ok(id_part.to_string())
}
} else {
let uuid_str = uuid::Uuid::new_v4().to_string();
Ok(format!("unknown_{}", &uuid_str[..8]))
}
}
fn group_operations_by_entity(
&self,
operations: &[UpdateOperation],
) -> Result<
(
IndexMap<String, ResourceUpdate>,
IndexMap<String, ReleaseUpdate>,
IndexMap<String, DealUpdate>,
),
BuildError,
> {
let mut resource_updates = IndexMap::new();
let mut release_updates = IndexMap::new();
let mut deal_updates = IndexMap::new();
for operation in operations {
match operation.entity_type {
EntityType::Resource => {
let resource_update = self.operation_to_resource_update(operation)?;
resource_updates.insert(operation.entity_id.clone(), resource_update);
}
EntityType::Release => {
let release_update = self.operation_to_release_update(operation)?;
release_updates.insert(operation.entity_id.clone(), release_update);
}
EntityType::Deal => {
let deal_update = self.operation_to_deal_update(operation)?;
deal_updates.insert(operation.entity_id.clone(), deal_update);
}
_ => {} }
}
Ok((resource_updates, release_updates, deal_updates))
}
fn operation_to_resource_update(
&self,
operation: &UpdateOperation,
) -> Result<ResourceUpdate, BuildError> {
Ok(ResourceUpdate {
resource_id: operation.entity_id.clone(),
resource_reference: format!(
"R{:06}",
operation.operation_id[2..].parse::<u32>().unwrap_or(0)
),
action: operation.action,
resource_data: None, technical_updates: Vec::new(),
metadata_updates: IndexMap::new(),
})
}
fn operation_to_release_update(
&self,
operation: &UpdateOperation,
) -> Result<ReleaseUpdate, BuildError> {
Ok(ReleaseUpdate {
release_id: operation.entity_id.clone(),
release_reference: format!(
"REL{:06}",
operation.operation_id[2..].parse::<u32>().unwrap_or(0)
),
action: operation.action,
release_data: None, track_updates: Vec::new(),
resource_reference_updates: Vec::new(),
metadata_updates: IndexMap::new(),
})
}
fn operation_to_deal_update(
&self,
operation: &UpdateOperation,
) -> Result<DealUpdate, BuildError> {
Ok(DealUpdate {
deal_id: operation.entity_id.clone(),
deal_reference: format!(
"D{:06}",
operation.operation_id[2..].parse::<u32>().unwrap_or(0)
),
action: operation.action,
deal_data: None, terms_updates: Vec::new(),
})
}
fn create_update_metadata(
&self,
original_message_id: &str,
operations: &[UpdateOperation],
changeset: &ChangeSet,
) -> UpdateMetadata {
UpdateMetadata {
original_message_id: original_message_id.to_string(),
original_message_version: None,
original_message_timestamp: None,
update_created_timestamp: Utc::now(),
update_sequence: 1,
total_operations: operations.len(),
impact_level: changeset.impact_level().to_string(),
validation_status: ValidationStatus::Pending,
custom_metadata: IndexMap::new(),
}
}
fn create_update_header(
&self,
original_message_id: &str,
metadata: &UpdateMetadata,
) -> MessageHeaderRequest {
MessageHeaderRequest {
message_id: Some(format!(
"UPD-{}-{:04}",
original_message_id, metadata.update_sequence
)),
message_sender: crate::builder::PartyRequest {
party_name: vec![crate::builder::LocalizedStringRequest {
text: "DDEX Builder Update Engine".to_string(),
language_code: None,
}],
party_id: None,
party_reference: None,
},
message_recipient: crate::builder::PartyRequest {
party_name: vec![crate::builder::LocalizedStringRequest {
text: "Update Recipient".to_string(),
language_code: None,
}],
party_id: None,
party_reference: None,
},
message_control_type: Some("UpdateMessage".to_string()),
message_created_date_time: Some(metadata.update_created_timestamp.to_rfc3339()),
}
}
fn order_operations_by_dependencies(
&self,
operations: &[UpdateOperation],
) -> Result<Vec<UpdateOperation>, BuildError> {
let mut ordered = operations.to_vec();
ordered.sort_by(|a, b| a.operation_id.cmp(&b.operation_id));
Ok(ordered)
}
fn apply_operation_to_ast(
&self,
_ast: &mut crate::ast::AST,
operation: &UpdateOperation,
) -> Result<(), BuildError> {
match operation.action {
UpdateAction::Add => {
}
UpdateAction::Delete => {
}
UpdateAction::Replace => {
}
UpdateAction::Move => {
}
}
Ok(())
}
fn apply_resource_updates(
&self,
_ast: &mut crate::ast::AST,
updates: &IndexMap<String, ResourceUpdate>,
) -> Result<(), BuildError> {
for (_resource_id, _update) in updates {
}
Ok(())
}
fn apply_release_updates(
&self,
_ast: &mut crate::ast::AST,
updates: &IndexMap<String, ReleaseUpdate>,
) -> Result<(), BuildError> {
for (_release_id, _update) in updates {
}
Ok(())
}
fn apply_deal_updates(
&self,
_ast: &mut crate::ast::AST,
updates: &IndexMap<String, DealUpdate>,
) -> Result<(), BuildError> {
for (_deal_id, _update) in updates {
}
Ok(())
}
fn ast_to_xml(&self, _ast: &crate::ast::AST) -> Result<String, BuildError> {
Ok(format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- Updated DDEX Message -->\n"
))
}
fn validate_operation(
&self,
operation: &UpdateOperation,
_update: &UpdateReleaseMessage,
) -> Result<(), BuildError> {
if operation.entity_id.is_empty() {
return Err(BuildError::InvalidFormat {
field: "entity_id".to_string(),
message: "Entity ID cannot be empty".to_string(),
});
}
match operation.action {
UpdateAction::Add => {
if operation.new_value.is_none() {
return Err(BuildError::InvalidFormat {
field: "new_value".to_string(),
message: "Add operation requires new_value".to_string(),
});
}
}
UpdateAction::Delete => {
if operation.old_value.is_none() {
return Err(BuildError::InvalidFormat {
field: "old_value".to_string(),
message: "Delete operation requires old_value".to_string(),
});
}
}
UpdateAction::Replace => {
if operation.old_value.is_none() || operation.new_value.is_none() {
return Err(BuildError::InvalidFormat {
field: "values".to_string(),
message: "Replace operation requires both old_value and new_value"
.to_string(),
});
}
}
UpdateAction::Move => {
}
}
Ok(())
}
fn validate_references(&self, update: &UpdateReleaseMessage) -> Result<(), BuildError> {
let mut referenced_resources = IndexSet::new();
let mut referenced_releases = IndexSet::new();
for operation in &update.update_list {
match operation.entity_type {
EntityType::Resource => {
referenced_resources.insert(operation.entity_id.clone());
}
EntityType::Release => {
referenced_releases.insert(operation.entity_id.clone());
}
_ => {}
}
}
for resource_id in &referenced_resources {
if !update.resource_updates.contains_key(resource_id) {
return Err(BuildError::InvalidReference {
reference: resource_id.clone(),
});
}
}
Ok(())
}
fn validate_dependencies(&self, operations: &[UpdateOperation]) -> Result<(), BuildError> {
let operation_ids: IndexSet<_> = operations.iter().map(|op| &op.operation_id).collect();
for operation in operations {
for dependency in &operation.dependencies {
if !operation_ids.contains(&dependency) {
return Err(BuildError::InvalidReference {
reference: format!("Missing dependency: {}", dependency),
});
}
}
}
Ok(())
}
fn detect_conflicts(&self, operations: &[UpdateOperation]) -> Result<Vec<String>, BuildError> {
let mut conflicts = Vec::new();
let mut path_operations: IndexMap<String, Vec<&UpdateOperation>> = IndexMap::new();
for operation in operations {
path_operations
.entry(operation.target_path.clone())
.or_default()
.push(operation);
}
for (path, ops) in path_operations {
if ops.len() > 1 {
let conflicting_ops: Vec<_> = ops.iter().map(|op| &op.operation_id).collect();
conflicts.push(format!(
"Path {} has conflicting operations: {:?}",
path, conflicting_ops
));
}
}
Ok(conflicts)
}
}
impl Default for UpdateGenerator {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for UpdateAction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UpdateAction::Add => write!(f, "Add"),
UpdateAction::Delete => write!(f, "Delete"),
UpdateAction::Replace => write!(f, "Replace"),
UpdateAction::Move => write!(f, "Move"),
}
}
}
impl std::fmt::Display for EntityType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EntityType::Resource => write!(f, "Resource"),
EntityType::Release => write!(f, "Release"),
EntityType::Deal => write!(f, "Deal"),
EntityType::Party => write!(f, "Party"),
EntityType::Metadata => write!(f, "Metadata"),
}
}
}
impl std::fmt::Display for ValidationStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ValidationStatus::Validated => write!(f, "Validated"),
ValidationStatus::WarningsOnly => write!(f, "Warnings Only"),
ValidationStatus::Invalid => write!(f, "Invalid"),
ValidationStatus::Pending => write!(f, "Pending"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_update_generator_creation() {
let generator = UpdateGenerator::new();
assert_eq!(generator.operation_counter, 0);
}
#[test]
fn test_update_config_defaults() {
let config = UpdateConfig::default();
assert!(config.include_non_critical);
assert_eq!(config.max_operations_per_update, 1000);
assert!(config.validate_references);
}
#[test]
fn test_operation_validation() {
let generator = UpdateGenerator::new();
let operation = UpdateOperation {
operation_id: "OP000001".to_string(),
action: UpdateAction::Add,
target_path: "/Release/Title".to_string(),
entity_type: EntityType::Release,
entity_id: "release-001".to_string(),
old_value: None,
new_value: Some("New Title".to_string()),
is_critical: false,
description: "Update title".to_string(),
dependencies: Vec::new(),
};
let update = UpdateReleaseMessage {
header: MessageHeaderRequest {
message_id: Some("TEST-001".to_string()),
message_sender: crate::builder::PartyRequest {
party_name: vec![crate::builder::LocalizedStringRequest {
text: "Test".to_string(),
language_code: None,
}],
party_id: None,
party_reference: None,
},
message_recipient: crate::builder::PartyRequest {
party_name: vec![crate::builder::LocalizedStringRequest {
text: "Test".to_string(),
language_code: None,
}],
party_id: None,
party_reference: None,
},
message_control_type: None,
message_created_date_time: None,
},
update_list: vec![operation.clone()],
resource_updates: IndexMap::new(),
release_updates: IndexMap::new(),
deal_updates: IndexMap::new(),
update_metadata: UpdateMetadata {
original_message_id: "ORIG-001".to_string(),
original_message_version: None,
original_message_timestamp: None,
update_created_timestamp: Utc::now(),
update_sequence: 1,
total_operations: 1,
impact_level: "Low".to_string(),
validation_status: ValidationStatus::Pending,
custom_metadata: IndexMap::new(),
},
};
assert!(generator.validate_operation(&operation, &update).is_ok());
}
#[test]
fn test_entity_type_determination() {
let generator = UpdateGenerator::new();
let resource_path = crate::diff::types::DiffPath::root()
.with_element("ResourceList")
.with_element("SoundRecording");
let release_path = crate::diff::types::DiffPath::root()
.with_element("ReleaseList")
.with_element("Release");
assert_eq!(
generator.determine_entity_type(&resource_path),
EntityType::Resource
);
assert_eq!(
generator.determine_entity_type(&release_path),
EntityType::Release
);
}
}