1use crate::builder::MessageHeaderRequest;
8use crate::diff::types::{ChangeSet, ChangeType, SemanticChange};
9use crate::diff::DiffEngine;
10use crate::error::BuildError;
11use chrono::{DateTime, Utc};
12use indexmap::{IndexMap, IndexSet};
13use serde::{Deserialize, Serialize};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct UpdateReleaseMessage {
70 pub header: MessageHeaderRequest,
72
73 pub update_list: Vec<UpdateOperation>,
75
76 pub resource_updates: IndexMap<String, ResourceUpdate>,
78
79 pub release_updates: IndexMap<String, ReleaseUpdate>,
81
82 pub deal_updates: IndexMap<String, DealUpdate>,
84
85 pub update_metadata: UpdateMetadata,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct UpdateOperation {
92 pub operation_id: String,
94
95 pub action: UpdateAction,
97
98 pub target_path: String,
100
101 pub entity_type: EntityType,
103
104 pub entity_id: String,
106
107 pub old_value: Option<String>,
109
110 pub new_value: Option<String>,
112
113 pub is_critical: bool,
115
116 pub description: String,
118
119 pub dependencies: Vec<String>,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
125pub enum UpdateAction {
126 Add,
128 Delete,
130 Replace,
132 Move,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
138pub enum EntityType {
139 Resource,
141 Release,
143 Deal,
145 Party,
147 Metadata,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct ResourceUpdate {
154 pub resource_id: String,
156
157 pub resource_reference: String,
159
160 pub action: UpdateAction,
162
163 pub resource_data: Option<ResourceData>,
165
166 pub technical_updates: Vec<TechnicalUpdate>,
168
169 pub metadata_updates: IndexMap<String, String>,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct ReleaseUpdate {
176 pub release_id: String,
178
179 pub release_reference: String,
181
182 pub action: UpdateAction,
184
185 pub release_data: Option<ReleaseData>,
187
188 pub track_updates: Vec<TrackUpdate>,
190
191 pub resource_reference_updates: Vec<ReferenceUpdate>,
193
194 pub metadata_updates: IndexMap<String, String>,
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct DealUpdate {
201 pub deal_id: String,
203
204 pub deal_reference: String,
206
207 pub action: UpdateAction,
209
210 pub deal_data: Option<DealData>,
212
213 pub terms_updates: Vec<TermsUpdate>,
215}
216
217#[derive(Debug, Clone)]
243pub struct UpdatedResource {
244 pub resource_type: String,
246 pub title: String,
248 pub artist: String,
250 pub isrc: Option<String>,
252 pub duration: Option<String>,
254 pub file_path: Option<String>,
256 pub technical_details: Option<TechnicalDetails>,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct ResourceData {
263 pub resource_type: String,
265 pub title: String,
267 pub artist: String,
269 pub isrc: Option<String>,
271 pub duration: Option<String>,
273 pub file_path: Option<String>,
275 pub technical_details: Option<TechnicalDetails>,
277}
278
279#[derive(Debug, Clone)]
305pub struct UpdatedRelease {
306 pub release_type: String,
308 pub title: String,
310 pub artist: String,
312 pub label: Option<String>,
314 pub upc: Option<String>,
316 pub release_date: Option<String>,
318 pub genre: Option<String>,
320 pub resource_references: Vec<String>,
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct ReleaseData {
327 pub release_type: String,
329 pub title: String,
331 pub artist: String,
333 pub label: Option<String>,
335 pub upc: Option<String>,
337 pub release_date: Option<String>,
339 pub genre: Option<String>,
341 pub resource_references: Vec<String>,
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct DealData {
348 pub commercial_model_type: String,
350 pub territory_codes: Vec<String>,
352 pub start_date: Option<String>,
354 pub end_date: Option<String>,
356 pub price: Option<PriceData>,
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize)]
362pub struct TechnicalUpdate {
363 pub field_name: String,
365 pub old_value: Option<String>,
367 pub new_value: Option<String>,
369 pub update_action: UpdateAction,
371}
372
373#[derive(Debug, Clone, Serialize, Deserialize)]
375pub struct TrackUpdate {
376 pub track_id: String,
378 pub action: UpdateAction,
380 pub old_resource_reference: Option<String>,
382 pub new_resource_reference: Option<String>,
384 pub position_change: Option<PositionChange>,
386}
387
388#[derive(Debug, Clone, Serialize, Deserialize)]
390pub struct ReferenceUpdate {
391 pub old_reference: String,
393 pub new_reference: String,
395 pub reference_type: String,
397 pub update_reason: String,
399}
400
401#[derive(Debug, Clone, Serialize, Deserialize)]
403pub struct TermsUpdate {
404 pub field_name: String,
406 pub old_value: Option<String>,
408 pub new_value: Option<String>,
410 pub effective_date: Option<String>,
412}
413
414#[derive(Debug, Clone, Serialize, Deserialize)]
416pub struct PositionChange {
417 pub old_position: usize,
419 pub new_position: usize,
421}
422
423#[derive(Debug, Clone, Serialize, Deserialize)]
447pub struct TechnicalDetails {
448 pub file_name: Option<String>,
450 pub codec_type: Option<String>,
452 pub bit_rate: Option<String>,
454 pub sample_rate: Option<String>,
456}
457
458#[derive(Debug, Clone, Serialize, Deserialize)]
460pub struct PriceData {
461 pub amount: String,
463 pub currency_code: String,
465 pub price_type: Option<String>,
467}
468
469#[derive(Debug, Clone, Serialize, Deserialize)]
471pub struct UpdateMetadata {
472 pub original_message_id: String,
474
475 pub original_message_version: Option<String>,
477
478 pub original_message_timestamp: Option<DateTime<Utc>>,
480
481 pub update_created_timestamp: DateTime<Utc>,
483
484 pub update_sequence: u64,
486
487 pub total_operations: usize,
489
490 pub impact_level: String,
492
493 pub validation_status: ValidationStatus,
495
496 pub custom_metadata: IndexMap<String, String>,
498}
499
500#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
502pub enum ValidationStatus {
503 Validated,
505 WarningsOnly,
507 Invalid,
509 Pending,
511}
512
513#[derive(Debug, Clone, Serialize, Deserialize)]
515pub struct UpdateConfig {
516 pub include_non_critical: bool,
518
519 pub max_operations_per_update: usize,
521
522 pub validate_references: bool,
524
525 pub optimize_references: bool,
527
528 pub excluded_fields: IndexSet<String>,
530
531 pub update_priorities: IndexMap<String, u8>,
533}
534
535impl Default for UpdateConfig {
536 fn default() -> Self {
537 let mut excluded_fields = IndexSet::new();
538 excluded_fields.insert("MessageId".to_string());
539 excluded_fields.insert("MessageCreatedDateTime".to_string());
540
541 Self {
542 include_non_critical: true,
543 max_operations_per_update: 1000,
544 validate_references: true,
545 optimize_references: true,
546 excluded_fields,
547 update_priorities: IndexMap::new(),
548 }
549 }
550}
551
552pub struct UpdateGenerator {
554 config: UpdateConfig,
555 diff_engine: DiffEngine,
556 operation_counter: u64,
557}
558
559impl UpdateGenerator {
560 pub fn new() -> Self {
562 Self {
563 config: UpdateConfig::default(),
564 diff_engine: DiffEngine::new(),
565 operation_counter: 0,
566 }
567 }
568
569 pub fn new_with_config(config: UpdateConfig) -> Self {
571 Self {
572 config,
573 diff_engine: DiffEngine::new(),
574 operation_counter: 0,
575 }
576 }
577
578 pub fn create_update(
580 &mut self,
581 original_xml: &str,
582 updated_xml: &str,
583 original_message_id: &str,
584 ) -> Result<UpdateReleaseMessage, BuildError> {
585 let original_ast = self.parse_xml_to_ast(original_xml)?;
587 let updated_ast = self.parse_xml_to_ast(updated_xml)?;
588
589 let changeset = self.diff_engine.diff(&original_ast, &updated_ast)?;
591
592 let update_operations = self.changeset_to_operations(&changeset)?;
594
595 let (resource_updates, release_updates, deal_updates) =
597 self.group_operations_by_entity(&update_operations)?;
598
599 let metadata =
601 self.create_update_metadata(original_message_id, &update_operations, &changeset);
602
603 let header = self.create_update_header(original_message_id, &metadata);
605
606 let update_message = UpdateReleaseMessage {
607 header,
608 update_list: update_operations,
609 resource_updates,
610 release_updates,
611 deal_updates,
612 update_metadata: metadata,
613 };
614
615 self.validate_update(&update_message)?;
617
618 Ok(update_message)
619 }
620
621 pub fn apply_update(
623 &self,
624 base_xml: &str,
625 update: &UpdateReleaseMessage,
626 ) -> Result<String, BuildError> {
627 let mut base_ast = self.parse_xml_to_ast(base_xml)?;
629
630 let ordered_operations = self.order_operations_by_dependencies(&update.update_list)?;
632
633 for operation in &ordered_operations {
634 self.apply_operation_to_ast(&mut base_ast, operation)?;
635 }
636
637 self.apply_resource_updates(&mut base_ast, &update.resource_updates)?;
639 self.apply_release_updates(&mut base_ast, &update.release_updates)?;
640 self.apply_deal_updates(&mut base_ast, &update.deal_updates)?;
641
642 self.ast_to_xml(&base_ast)
644 }
645
646 pub fn validate_update(
648 &self,
649 update: &UpdateReleaseMessage,
650 ) -> Result<ValidationStatus, BuildError> {
651 let mut errors = Vec::new();
652 let mut warnings = Vec::new();
653
654 for operation in &update.update_list {
656 if let Err(e) = self.validate_operation(operation, update) {
657 errors.push(format!("Operation {}: {}", operation.operation_id, e));
658 }
659 }
660
661 if self.config.validate_references {
663 if let Err(e) = self.validate_references(update) {
664 errors.push(format!("Reference validation: {}", e));
665 }
666 }
667
668 if let Err(e) = self.validate_dependencies(&update.update_list) {
670 errors.push(format!("Dependency validation: {}", e));
671 }
672
673 let conflicts = self.detect_conflicts(&update.update_list)?;
675 if !conflicts.is_empty() {
676 warnings.push(format!("Found {} potential conflicts", conflicts.len()));
677 }
678
679 if !errors.is_empty() {
680 Err(BuildError::ValidationFailed { errors })
681 } else if !warnings.is_empty() {
682 Ok(ValidationStatus::WarningsOnly)
683 } else {
684 Ok(ValidationStatus::Validated)
685 }
686 }
687
688 fn parse_xml_to_ast(&self, xml: &str) -> Result<crate::ast::AST, BuildError> {
691 let root = crate::ast::Element::new("NewReleaseMessage").with_text(xml);
693 Ok(crate::ast::AST {
694 root,
695 namespaces: IndexMap::new(),
696 schema_location: None,
697 })
698 }
699
700 fn changeset_to_operations(
701 &mut self,
702 changeset: &ChangeSet,
703 ) -> Result<Vec<UpdateOperation>, BuildError> {
704 let mut operations = Vec::new();
705
706 for change in &changeset.changes {
707 let operation = self.semantic_change_to_operation(change)?;
708 operations.push(operation);
709 }
710
711 Ok(operations)
712 }
713
714 fn semantic_change_to_operation(
715 &mut self,
716 change: &SemanticChange,
717 ) -> Result<UpdateOperation, BuildError> {
718 self.operation_counter += 1;
719
720 let action = match change.change_type {
721 ChangeType::ElementAdded | ChangeType::AttributeAdded => UpdateAction::Add,
722 ChangeType::ElementRemoved | ChangeType::AttributeRemoved => UpdateAction::Delete,
723 ChangeType::ElementMoved => UpdateAction::Move,
724 _ => UpdateAction::Replace,
725 };
726
727 let entity_type = self.determine_entity_type(&change.path);
728 let entity_id = self.extract_entity_id(&change.path)?;
729
730 Ok(UpdateOperation {
731 operation_id: format!("OP{:06}", self.operation_counter),
732 action,
733 target_path: change.path.to_string(),
734 entity_type,
735 entity_id,
736 old_value: change.old_value.clone(),
737 new_value: change.new_value.clone(),
738 is_critical: change.is_critical,
739 description: change.description.clone(),
740 dependencies: Vec::new(), })
742 }
743
744 fn determine_entity_type(&self, path: &crate::diff::types::DiffPath) -> EntityType {
745 let path_str = path.to_string().to_lowercase();
746
747 if path_str.contains("resource") {
748 EntityType::Resource
749 } else if path_str.contains("release") {
750 EntityType::Release
751 } else if path_str.contains("deal") {
752 EntityType::Deal
753 } else if path_str.contains("party") {
754 EntityType::Party
755 } else {
756 EntityType::Metadata
757 }
758 }
759
760 fn extract_entity_id(&self, path: &crate::diff::types::DiffPath) -> Result<String, BuildError> {
761 let path_str = path.to_string();
763 if let Some(id_start) = path_str.find("Id=") {
764 let id_part = &path_str[id_start + 3..];
765 if let Some(id_end) = id_part.find(&[']', '/', '@'][..]) {
766 Ok(id_part[..id_end].to_string())
767 } else {
768 Ok(id_part.to_string())
769 }
770 } else {
771 let uuid_str = uuid::Uuid::new_v4().to_string();
772 Ok(format!("unknown_{}", &uuid_str[..8]))
773 }
774 }
775
776 fn group_operations_by_entity(
777 &self,
778 operations: &[UpdateOperation],
779 ) -> Result<
780 (
781 IndexMap<String, ResourceUpdate>,
782 IndexMap<String, ReleaseUpdate>,
783 IndexMap<String, DealUpdate>,
784 ),
785 BuildError,
786 > {
787 let mut resource_updates = IndexMap::new();
788 let mut release_updates = IndexMap::new();
789 let mut deal_updates = IndexMap::new();
790
791 for operation in operations {
792 match operation.entity_type {
793 EntityType::Resource => {
794 let resource_update = self.operation_to_resource_update(operation)?;
795 resource_updates.insert(operation.entity_id.clone(), resource_update);
796 }
797 EntityType::Release => {
798 let release_update = self.operation_to_release_update(operation)?;
799 release_updates.insert(operation.entity_id.clone(), release_update);
800 }
801 EntityType::Deal => {
802 let deal_update = self.operation_to_deal_update(operation)?;
803 deal_updates.insert(operation.entity_id.clone(), deal_update);
804 }
805 _ => {} }
807 }
808
809 Ok((resource_updates, release_updates, deal_updates))
810 }
811
812 fn operation_to_resource_update(
813 &self,
814 operation: &UpdateOperation,
815 ) -> Result<ResourceUpdate, BuildError> {
816 Ok(ResourceUpdate {
817 resource_id: operation.entity_id.clone(),
818 resource_reference: format!(
819 "R{:06}",
820 operation.operation_id[2..].parse::<u32>().unwrap_or(0)
821 ),
822 action: operation.action,
823 resource_data: None, technical_updates: Vec::new(),
825 metadata_updates: IndexMap::new(),
826 })
827 }
828
829 fn operation_to_release_update(
830 &self,
831 operation: &UpdateOperation,
832 ) -> Result<ReleaseUpdate, BuildError> {
833 Ok(ReleaseUpdate {
834 release_id: operation.entity_id.clone(),
835 release_reference: format!(
836 "REL{:06}",
837 operation.operation_id[2..].parse::<u32>().unwrap_or(0)
838 ),
839 action: operation.action,
840 release_data: None, track_updates: Vec::new(),
842 resource_reference_updates: Vec::new(),
843 metadata_updates: IndexMap::new(),
844 })
845 }
846
847 fn operation_to_deal_update(
848 &self,
849 operation: &UpdateOperation,
850 ) -> Result<DealUpdate, BuildError> {
851 Ok(DealUpdate {
852 deal_id: operation.entity_id.clone(),
853 deal_reference: format!(
854 "D{:06}",
855 operation.operation_id[2..].parse::<u32>().unwrap_or(0)
856 ),
857 action: operation.action,
858 deal_data: None, terms_updates: Vec::new(),
860 })
861 }
862
863 fn create_update_metadata(
864 &self,
865 original_message_id: &str,
866 operations: &[UpdateOperation],
867 changeset: &ChangeSet,
868 ) -> UpdateMetadata {
869 UpdateMetadata {
870 original_message_id: original_message_id.to_string(),
871 original_message_version: None,
872 original_message_timestamp: None,
873 update_created_timestamp: Utc::now(),
874 update_sequence: 1,
875 total_operations: operations.len(),
876 impact_level: changeset.impact_level().to_string(),
877 validation_status: ValidationStatus::Pending,
878 custom_metadata: IndexMap::new(),
879 }
880 }
881
882 fn create_update_header(
883 &self,
884 original_message_id: &str,
885 metadata: &UpdateMetadata,
886 ) -> MessageHeaderRequest {
887 MessageHeaderRequest {
888 message_id: Some(format!(
889 "UPD-{}-{:04}",
890 original_message_id, metadata.update_sequence
891 )),
892 message_sender: crate::builder::PartyRequest {
893 party_name: vec![crate::builder::LocalizedStringRequest {
894 text: "DDEX Builder Update Engine".to_string(),
895 language_code: None,
896 }],
897 party_id: None,
898 party_reference: None,
899 },
900 message_recipient: crate::builder::PartyRequest {
901 party_name: vec![crate::builder::LocalizedStringRequest {
902 text: "Update Recipient".to_string(),
903 language_code: None,
904 }],
905 party_id: None,
906 party_reference: None,
907 },
908 message_control_type: Some("UpdateMessage".to_string()),
909 message_created_date_time: Some(metadata.update_created_timestamp.to_rfc3339()),
910 }
911 }
912
913 fn order_operations_by_dependencies(
914 &self,
915 operations: &[UpdateOperation],
916 ) -> Result<Vec<UpdateOperation>, BuildError> {
917 let mut ordered = operations.to_vec();
919
920 ordered.sort_by(|a, b| a.operation_id.cmp(&b.operation_id));
922
923 Ok(ordered)
924 }
925
926 fn apply_operation_to_ast(
927 &self,
928 _ast: &mut crate::ast::AST,
929 operation: &UpdateOperation,
930 ) -> Result<(), BuildError> {
931 match operation.action {
933 UpdateAction::Add => {
934 }
936 UpdateAction::Delete => {
937 }
939 UpdateAction::Replace => {
940 }
942 UpdateAction::Move => {
943 }
945 }
946 Ok(())
947 }
948
949 fn apply_resource_updates(
950 &self,
951 _ast: &mut crate::ast::AST,
952 updates: &IndexMap<String, ResourceUpdate>,
953 ) -> Result<(), BuildError> {
954 for (_resource_id, _update) in updates {
956 }
958 Ok(())
959 }
960
961 fn apply_release_updates(
962 &self,
963 _ast: &mut crate::ast::AST,
964 updates: &IndexMap<String, ReleaseUpdate>,
965 ) -> Result<(), BuildError> {
966 for (_release_id, _update) in updates {
968 }
970 Ok(())
971 }
972
973 fn apply_deal_updates(
974 &self,
975 _ast: &mut crate::ast::AST,
976 updates: &IndexMap<String, DealUpdate>,
977 ) -> Result<(), BuildError> {
978 for (_deal_id, _update) in updates {
980 }
982 Ok(())
983 }
984
985 fn ast_to_xml(&self, _ast: &crate::ast::AST) -> Result<String, BuildError> {
986 Ok(format!(
988 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- Updated DDEX Message -->\n"
989 ))
990 }
991
992 fn validate_operation(
993 &self,
994 operation: &UpdateOperation,
995 _update: &UpdateReleaseMessage,
996 ) -> Result<(), BuildError> {
997 if operation.entity_id.is_empty() {
999 return Err(BuildError::InvalidFormat {
1000 field: "entity_id".to_string(),
1001 message: "Entity ID cannot be empty".to_string(),
1002 });
1003 }
1004
1005 match operation.action {
1007 UpdateAction::Add => {
1008 if operation.new_value.is_none() {
1009 return Err(BuildError::InvalidFormat {
1010 field: "new_value".to_string(),
1011 message: "Add operation requires new_value".to_string(),
1012 });
1013 }
1014 }
1015 UpdateAction::Delete => {
1016 if operation.old_value.is_none() {
1017 return Err(BuildError::InvalidFormat {
1018 field: "old_value".to_string(),
1019 message: "Delete operation requires old_value".to_string(),
1020 });
1021 }
1022 }
1023 UpdateAction::Replace => {
1024 if operation.old_value.is_none() || operation.new_value.is_none() {
1025 return Err(BuildError::InvalidFormat {
1026 field: "values".to_string(),
1027 message: "Replace operation requires both old_value and new_value"
1028 .to_string(),
1029 });
1030 }
1031 }
1032 UpdateAction::Move => {
1033 }
1035 }
1036
1037 Ok(())
1038 }
1039
1040 fn validate_references(&self, update: &UpdateReleaseMessage) -> Result<(), BuildError> {
1041 let mut referenced_resources = IndexSet::new();
1043 let mut referenced_releases = IndexSet::new();
1044
1045 for operation in &update.update_list {
1047 match operation.entity_type {
1048 EntityType::Resource => {
1049 referenced_resources.insert(operation.entity_id.clone());
1050 }
1051 EntityType::Release => {
1052 referenced_releases.insert(operation.entity_id.clone());
1053 }
1054 _ => {}
1055 }
1056 }
1057
1058 for resource_id in &referenced_resources {
1060 if !update.resource_updates.contains_key(resource_id) {
1061 return Err(BuildError::InvalidReference {
1062 reference: resource_id.clone(),
1063 });
1064 }
1065 }
1066
1067 Ok(())
1068 }
1069
1070 fn validate_dependencies(&self, operations: &[UpdateOperation]) -> Result<(), BuildError> {
1071 let operation_ids: IndexSet<_> = operations.iter().map(|op| &op.operation_id).collect();
1072
1073 for operation in operations {
1074 for dependency in &operation.dependencies {
1075 if !operation_ids.contains(&dependency) {
1076 return Err(BuildError::InvalidReference {
1077 reference: format!("Missing dependency: {}", dependency),
1078 });
1079 }
1080 }
1081 }
1082
1083 Ok(())
1084 }
1085
1086 fn detect_conflicts(&self, operations: &[UpdateOperation]) -> Result<Vec<String>, BuildError> {
1087 let mut conflicts = Vec::new();
1088
1089 let mut path_operations: IndexMap<String, Vec<&UpdateOperation>> = IndexMap::new();
1091
1092 for operation in operations {
1093 path_operations
1094 .entry(operation.target_path.clone())
1095 .or_default()
1096 .push(operation);
1097 }
1098
1099 for (path, ops) in path_operations {
1100 if ops.len() > 1 {
1101 let conflicting_ops: Vec<_> = ops.iter().map(|op| &op.operation_id).collect();
1102 conflicts.push(format!(
1103 "Path {} has conflicting operations: {:?}",
1104 path, conflicting_ops
1105 ));
1106 }
1107 }
1108
1109 Ok(conflicts)
1110 }
1111}
1112
1113impl Default for UpdateGenerator {
1114 fn default() -> Self {
1115 Self::new()
1116 }
1117}
1118
1119impl std::fmt::Display for UpdateAction {
1121 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1122 match self {
1123 UpdateAction::Add => write!(f, "Add"),
1124 UpdateAction::Delete => write!(f, "Delete"),
1125 UpdateAction::Replace => write!(f, "Replace"),
1126 UpdateAction::Move => write!(f, "Move"),
1127 }
1128 }
1129}
1130
1131impl std::fmt::Display for EntityType {
1132 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1133 match self {
1134 EntityType::Resource => write!(f, "Resource"),
1135 EntityType::Release => write!(f, "Release"),
1136 EntityType::Deal => write!(f, "Deal"),
1137 EntityType::Party => write!(f, "Party"),
1138 EntityType::Metadata => write!(f, "Metadata"),
1139 }
1140 }
1141}
1142
1143impl std::fmt::Display for ValidationStatus {
1144 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1145 match self {
1146 ValidationStatus::Validated => write!(f, "Validated"),
1147 ValidationStatus::WarningsOnly => write!(f, "Warnings Only"),
1148 ValidationStatus::Invalid => write!(f, "Invalid"),
1149 ValidationStatus::Pending => write!(f, "Pending"),
1150 }
1151 }
1152}
1153
1154#[cfg(test)]
1155mod tests {
1156 use super::*;
1157
1158 #[test]
1159 fn test_update_generator_creation() {
1160 let generator = UpdateGenerator::new();
1161 assert_eq!(generator.operation_counter, 0);
1162 }
1163
1164 #[test]
1165 fn test_update_config_defaults() {
1166 let config = UpdateConfig::default();
1167 assert!(config.include_non_critical);
1168 assert_eq!(config.max_operations_per_update, 1000);
1169 assert!(config.validate_references);
1170 }
1171
1172 #[test]
1173 fn test_operation_validation() {
1174 let generator = UpdateGenerator::new();
1175
1176 let operation = UpdateOperation {
1177 operation_id: "OP000001".to_string(),
1178 action: UpdateAction::Add,
1179 target_path: "/Release/Title".to_string(),
1180 entity_type: EntityType::Release,
1181 entity_id: "release-001".to_string(),
1182 old_value: None,
1183 new_value: Some("New Title".to_string()),
1184 is_critical: false,
1185 description: "Update title".to_string(),
1186 dependencies: Vec::new(),
1187 };
1188
1189 let update = UpdateReleaseMessage {
1190 header: MessageHeaderRequest {
1191 message_id: Some("TEST-001".to_string()),
1192 message_sender: crate::builder::PartyRequest {
1193 party_name: vec![crate::builder::LocalizedStringRequest {
1194 text: "Test".to_string(),
1195 language_code: None,
1196 }],
1197 party_id: None,
1198 party_reference: None,
1199 },
1200 message_recipient: crate::builder::PartyRequest {
1201 party_name: vec![crate::builder::LocalizedStringRequest {
1202 text: "Test".to_string(),
1203 language_code: None,
1204 }],
1205 party_id: None,
1206 party_reference: None,
1207 },
1208 message_control_type: None,
1209 message_created_date_time: None,
1210 },
1211 update_list: vec![operation.clone()],
1212 resource_updates: IndexMap::new(),
1213 release_updates: IndexMap::new(),
1214 deal_updates: IndexMap::new(),
1215 update_metadata: UpdateMetadata {
1216 original_message_id: "ORIG-001".to_string(),
1217 original_message_version: None,
1218 original_message_timestamp: None,
1219 update_created_timestamp: Utc::now(),
1220 update_sequence: 1,
1221 total_operations: 1,
1222 impact_level: "Low".to_string(),
1223 validation_status: ValidationStatus::Pending,
1224 custom_metadata: IndexMap::new(),
1225 },
1226 };
1227
1228 assert!(generator.validate_operation(&operation, &update).is_ok());
1229 }
1230
1231 #[test]
1232 fn test_entity_type_determination() {
1233 let generator = UpdateGenerator::new();
1234
1235 let resource_path = crate::diff::types::DiffPath::root()
1236 .with_element("ResourceList")
1237 .with_element("SoundRecording");
1238
1239 let release_path = crate::diff::types::DiffPath::root()
1240 .with_element("ReleaseList")
1241 .with_element("Release");
1242
1243 assert_eq!(
1244 generator.determine_entity_type(&resource_path),
1245 EntityType::Resource
1246 );
1247 assert_eq!(
1248 generator.determine_entity_type(&release_path),
1249 EntityType::Release
1250 );
1251 }
1252}