1pub use super::preflight::PreflightLevel;
4use crate::generator::{xml_writer::XmlWriter, ASTGenerator};
5use indexmap::IndexMap;
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct BuildRequest {
43 pub header: MessageHeaderRequest,
45
46 pub version: String,
48
49 pub profile: Option<String>,
51
52 pub releases: Vec<ReleaseRequest>,
55
56 pub deals: Vec<DealRequest>,
58
59 pub extensions: Option<IndexMap<String, String>>,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct MessageHeaderRequest {
89 pub message_id: Option<String>,
91 pub message_sender: PartyRequest,
93 pub message_recipient: PartyRequest,
95 pub message_control_type: Option<String>,
97 pub message_created_date_time: Option<String>,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct PartyRequest {
127 pub party_name: Vec<LocalizedStringRequest>,
129 pub party_id: Option<String>,
131 pub party_reference: Option<String>,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct LocalizedStringRequest {
157 pub text: String,
159 pub language_code: Option<String>,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct ReleaseRequest {
198 pub release_id: String,
200 pub release_reference: Option<String>,
202 pub title: Vec<LocalizedStringRequest>,
204 pub artist: String,
206 pub label: Option<String>,
208 pub release_date: Option<String>,
210 pub upc: Option<String>,
212 pub tracks: Vec<TrackRequest>,
214 pub resource_references: Option<Vec<String>>,
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct TrackRequest {
238 pub track_id: String,
240 pub resource_reference: Option<String>,
242 pub isrc: String,
244 pub title: String,
246 pub duration: String,
248 pub artist: String,
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct DealRequest {
273 pub deal_reference: Option<String>,
275 pub deal_terms: DealTerms,
277 pub release_references: Vec<String>,
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
297pub struct DealTerms {
298 pub commercial_model_type: String,
300 pub territory_code: Vec<String>,
302 pub start_date: Option<String>,
304}
305
306#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct BuildOptions {
309 pub determinism: Option<super::determinism::DeterminismConfig>,
311
312 pub preflight_level: super::preflight::PreflightLevel,
314
315 pub id_strategy: IdStrategy,
317
318 pub stable_hash_config: Option<super::id_generator::StableHashConfig>,
320}
321
322impl Default for BuildOptions {
323 fn default() -> Self {
324 Self {
325 determinism: None,
326 preflight_level: super::preflight::PreflightLevel::Warn,
327 id_strategy: IdStrategy::UUID,
328 stable_hash_config: None,
329 }
330 }
331}
332
333#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
335pub enum IdStrategy {
336 UUID,
338 UUIDv7,
340 Sequential,
342 StableHash,
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize)]
348pub struct BuildResult {
349 pub xml: String,
351
352 pub warnings: Vec<BuildWarning>,
354
355 pub errors: Vec<super::error::BuildError>,
357
358 pub statistics: BuildStatistics,
360
361 pub canonical_hash: Option<String>,
363
364 pub reproducibility_banner: Option<String>,
366}
367
368#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct BuildWarning {
371 pub code: String,
373 pub message: String,
375 pub location: Option<String>,
377}
378
379#[derive(Debug, Clone, Serialize, Deserialize)]
381pub struct BuildStatistics {
382 pub releases: usize,
384 pub tracks: usize,
386 pub deals: usize,
388 pub generation_time_ms: u64,
390 pub xml_size_bytes: usize,
392}
393
394impl Default for BuildStatistics {
395 fn default() -> Self {
396 Self {
397 releases: 0,
398 tracks: 0,
399 deals: 0,
400 generation_time_ms: 0,
401 xml_size_bytes: 0,
402 }
403 }
404}
405
406pub struct DDEXBuilder {
408 _inner: super::Builder,
409}
410
411impl DDEXBuilder {
412 pub fn new() -> Self {
414 Self {
415 _inner: super::Builder::new(),
416 }
417 }
418
419 pub fn build(
421 &self,
422 mut request: BuildRequest,
423 options: BuildOptions,
424 ) -> Result<BuildResult, super::error::BuildError> {
425 let start = std::time::Instant::now();
426 let mut warnings = Vec::new();
427
428 let validator =
430 super::preflight::PreflightValidator::new(super::preflight::ValidationConfig {
431 level: options.preflight_level,
432 profile: request.profile.clone(),
433 validate_identifiers: true,
434 validate_checksums: true,
435 check_required_fields: true,
436 validate_dates: true,
437 validate_references: true,
438 });
439
440 let validation_result = validator.validate(&request)?;
441
442 for warning in validation_result.warnings {
444 warnings.push(BuildWarning {
445 code: warning.code,
446 message: warning.message,
447 location: Some(warning.location),
448 });
449 }
450
451 if !validation_result.passed {
453 if options.preflight_level == super::preflight::PreflightLevel::Strict {
454 return Err(super::error::BuildError::ValidationFailed {
455 errors: validation_result
456 .errors
457 .iter()
458 .map(|e| format!("{}: {}", e.code, e.message))
459 .collect(),
460 });
461 }
462 }
463
464 self.generate_ids(&mut request, &options)?;
466
467 let mut generator = ASTGenerator::new(request.version.clone());
469 let ast = generator.generate(&request)?;
470
471 let config = options.determinism.unwrap_or_default();
473
474 let writer = XmlWriter::new(config.clone());
476 let xml = writer.write(&ast)?;
477
478 let (final_xml, canonical_hash) =
480 if config.canon_mode == super::determinism::CanonMode::DbC14n {
481 let canonicalizer = super::canonical::DB_C14N::new(config.clone());
482 let canonical = canonicalizer.canonicalize(&xml)?;
483 let hash = Some(canonicalizer.canonical_hash(&canonical)?);
484 (canonical, hash)
485 } else {
486 (xml, None)
487 };
488
489 let reproducibility_banner = if config.emit_reproducibility_banner {
491 Some(format!(
492 "Generated by DDEX Builder v{} with DB-C14N/{}",
493 env!("CARGO_PKG_VERSION"),
494 super::DB_C14N_VERSION
495 ))
496 } else {
497 None
498 };
499
500 let elapsed = start.elapsed();
501
502 Ok(BuildResult {
503 xml: final_xml.clone(),
504 warnings,
505 errors: Vec::new(),
506 statistics: BuildStatistics {
507 releases: request.releases.len(),
508 tracks: request.releases.iter().map(|r| r.tracks.len()).sum(),
509 deals: request.deals.len(),
510 generation_time_ms: elapsed.as_millis() as u64,
511 xml_size_bytes: final_xml.len(),
512 },
513 canonical_hash,
514 reproducibility_banner,
515 })
516 }
517
518 fn generate_ids(
520 &self,
521 request: &mut BuildRequest,
522 options: &BuildOptions,
523 ) -> Result<(), super::error::BuildError> {
524 match options.id_strategy {
525 IdStrategy::UUID => {
526 self.generate_uuid_ids(request)?;
527 }
528 IdStrategy::UUIDv7 => {
529 self.generate_uuidv7_ids(request)?;
530 }
531 IdStrategy::Sequential => {
532 self.generate_sequential_ids(request)?;
533 }
534 IdStrategy::StableHash => {
535 self.generate_stable_hash_ids(request, options)?;
536 }
537 }
538 Ok(())
539 }
540
541 fn generate_uuid_ids(
543 &self,
544 request: &mut BuildRequest,
545 ) -> Result<(), super::error::BuildError> {
546 use uuid::Uuid;
547
548 if request.header.message_id.is_none() {
550 request.header.message_id = Some(format!("MSG_{}", Uuid::new_v4()));
551 }
552
553 for release in &mut request.releases {
555 if release.release_reference.is_none() {
556 release.release_reference = Some(format!("R{}", Uuid::new_v4().simple()));
557 }
558
559 for track in &mut release.tracks {
561 if track.resource_reference.is_none() {
562 track.resource_reference = Some(format!("A{}", Uuid::new_v4().simple()));
563 }
564 }
565 }
566
567 for (idx, deal) in request.deals.iter_mut().enumerate() {
569 if deal.deal_reference.is_none() {
570 deal.deal_reference = Some(format!("D{}", idx + 1));
571 }
572 }
573
574 Ok(())
575 }
576
577 fn generate_uuidv7_ids(
579 &self,
580 request: &mut BuildRequest,
581 ) -> Result<(), super::error::BuildError> {
582 self.generate_uuid_ids(request)
585 }
586
587 fn generate_sequential_ids(
589 &self,
590 request: &mut BuildRequest,
591 ) -> Result<(), super::error::BuildError> {
592 if request.header.message_id.is_none() {
594 request.header.message_id = Some(format!("MSG_{}", chrono::Utc::now().timestamp()));
595 }
596
597 for (idx, release) in request.releases.iter_mut().enumerate() {
599 if release.release_reference.is_none() {
600 release.release_reference = Some(format!("R{}", idx + 1));
601 }
602
603 for (track_idx, track) in release.tracks.iter_mut().enumerate() {
605 if track.resource_reference.is_none() {
606 track.resource_reference = Some(format!("A{}", (idx * 1000) + track_idx + 1));
607 }
608 }
609 }
610
611 for (idx, deal) in request.deals.iter_mut().enumerate() {
613 if deal.deal_reference.is_none() {
614 deal.deal_reference = Some(format!("D{}", idx + 1));
615 }
616 }
617
618 Ok(())
619 }
620
621 fn generate_stable_hash_ids(
623 &self,
624 request: &mut BuildRequest,
625 options: &BuildOptions,
626 ) -> Result<(), super::error::BuildError> {
627 let config = options.stable_hash_config.clone().unwrap_or_default();
628 let mut id_gen = super::id_generator::StableHashGenerator::new(config);
629
630 if request.header.message_id.is_none() {
632 let sender_name = request
634 .header
635 .message_sender
636 .party_name
637 .first()
638 .map(|s| s.text.clone())
639 .unwrap_or_default();
640 let recipient_name = request
641 .header
642 .message_recipient
643 .party_name
644 .first()
645 .map(|s| s.text.clone())
646 .unwrap_or_default();
647
648 let msg_id = id_gen.generate_party_id(
649 &format!("{}-{}", sender_name, recipient_name),
650 "MessageHeader",
651 &[chrono::Utc::now().format("%Y%m%d").to_string()],
652 )?;
653 request.header.message_id = Some(msg_id);
654 }
655
656 for release in &mut request.releases {
658 if release.release_reference.is_none() {
659 let id = id_gen.generate_release_id(
660 release.upc.as_deref().unwrap_or(&release.release_id),
661 "Album",
662 &release
663 .tracks
664 .iter()
665 .map(|t| t.isrc.clone())
666 .collect::<Vec<_>>(),
667 &[], )?;
669 release.release_reference = Some(id);
670 }
671
672 for track in &mut release.tracks {
674 if track.resource_reference.is_none() {
675 let duration_seconds =
677 self.parse_duration_to_seconds(&track.duration).unwrap_or(0);
678
679 let id = id_gen.generate_resource_id(
680 &track.isrc,
681 duration_seconds,
682 None, )?;
684 track.resource_reference = Some(id);
685 }
686 }
687 }
688
689 for (_idx, deal) in request.deals.iter_mut().enumerate() {
691 if deal.deal_reference.is_none() {
692 let territories = deal.deal_terms.territory_code.join(",");
694 deal.deal_reference = Some(format!(
695 "DEAL_{}_{}",
696 deal.deal_terms.commercial_model_type, territories
697 ));
698 }
699 }
700
701 Ok(())
702 }
703
704 fn parse_duration_to_seconds(&self, duration: &str) -> Option<u32> {
706 if !duration.starts_with("PT") {
708 return None;
709 }
710
711 let mut seconds = 0u32;
712 let mut current_num = String::new();
713
714 for ch in duration[2..].chars() {
715 match ch {
716 '0'..='9' => current_num.push(ch),
717 'H' => {
718 if let Ok(hours) = current_num.parse::<u32>() {
719 seconds += hours * 3600;
720 }
721 current_num.clear();
722 }
723 'M' => {
724 if let Ok(minutes) = current_num.parse::<u32>() {
725 seconds += minutes * 60;
726 }
727 current_num.clear();
728 }
729 'S' => {
730 if let Ok(secs) = current_num.parse::<u32>() {
731 seconds += secs;
732 }
733 current_num.clear();
734 }
735 _ => {}
736 }
737 }
738
739 Some(seconds)
740 }
741
742 #[allow(dead_code)]
744 fn preflight(
745 &self,
746 request: &BuildRequest,
747 level: super::preflight::PreflightLevel,
748 ) -> Result<Vec<BuildWarning>, super::error::BuildError> {
749 let mut warnings = Vec::new();
750
751 if level == super::preflight::PreflightLevel::None {
752 return Ok(warnings);
753 }
754
755 if request.releases.is_empty() {
757 warnings.push(BuildWarning {
758 code: "NO_RELEASES".to_string(),
759 message: "No releases in request".to_string(),
760 location: Some("/releases".to_string()),
761 });
762 }
763
764 if level == super::preflight::PreflightLevel::Strict && !warnings.is_empty() {
765 return Err(super::error::BuildError::InvalidFormat {
766 field: "request".to_string(),
767 message: format!("{} validation warnings in strict mode", warnings.len()),
768 });
769 }
770
771 Ok(warnings)
772 }
773
774 pub fn diff_xml(
779 &self,
780 old_xml: &str,
781 new_xml: &str,
782 ) -> Result<super::diff::types::ChangeSet, super::error::BuildError> {
783 self.diff_xml_with_config(old_xml, new_xml, super::diff::DiffConfig::default())
784 }
785
786 pub fn diff_xml_with_config(
788 &self,
789 old_xml: &str,
790 new_xml: &str,
791 config: super::diff::DiffConfig,
792 ) -> Result<super::diff::types::ChangeSet, super::error::BuildError> {
793 let old_ast = self.parse_xml_to_ast(old_xml)?;
795 let new_ast = self.parse_xml_to_ast(new_xml)?;
796
797 let mut diff_engine = super::diff::DiffEngine::new_with_config(config);
799 diff_engine.diff(&old_ast, &new_ast)
800 }
801
802 pub fn diff_request_with_xml(
804 &self,
805 request: &BuildRequest,
806 existing_xml: &str,
807 ) -> Result<super::diff::types::ChangeSet, super::error::BuildError> {
808 let build_result = self.build(request.clone(), BuildOptions::default())?;
810
811 self.diff_xml(existing_xml, &build_result.xml)
813 }
814
815 fn parse_xml_to_ast(&self, xml: &str) -> Result<super::ast::AST, super::error::BuildError> {
817 use quick_xml::Reader;
818
819 let mut reader = Reader::from_str(xml);
820 reader.config_mut().trim_text(true);
821
822 let mut root_element = super::ast::Element::new("Root");
825 let namespace_map = indexmap::IndexMap::new();
826
827 root_element = root_element.with_text(xml);
830
831 Ok(super::ast::AST {
832 root: root_element,
833 namespaces: namespace_map,
834 schema_location: None,
835 })
836 }
837
838 pub fn create_update(
843 &self,
844 original_xml: &str,
845 updated_xml: &str,
846 original_message_id: &str,
847 ) -> Result<super::messages::UpdateReleaseMessage, super::error::BuildError> {
848 let mut update_generator = super::messages::UpdateGenerator::new();
849 update_generator.create_update(original_xml, updated_xml, original_message_id)
850 }
851
852 pub fn create_update_with_config(
854 &self,
855 original_xml: &str,
856 updated_xml: &str,
857 original_message_id: &str,
858 config: super::messages::UpdateConfig,
859 ) -> Result<super::messages::UpdateReleaseMessage, super::error::BuildError> {
860 let mut update_generator = super::messages::UpdateGenerator::new_with_config(config);
861 update_generator.create_update(original_xml, updated_xml, original_message_id)
862 }
863
864 pub fn apply_update(
869 &self,
870 base_xml: &str,
871 update: &super::messages::UpdateReleaseMessage,
872 ) -> Result<String, super::error::BuildError> {
873 let update_generator = super::messages::UpdateGenerator::new();
874 update_generator.apply_update(base_xml, update)
875 }
876
877 pub fn create_update_from_request(
882 &self,
883 existing_xml: &str,
884 request: &BuildRequest,
885 original_message_id: &str,
886 ) -> Result<super::messages::UpdateReleaseMessage, super::error::BuildError> {
887 let build_result = self.build(request.clone(), BuildOptions::default())?;
889
890 self.create_update(existing_xml, &build_result.xml, original_message_id)
892 }
893
894 pub fn validate_update(
896 &self,
897 update: &super::messages::UpdateReleaseMessage,
898 ) -> Result<super::messages::ValidationStatus, super::error::BuildError> {
899 let update_generator = super::messages::UpdateGenerator::new();
900 update_generator.validate_update(update)
901 }
902
903 pub fn serialize_update(
905 &self,
906 update: &super::messages::UpdateReleaseMessage,
907 ) -> Result<String, super::error::BuildError> {
908 self.serialize_update_message_to_xml(update)
909 }
910
911 fn serialize_update_message_to_xml(
914 &self,
915 update: &super::messages::UpdateReleaseMessage,
916 ) -> Result<String, super::error::BuildError> {
917 let mut xml = String::new();
918
919 xml.push_str(r#"<?xml version="1.0" encoding="UTF-8"?>"#);
921 xml.push('\n');
922 xml.push_str(r#"<UpdateReleaseMessage xmlns="http://ddex.net/xml/ern/43" MessageSchemaVersionId="ern/43">"#);
923 xml.push('\n');
924
925 self.serialize_update_header(&mut xml, &update.header)?;
927
928 self.serialize_update_metadata(&mut xml, &update.update_metadata)?;
930
931 self.serialize_update_list(&mut xml, &update.update_list)?;
933
934 if !update.resource_updates.is_empty() {
936 self.serialize_resource_updates(&mut xml, &update.resource_updates)?;
937 }
938
939 if !update.release_updates.is_empty() {
941 self.serialize_release_updates(&mut xml, &update.release_updates)?;
942 }
943
944 if !update.deal_updates.is_empty() {
946 self.serialize_deal_updates(&mut xml, &update.deal_updates)?;
947 }
948
949 xml.push_str("</UpdateReleaseMessage>\n");
951
952 Ok(xml)
953 }
954
955 fn serialize_update_header(
956 &self,
957 xml: &mut String,
958 header: &MessageHeaderRequest,
959 ) -> Result<(), super::error::BuildError> {
960 xml.push_str(" <MessageHeader>\n");
961
962 if let Some(ref message_id) = header.message_id {
963 xml.push_str(&format!(
964 " <MessageId>{}</MessageId>\n",
965 self.escape_xml(message_id)
966 ));
967 }
968
969 xml.push_str(" <MessageSender>\n");
971 if !header.message_sender.party_name.is_empty() {
972 xml.push_str(&format!(
973 " <PartyName>{}</PartyName>\n",
974 self.escape_xml(&header.message_sender.party_name[0].text)
975 ));
976 }
977 xml.push_str(" </MessageSender>\n");
978
979 xml.push_str(" <MessageRecipient>\n");
981 if !header.message_recipient.party_name.is_empty() {
982 xml.push_str(&format!(
983 " <PartyName>{}</PartyName>\n",
984 self.escape_xml(&header.message_recipient.party_name[0].text)
985 ));
986 }
987 xml.push_str(" </MessageRecipient>\n");
988
989 if let Some(ref created_time) = header.message_created_date_time {
991 xml.push_str(&format!(
992 " <MessageCreatedDateTime>{}</MessageCreatedDateTime>\n",
993 self.escape_xml(created_time)
994 ));
995 } else {
996 let default_time = chrono::Utc::now().to_rfc3339();
997 xml.push_str(&format!(
998 " <MessageCreatedDateTime>{}</MessageCreatedDateTime>\n",
999 self.escape_xml(&default_time)
1000 ));
1001 }
1002
1003 xml.push_str(" </MessageHeader>\n");
1004 Ok(())
1005 }
1006
1007 fn serialize_update_metadata(
1008 &self,
1009 xml: &mut String,
1010 metadata: &super::messages::UpdateMetadata,
1011 ) -> Result<(), super::error::BuildError> {
1012 xml.push_str(" <UpdateMetadata>\n");
1013 xml.push_str(&format!(
1014 " <OriginalMessageId>{}</OriginalMessageId>\n",
1015 self.escape_xml(&metadata.original_message_id)
1016 ));
1017 xml.push_str(&format!(
1018 " <UpdateSequence>{}</UpdateSequence>\n",
1019 metadata.update_sequence
1020 ));
1021 xml.push_str(&format!(
1022 " <TotalOperations>{}</TotalOperations>\n",
1023 metadata.total_operations
1024 ));
1025 xml.push_str(&format!(
1026 " <ImpactLevel>{}</ImpactLevel>\n",
1027 self.escape_xml(&metadata.impact_level)
1028 ));
1029 xml.push_str(&format!(
1030 " <ValidationStatus>{}</ValidationStatus>\n",
1031 metadata.validation_status
1032 ));
1033 xml.push_str(&format!(
1034 " <UpdateCreatedDateTime>{}</UpdateCreatedDateTime>\n",
1035 metadata.update_created_timestamp.to_rfc3339()
1036 ));
1037 xml.push_str(" </UpdateMetadata>\n");
1038 Ok(())
1039 }
1040
1041 fn serialize_update_list(
1042 &self,
1043 xml: &mut String,
1044 operations: &[super::messages::UpdateOperation],
1045 ) -> Result<(), super::error::BuildError> {
1046 xml.push_str(" <UpdateList>\n");
1047
1048 for operation in operations {
1049 xml.push_str(" <UpdateOperation>\n");
1050 xml.push_str(&format!(
1051 " <OperationId>{}</OperationId>\n",
1052 self.escape_xml(&operation.operation_id)
1053 ));
1054 xml.push_str(&format!(" <Action>{}</Action>\n", operation.action));
1055 xml.push_str(&format!(
1056 " <TargetPath>{}</TargetPath>\n",
1057 self.escape_xml(&operation.target_path)
1058 ));
1059 xml.push_str(&format!(
1060 " <EntityType>{}</EntityType>\n",
1061 operation.entity_type
1062 ));
1063 xml.push_str(&format!(
1064 " <EntityId>{}</EntityId>\n",
1065 self.escape_xml(&operation.entity_id)
1066 ));
1067
1068 if let Some(ref old_value) = operation.old_value {
1069 xml.push_str(&format!(
1070 " <OldValue>{}</OldValue>\n",
1071 self.escape_xml(old_value)
1072 ));
1073 }
1074
1075 if let Some(ref new_value) = operation.new_value {
1076 xml.push_str(&format!(
1077 " <NewValue>{}</NewValue>\n",
1078 self.escape_xml(new_value)
1079 ));
1080 }
1081
1082 xml.push_str(&format!(
1083 " <IsCritical>{}</IsCritical>\n",
1084 operation.is_critical
1085 ));
1086 xml.push_str(&format!(
1087 " <Description>{}</Description>\n",
1088 self.escape_xml(&operation.description)
1089 ));
1090
1091 if !operation.dependencies.is_empty() {
1092 xml.push_str(" <Dependencies>\n");
1093 for dependency in &operation.dependencies {
1094 xml.push_str(&format!(
1095 " <Dependency>{}</Dependency>\n",
1096 self.escape_xml(dependency)
1097 ));
1098 }
1099 xml.push_str(" </Dependencies>\n");
1100 }
1101
1102 xml.push_str(" </UpdateOperation>\n");
1103 }
1104
1105 xml.push_str(" </UpdateList>\n");
1106 Ok(())
1107 }
1108
1109 fn serialize_resource_updates(
1110 &self,
1111 xml: &mut String,
1112 resource_updates: &indexmap::IndexMap<String, super::messages::ResourceUpdate>,
1113 ) -> Result<(), super::error::BuildError> {
1114 xml.push_str(" <ResourceUpdates>\n");
1115
1116 for (resource_id, update) in resource_updates {
1117 xml.push_str(" <ResourceUpdate>\n");
1118 xml.push_str(&format!(
1119 " <ResourceId>{}</ResourceId>\n",
1120 self.escape_xml(resource_id)
1121 ));
1122 xml.push_str(&format!(
1123 " <ResourceReference>{}</ResourceReference>\n",
1124 self.escape_xml(&update.resource_reference)
1125 ));
1126 xml.push_str(&format!(" <Action>{}</Action>\n", update.action));
1127
1128 if let Some(ref data) = update.resource_data {
1130 xml.push_str(" <ResourceData>\n");
1131 xml.push_str(&format!(
1132 " <Type>{}</Type>\n",
1133 self.escape_xml(&data.resource_type)
1134 ));
1135 xml.push_str(&format!(
1136 " <Title>{}</Title>\n",
1137 self.escape_xml(&data.title)
1138 ));
1139 xml.push_str(&format!(
1140 " <Artist>{}</Artist>\n",
1141 self.escape_xml(&data.artist)
1142 ));
1143
1144 if let Some(ref isrc) = data.isrc {
1145 xml.push_str(&format!(" <ISRC>{}</ISRC>\n", self.escape_xml(isrc)));
1146 }
1147
1148 if let Some(ref duration) = data.duration {
1149 xml.push_str(&format!(
1150 " <Duration>{}</Duration>\n",
1151 self.escape_xml(duration)
1152 ));
1153 }
1154
1155 xml.push_str(" </ResourceData>\n");
1156 }
1157
1158 xml.push_str(" </ResourceUpdate>\n");
1159 }
1160
1161 xml.push_str(" </ResourceUpdates>\n");
1162 Ok(())
1163 }
1164
1165 fn serialize_release_updates(
1166 &self,
1167 xml: &mut String,
1168 release_updates: &indexmap::IndexMap<String, super::messages::ReleaseUpdate>,
1169 ) -> Result<(), super::error::BuildError> {
1170 xml.push_str(" <ReleaseUpdates>\n");
1171
1172 for (release_id, update) in release_updates {
1173 xml.push_str(" <ReleaseUpdate>\n");
1174 xml.push_str(&format!(
1175 " <ReleaseId>{}</ReleaseId>\n",
1176 self.escape_xml(release_id)
1177 ));
1178 xml.push_str(&format!(
1179 " <ReleaseReference>{}</ReleaseReference>\n",
1180 self.escape_xml(&update.release_reference)
1181 ));
1182 xml.push_str(&format!(" <Action>{}</Action>\n", update.action));
1183
1184 if let Some(ref data) = update.release_data {
1186 xml.push_str(" <ReleaseData>\n");
1187 xml.push_str(&format!(
1188 " <Type>{}</Type>\n",
1189 self.escape_xml(&data.release_type)
1190 ));
1191 xml.push_str(&format!(
1192 " <Title>{}</Title>\n",
1193 self.escape_xml(&data.title)
1194 ));
1195 xml.push_str(&format!(
1196 " <Artist>{}</Artist>\n",
1197 self.escape_xml(&data.artist)
1198 ));
1199
1200 if let Some(ref label) = data.label {
1201 xml.push_str(&format!(
1202 " <Label>{}</Label>\n",
1203 self.escape_xml(label)
1204 ));
1205 }
1206
1207 if let Some(ref upc) = data.upc {
1208 xml.push_str(&format!(" <UPC>{}</UPC>\n", self.escape_xml(upc)));
1209 }
1210
1211 xml.push_str(" </ReleaseData>\n");
1212 }
1213
1214 xml.push_str(" </ReleaseUpdate>\n");
1215 }
1216
1217 xml.push_str(" </ReleaseUpdates>\n");
1218 Ok(())
1219 }
1220
1221 fn serialize_deal_updates(
1222 &self,
1223 xml: &mut String,
1224 deal_updates: &indexmap::IndexMap<String, super::messages::DealUpdate>,
1225 ) -> Result<(), super::error::BuildError> {
1226 xml.push_str(" <DealUpdates>\n");
1227
1228 for (deal_id, update) in deal_updates {
1229 xml.push_str(" <DealUpdate>\n");
1230 xml.push_str(&format!(
1231 " <DealId>{}</DealId>\n",
1232 self.escape_xml(deal_id)
1233 ));
1234 xml.push_str(&format!(
1235 " <DealReference>{}</DealReference>\n",
1236 self.escape_xml(&update.deal_reference)
1237 ));
1238 xml.push_str(&format!(" <Action>{}</Action>\n", update.action));
1239
1240 xml.push_str(" </DealUpdate>\n");
1241 }
1242
1243 xml.push_str(" </DealUpdates>\n");
1244 Ok(())
1245 }
1246
1247 fn escape_xml(&self, text: &str) -> String {
1248 text.replace('&', "&")
1249 .replace('<', "<")
1250 .replace('>', ">")
1251 .replace('"', """)
1252 .replace('\'', "'")
1253 }
1254}
1255
1256impl Default for DDEXBuilder {
1257 fn default() -> Self {
1258 Self::new()
1259 }
1260}