1use crate::error::BuildError;
8use crate::builder::MessageHeaderRequest;
9use crate::diff::DiffEngine;
10use crate::diff::types::{ChangeSet, SemanticChange, ChangeType};
11use serde::{Serialize, Deserialize};
12use indexmap::{IndexMap, IndexSet};
13use chrono::{DateTime, Utc};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct UpdateReleaseMessage {
18 pub header: MessageHeaderRequest,
20
21 pub update_list: Vec<UpdateOperation>,
23
24 pub resource_updates: IndexMap<String, ResourceUpdate>,
26
27 pub release_updates: IndexMap<String, ReleaseUpdate>,
29
30 pub deal_updates: IndexMap<String, DealUpdate>,
32
33 pub update_metadata: UpdateMetadata,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct UpdateOperation {
40 pub operation_id: String,
42
43 pub action: UpdateAction,
45
46 pub target_path: String,
48
49 pub entity_type: EntityType,
51
52 pub entity_id: String,
54
55 pub old_value: Option<String>,
57
58 pub new_value: Option<String>,
60
61 pub is_critical: bool,
63
64 pub description: String,
66
67 pub dependencies: Vec<String>,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
73pub enum UpdateAction {
74 Add,
76 Delete,
78 Replace,
80 Move,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
86pub enum EntityType {
87 Resource,
89 Release,
91 Deal,
93 Party,
95 Metadata,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct ResourceUpdate {
102 pub resource_id: String,
104
105 pub resource_reference: String,
107
108 pub action: UpdateAction,
110
111 pub resource_data: Option<ResourceData>,
113
114 pub technical_updates: Vec<TechnicalUpdate>,
116
117 pub metadata_updates: IndexMap<String, String>,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct ReleaseUpdate {
124 pub release_id: String,
126
127 pub release_reference: String,
129
130 pub action: UpdateAction,
132
133 pub release_data: Option<ReleaseData>,
135
136 pub track_updates: Vec<TrackUpdate>,
138
139 pub resource_reference_updates: Vec<ReferenceUpdate>,
141
142 pub metadata_updates: IndexMap<String, String>,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct DealUpdate {
149 pub deal_id: String,
151
152 pub deal_reference: String,
154
155 pub action: UpdateAction,
157
158 pub deal_data: Option<DealData>,
160
161 pub terms_updates: Vec<TermsUpdate>,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct ResourceData {
168 pub resource_type: String,
169 pub title: String,
170 pub artist: String,
171 pub isrc: Option<String>,
172 pub duration: Option<String>,
173 pub file_path: Option<String>,
174 pub technical_details: Option<TechnicalDetails>,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct ReleaseData {
180 pub release_type: String,
181 pub title: String,
182 pub artist: String,
183 pub label: Option<String>,
184 pub upc: Option<String>,
185 pub release_date: Option<String>,
186 pub genre: Option<String>,
187 pub resource_references: Vec<String>,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct DealData {
193 pub commercial_model_type: String,
194 pub territory_codes: Vec<String>,
195 pub start_date: Option<String>,
196 pub end_date: Option<String>,
197 pub price: Option<PriceData>,
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct TechnicalUpdate {
203 pub field_name: String,
204 pub old_value: Option<String>,
205 pub new_value: Option<String>,
206 pub update_action: UpdateAction,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct TrackUpdate {
212 pub track_id: String,
213 pub action: UpdateAction,
214 pub old_resource_reference: Option<String>,
215 pub new_resource_reference: Option<String>,
216 pub position_change: Option<PositionChange>,
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct ReferenceUpdate {
222 pub old_reference: String,
223 pub new_reference: String,
224 pub reference_type: String,
225 pub update_reason: String,
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct TermsUpdate {
231 pub field_name: String,
232 pub old_value: Option<String>,
233 pub new_value: Option<String>,
234 pub effective_date: Option<String>,
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize)]
239pub struct PositionChange {
240 pub old_position: usize,
241 pub new_position: usize,
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct TechnicalDetails {
247 pub file_name: Option<String>,
248 pub codec_type: Option<String>,
249 pub bit_rate: Option<String>,
250 pub sample_rate: Option<String>,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct PriceData {
256 pub amount: String,
257 pub currency_code: String,
258 pub price_type: Option<String>,
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct UpdateMetadata {
264 pub original_message_id: String,
266
267 pub original_message_version: Option<String>,
269
270 pub original_message_timestamp: Option<DateTime<Utc>>,
272
273 pub update_created_timestamp: DateTime<Utc>,
275
276 pub update_sequence: u64,
278
279 pub total_operations: usize,
281
282 pub impact_level: String,
284
285 pub validation_status: ValidationStatus,
287
288 pub custom_metadata: IndexMap<String, String>,
290}
291
292#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
294pub enum ValidationStatus {
295 Validated,
297 WarningsOnly,
299 Invalid,
301 Pending,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct UpdateConfig {
308 pub include_non_critical: bool,
310
311 pub max_operations_per_update: usize,
313
314 pub validate_references: bool,
316
317 pub optimize_references: bool,
319
320 pub excluded_fields: IndexSet<String>,
322
323 pub update_priorities: IndexMap<String, u8>,
325}
326
327impl Default for UpdateConfig {
328 fn default() -> Self {
329 let mut excluded_fields = IndexSet::new();
330 excluded_fields.insert("MessageId".to_string());
331 excluded_fields.insert("MessageCreatedDateTime".to_string());
332
333 Self {
334 include_non_critical: true,
335 max_operations_per_update: 1000,
336 validate_references: true,
337 optimize_references: true,
338 excluded_fields,
339 update_priorities: IndexMap::new(),
340 }
341 }
342}
343
344pub struct UpdateGenerator {
346 config: UpdateConfig,
347 diff_engine: DiffEngine,
348 operation_counter: u64,
349}
350
351impl UpdateGenerator {
352 pub fn new() -> Self {
354 Self {
355 config: UpdateConfig::default(),
356 diff_engine: DiffEngine::new(),
357 operation_counter: 0,
358 }
359 }
360
361 pub fn new_with_config(config: UpdateConfig) -> Self {
363 Self {
364 config,
365 diff_engine: DiffEngine::new(),
366 operation_counter: 0,
367 }
368 }
369
370 pub fn create_update(
372 &mut self,
373 original_xml: &str,
374 updated_xml: &str,
375 original_message_id: &str,
376 ) -> Result<UpdateReleaseMessage, BuildError> {
377 let original_ast = self.parse_xml_to_ast(original_xml)?;
379 let updated_ast = self.parse_xml_to_ast(updated_xml)?;
380
381 let changeset = self.diff_engine.diff(&original_ast, &updated_ast)?;
383
384 let update_operations = self.changeset_to_operations(&changeset)?;
386
387 let (resource_updates, release_updates, deal_updates) =
389 self.group_operations_by_entity(&update_operations)?;
390
391 let metadata = self.create_update_metadata(
393 original_message_id,
394 &update_operations,
395 &changeset,
396 );
397
398 let header = self.create_update_header(original_message_id, &metadata);
400
401 let update_message = UpdateReleaseMessage {
402 header,
403 update_list: update_operations,
404 resource_updates,
405 release_updates,
406 deal_updates,
407 update_metadata: metadata,
408 };
409
410 self.validate_update(&update_message)?;
412
413 Ok(update_message)
414 }
415
416 pub fn apply_update(
418 &self,
419 base_xml: &str,
420 update: &UpdateReleaseMessage,
421 ) -> Result<String, BuildError> {
422 let mut base_ast = self.parse_xml_to_ast(base_xml)?;
424
425 let ordered_operations = self.order_operations_by_dependencies(&update.update_list)?;
427
428 for operation in &ordered_operations {
429 self.apply_operation_to_ast(&mut base_ast, operation)?;
430 }
431
432 self.apply_resource_updates(&mut base_ast, &update.resource_updates)?;
434 self.apply_release_updates(&mut base_ast, &update.release_updates)?;
435 self.apply_deal_updates(&mut base_ast, &update.deal_updates)?;
436
437 self.ast_to_xml(&base_ast)
439 }
440
441 pub fn validate_update(&self, update: &UpdateReleaseMessage) -> Result<ValidationStatus, BuildError> {
443 let mut errors = Vec::new();
444 let mut warnings = Vec::new();
445
446 for operation in &update.update_list {
448 if let Err(e) = self.validate_operation(operation, update) {
449 errors.push(format!("Operation {}: {}", operation.operation_id, e));
450 }
451 }
452
453 if self.config.validate_references {
455 if let Err(e) = self.validate_references(update) {
456 errors.push(format!("Reference validation: {}", e));
457 }
458 }
459
460 if let Err(e) = self.validate_dependencies(&update.update_list) {
462 errors.push(format!("Dependency validation: {}", e));
463 }
464
465 let conflicts = self.detect_conflicts(&update.update_list)?;
467 if !conflicts.is_empty() {
468 warnings.push(format!("Found {} potential conflicts", conflicts.len()));
469 }
470
471 if !errors.is_empty() {
472 Err(BuildError::ValidationFailed { errors })
473 } else if !warnings.is_empty() {
474 Ok(ValidationStatus::WarningsOnly)
475 } else {
476 Ok(ValidationStatus::Validated)
477 }
478 }
479
480 fn parse_xml_to_ast(&self, xml: &str) -> Result<crate::ast::AST, BuildError> {
483 let root = crate::ast::Element::new("NewReleaseMessage").with_text(xml);
485 Ok(crate::ast::AST {
486 root,
487 namespaces: IndexMap::new(),
488 schema_location: None,
489 })
490 }
491
492 fn changeset_to_operations(&mut self, changeset: &ChangeSet) -> Result<Vec<UpdateOperation>, BuildError> {
493 let mut operations = Vec::new();
494
495 for change in &changeset.changes {
496 let operation = self.semantic_change_to_operation(change)?;
497 operations.push(operation);
498 }
499
500 Ok(operations)
501 }
502
503 fn semantic_change_to_operation(&mut self, change: &SemanticChange) -> Result<UpdateOperation, BuildError> {
504 self.operation_counter += 1;
505
506 let action = match change.change_type {
507 ChangeType::ElementAdded | ChangeType::AttributeAdded => UpdateAction::Add,
508 ChangeType::ElementRemoved | ChangeType::AttributeRemoved => UpdateAction::Delete,
509 ChangeType::ElementMoved => UpdateAction::Move,
510 _ => UpdateAction::Replace,
511 };
512
513 let entity_type = self.determine_entity_type(&change.path);
514 let entity_id = self.extract_entity_id(&change.path)?;
515
516 Ok(UpdateOperation {
517 operation_id: format!("OP{:06}", self.operation_counter),
518 action,
519 target_path: change.path.to_string(),
520 entity_type,
521 entity_id,
522 old_value: change.old_value.clone(),
523 new_value: change.new_value.clone(),
524 is_critical: change.is_critical,
525 description: change.description.clone(),
526 dependencies: Vec::new(), })
528 }
529
530 fn determine_entity_type(&self, path: &crate::diff::types::DiffPath) -> EntityType {
531 let path_str = path.to_string().to_lowercase();
532
533 if path_str.contains("resource") {
534 EntityType::Resource
535 } else if path_str.contains("release") {
536 EntityType::Release
537 } else if path_str.contains("deal") {
538 EntityType::Deal
539 } else if path_str.contains("party") {
540 EntityType::Party
541 } else {
542 EntityType::Metadata
543 }
544 }
545
546 fn extract_entity_id(&self, path: &crate::diff::types::DiffPath) -> Result<String, BuildError> {
547 let path_str = path.to_string();
549 if let Some(id_start) = path_str.find("Id=") {
550 let id_part = &path_str[id_start + 3..];
551 if let Some(id_end) = id_part.find(&[']', '/', '@'][..]) {
552 Ok(id_part[..id_end].to_string())
553 } else {
554 Ok(id_part.to_string())
555 }
556 } else {
557 let uuid_str = uuid::Uuid::new_v4().to_string();
558 Ok(format!("unknown_{}", &uuid_str[..8]))
559 }
560 }
561
562 fn group_operations_by_entity(
563 &self,
564 operations: &[UpdateOperation],
565 ) -> Result<(IndexMap<String, ResourceUpdate>, IndexMap<String, ReleaseUpdate>, IndexMap<String, DealUpdate>), BuildError> {
566 let mut resource_updates = IndexMap::new();
567 let mut release_updates = IndexMap::new();
568 let mut deal_updates = IndexMap::new();
569
570 for operation in operations {
571 match operation.entity_type {
572 EntityType::Resource => {
573 let resource_update = self.operation_to_resource_update(operation)?;
574 resource_updates.insert(operation.entity_id.clone(), resource_update);
575 },
576 EntityType::Release => {
577 let release_update = self.operation_to_release_update(operation)?;
578 release_updates.insert(operation.entity_id.clone(), release_update);
579 },
580 EntityType::Deal => {
581 let deal_update = self.operation_to_deal_update(operation)?;
582 deal_updates.insert(operation.entity_id.clone(), deal_update);
583 },
584 _ => {}, }
586 }
587
588 Ok((resource_updates, release_updates, deal_updates))
589 }
590
591 fn operation_to_resource_update(&self, operation: &UpdateOperation) -> Result<ResourceUpdate, BuildError> {
592 Ok(ResourceUpdate {
593 resource_id: operation.entity_id.clone(),
594 resource_reference: format!("R{:06}", operation.operation_id[2..].parse::<u32>().unwrap_or(0)),
595 action: operation.action,
596 resource_data: None, technical_updates: Vec::new(),
598 metadata_updates: IndexMap::new(),
599 })
600 }
601
602 fn operation_to_release_update(&self, operation: &UpdateOperation) -> Result<ReleaseUpdate, BuildError> {
603 Ok(ReleaseUpdate {
604 release_id: operation.entity_id.clone(),
605 release_reference: format!("REL{:06}", operation.operation_id[2..].parse::<u32>().unwrap_or(0)),
606 action: operation.action,
607 release_data: None, track_updates: Vec::new(),
609 resource_reference_updates: Vec::new(),
610 metadata_updates: IndexMap::new(),
611 })
612 }
613
614 fn operation_to_deal_update(&self, operation: &UpdateOperation) -> Result<DealUpdate, BuildError> {
615 Ok(DealUpdate {
616 deal_id: operation.entity_id.clone(),
617 deal_reference: format!("D{:06}", operation.operation_id[2..].parse::<u32>().unwrap_or(0)),
618 action: operation.action,
619 deal_data: None, terms_updates: Vec::new(),
621 })
622 }
623
624 fn create_update_metadata(
625 &self,
626 original_message_id: &str,
627 operations: &[UpdateOperation],
628 changeset: &ChangeSet,
629 ) -> UpdateMetadata {
630 UpdateMetadata {
631 original_message_id: original_message_id.to_string(),
632 original_message_version: None,
633 original_message_timestamp: None,
634 update_created_timestamp: Utc::now(),
635 update_sequence: 1,
636 total_operations: operations.len(),
637 impact_level: changeset.impact_level().to_string(),
638 validation_status: ValidationStatus::Pending,
639 custom_metadata: IndexMap::new(),
640 }
641 }
642
643 fn create_update_header(&self, original_message_id: &str, metadata: &UpdateMetadata) -> MessageHeaderRequest {
644 MessageHeaderRequest {
645 message_id: Some(format!("UPD-{}-{:04}", original_message_id, metadata.update_sequence)),
646 message_sender: crate::builder::PartyRequest {
647 party_name: vec![crate::builder::LocalizedStringRequest {
648 text: "DDEX Builder Update Engine".to_string(),
649 language_code: None,
650 }],
651 party_id: None,
652 party_reference: None,
653 },
654 message_recipient: crate::builder::PartyRequest {
655 party_name: vec![crate::builder::LocalizedStringRequest {
656 text: "Update Recipient".to_string(),
657 language_code: None,
658 }],
659 party_id: None,
660 party_reference: None,
661 },
662 message_control_type: Some("UpdateMessage".to_string()),
663 message_created_date_time: Some(metadata.update_created_timestamp.to_rfc3339()),
664 }
665 }
666
667 fn order_operations_by_dependencies(&self, operations: &[UpdateOperation]) -> Result<Vec<UpdateOperation>, BuildError> {
668 let mut ordered = operations.to_vec();
670
671 ordered.sort_by(|a, b| a.operation_id.cmp(&b.operation_id));
673
674 Ok(ordered)
675 }
676
677 fn apply_operation_to_ast(&self, _ast: &mut crate::ast::AST, operation: &UpdateOperation) -> Result<(), BuildError> {
678 match operation.action {
680 UpdateAction::Add => {
681 },
683 UpdateAction::Delete => {
684 },
686 UpdateAction::Replace => {
687 },
689 UpdateAction::Move => {
690 },
692 }
693 Ok(())
694 }
695
696 fn apply_resource_updates(&self, _ast: &mut crate::ast::AST, updates: &IndexMap<String, ResourceUpdate>) -> Result<(), BuildError> {
697 for (_resource_id, _update) in updates {
699 }
701 Ok(())
702 }
703
704 fn apply_release_updates(&self, _ast: &mut crate::ast::AST, updates: &IndexMap<String, ReleaseUpdate>) -> Result<(), BuildError> {
705 for (_release_id, _update) in updates {
707 }
709 Ok(())
710 }
711
712 fn apply_deal_updates(&self, _ast: &mut crate::ast::AST, updates: &IndexMap<String, DealUpdate>) -> Result<(), BuildError> {
713 for (_deal_id, _update) in updates {
715 }
717 Ok(())
718 }
719
720 fn ast_to_xml(&self, _ast: &crate::ast::AST) -> Result<String, BuildError> {
721 Ok(format!("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- Updated DDEX Message -->\n"))
723 }
724
725 fn validate_operation(&self, operation: &UpdateOperation, _update: &UpdateReleaseMessage) -> Result<(), BuildError> {
726 if operation.entity_id.is_empty() {
728 return Err(BuildError::InvalidFormat {
729 field: "entity_id".to_string(),
730 message: "Entity ID cannot be empty".to_string(),
731 });
732 }
733
734 match operation.action {
736 UpdateAction::Add => {
737 if operation.new_value.is_none() {
738 return Err(BuildError::InvalidFormat {
739 field: "new_value".to_string(),
740 message: "Add operation requires new_value".to_string(),
741 });
742 }
743 },
744 UpdateAction::Delete => {
745 if operation.old_value.is_none() {
746 return Err(BuildError::InvalidFormat {
747 field: "old_value".to_string(),
748 message: "Delete operation requires old_value".to_string(),
749 });
750 }
751 },
752 UpdateAction::Replace => {
753 if operation.old_value.is_none() || operation.new_value.is_none() {
754 return Err(BuildError::InvalidFormat {
755 field: "values".to_string(),
756 message: "Replace operation requires both old_value and new_value".to_string(),
757 });
758 }
759 },
760 UpdateAction::Move => {
761 },
763 }
764
765 Ok(())
766 }
767
768 fn validate_references(&self, update: &UpdateReleaseMessage) -> Result<(), BuildError> {
769 let mut referenced_resources = IndexSet::new();
771 let mut referenced_releases = IndexSet::new();
772
773 for operation in &update.update_list {
775 match operation.entity_type {
776 EntityType::Resource => {
777 referenced_resources.insert(operation.entity_id.clone());
778 },
779 EntityType::Release => {
780 referenced_releases.insert(operation.entity_id.clone());
781 },
782 _ => {},
783 }
784 }
785
786 for resource_id in &referenced_resources {
788 if !update.resource_updates.contains_key(resource_id) {
789 return Err(BuildError::InvalidReference {
790 reference: resource_id.clone(),
791 });
792 }
793 }
794
795 Ok(())
796 }
797
798 fn validate_dependencies(&self, operations: &[UpdateOperation]) -> Result<(), BuildError> {
799 let operation_ids: IndexSet<_> = operations.iter().map(|op| &op.operation_id).collect();
800
801 for operation in operations {
802 for dependency in &operation.dependencies {
803 if !operation_ids.contains(&dependency) {
804 return Err(BuildError::InvalidReference {
805 reference: format!("Missing dependency: {}", dependency),
806 });
807 }
808 }
809 }
810
811 Ok(())
812 }
813
814 fn detect_conflicts(&self, operations: &[UpdateOperation]) -> Result<Vec<String>, BuildError> {
815 let mut conflicts = Vec::new();
816
817 let mut path_operations: IndexMap<String, Vec<&UpdateOperation>> = IndexMap::new();
819
820 for operation in operations {
821 path_operations.entry(operation.target_path.clone())
822 .or_default()
823 .push(operation);
824 }
825
826 for (path, ops) in path_operations {
827 if ops.len() > 1 {
828 let conflicting_ops: Vec<_> = ops.iter().map(|op| &op.operation_id).collect();
829 conflicts.push(format!("Path {} has conflicting operations: {:?}", path, conflicting_ops));
830 }
831 }
832
833 Ok(conflicts)
834 }
835}
836
837impl Default for UpdateGenerator {
838 fn default() -> Self {
839 Self::new()
840 }
841}
842
843impl std::fmt::Display for UpdateAction {
845 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
846 match self {
847 UpdateAction::Add => write!(f, "Add"),
848 UpdateAction::Delete => write!(f, "Delete"),
849 UpdateAction::Replace => write!(f, "Replace"),
850 UpdateAction::Move => write!(f, "Move"),
851 }
852 }
853}
854
855impl std::fmt::Display for EntityType {
856 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
857 match self {
858 EntityType::Resource => write!(f, "Resource"),
859 EntityType::Release => write!(f, "Release"),
860 EntityType::Deal => write!(f, "Deal"),
861 EntityType::Party => write!(f, "Party"),
862 EntityType::Metadata => write!(f, "Metadata"),
863 }
864 }
865}
866
867impl std::fmt::Display for ValidationStatus {
868 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
869 match self {
870 ValidationStatus::Validated => write!(f, "Validated"),
871 ValidationStatus::WarningsOnly => write!(f, "Warnings Only"),
872 ValidationStatus::Invalid => write!(f, "Invalid"),
873 ValidationStatus::Pending => write!(f, "Pending"),
874 }
875 }
876}
877
878#[cfg(test)]
879mod tests {
880 use super::*;
881
882 #[test]
883 fn test_update_generator_creation() {
884 let generator = UpdateGenerator::new();
885 assert_eq!(generator.operation_counter, 0);
886 }
887
888 #[test]
889 fn test_update_config_defaults() {
890 let config = UpdateConfig::default();
891 assert!(config.include_non_critical);
892 assert_eq!(config.max_operations_per_update, 1000);
893 assert!(config.validate_references);
894 }
895
896 #[test]
897 fn test_operation_validation() {
898 let generator = UpdateGenerator::new();
899
900 let operation = UpdateOperation {
901 operation_id: "OP000001".to_string(),
902 action: UpdateAction::Add,
903 target_path: "/Release/Title".to_string(),
904 entity_type: EntityType::Release,
905 entity_id: "release-001".to_string(),
906 old_value: None,
907 new_value: Some("New Title".to_string()),
908 is_critical: false,
909 description: "Update title".to_string(),
910 dependencies: Vec::new(),
911 };
912
913 let update = UpdateReleaseMessage {
914 header: MessageHeaderRequest {
915 message_id: Some("TEST-001".to_string()),
916 message_sender: crate::builder::PartyRequest {
917 party_name: vec![crate::builder::LocalizedStringRequest {
918 text: "Test".to_string(),
919 language_code: None,
920 }],
921 party_id: None,
922 party_reference: None,
923 },
924 message_recipient: crate::builder::PartyRequest {
925 party_name: vec![crate::builder::LocalizedStringRequest {
926 text: "Test".to_string(),
927 language_code: None,
928 }],
929 party_id: None,
930 party_reference: None,
931 },
932 message_control_type: None,
933 message_created_date_time: None,
934 },
935 update_list: vec![operation.clone()],
936 resource_updates: IndexMap::new(),
937 release_updates: IndexMap::new(),
938 deal_updates: IndexMap::new(),
939 update_metadata: UpdateMetadata {
940 original_message_id: "ORIG-001".to_string(),
941 original_message_version: None,
942 original_message_timestamp: None,
943 update_created_timestamp: Utc::now(),
944 update_sequence: 1,
945 total_operations: 1,
946 impact_level: "Low".to_string(),
947 validation_status: ValidationStatus::Pending,
948 custom_metadata: IndexMap::new(),
949 },
950 };
951
952 assert!(generator.validate_operation(&operation, &update).is_ok());
953 }
954
955 #[test]
956 fn test_entity_type_determination() {
957 let generator = UpdateGenerator::new();
958
959 let resource_path = crate::diff::types::DiffPath::root()
960 .with_element("ResourceList")
961 .with_element("SoundRecording");
962
963 let release_path = crate::diff::types::DiffPath::root()
964 .with_element("ReleaseList")
965 .with_element("Release");
966
967 assert_eq!(generator.determine_entity_type(&resource_path), EntityType::Resource);
968 assert_eq!(generator.determine_entity_type(&release_path), EntityType::Release);
969 }
970}