1use crate::generator::{ASTGenerator, xml_writer::XmlWriter};
4use indexmap::IndexMap;
5use serde::{Deserialize, Serialize};
6pub use super::preflight::PreflightLevel;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct BuildRequest {
11 pub header: MessageHeaderRequest,
13
14 pub version: String,
16
17 pub profile: Option<String>,
19
20 pub releases: Vec<ReleaseRequest>,
22
23 pub deals: Vec<DealRequest>,
25
26 pub extensions: Option<IndexMap<String, String>>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct MessageHeaderRequest {
33 pub message_id: Option<String>,
34 pub message_sender: PartyRequest,
35 pub message_recipient: PartyRequest,
36 pub message_control_type: Option<String>,
37 pub message_created_date_time: Option<String>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct PartyRequest {
43 pub party_name: Vec<LocalizedStringRequest>,
44 pub party_id: Option<String>,
45 pub party_reference: Option<String>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct LocalizedStringRequest {
51 pub text: String,
52 pub language_code: Option<String>,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct ReleaseRequest {
58 pub release_id: String,
59 pub release_reference: Option<String>, pub title: Vec<LocalizedStringRequest>,
61 pub artist: String,
62 pub label: Option<String>, pub release_date: Option<String>, pub upc: Option<String>, pub tracks: Vec<TrackRequest>,
66 pub resource_references: Option<Vec<String>>, }
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct TrackRequest {
72 pub track_id: String, pub resource_reference: Option<String>, pub isrc: String, pub title: String,
76 pub duration: String, pub artist: String,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct DealRequest {
83 pub deal_reference: Option<String>, pub deal_terms: DealTerms, pub release_references: Vec<String>, }
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct DealTerms {
91 pub commercial_model_type: String,
92 pub territory_code: Vec<String>,
93 pub start_date: Option<String>,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct BuildOptions {
99 pub determinism: Option<super::determinism::DeterminismConfig>,
101
102 pub preflight_level: super::preflight::PreflightLevel,
104
105 pub id_strategy: IdStrategy,
107
108 pub stable_hash_config: Option<super::id_generator::StableHashConfig>,
110}
111
112impl Default for BuildOptions {
113 fn default() -> Self {
114 Self {
115 determinism: None,
116 preflight_level: super::preflight::PreflightLevel::Warn,
117 id_strategy: IdStrategy::UUID,
118 stable_hash_config: None,
119 }
120 }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
125pub enum IdStrategy {
126 UUID,
128 UUIDv7,
130 Sequential,
132 StableHash,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct BuildResult {
139 pub xml: String,
141
142 pub warnings: Vec<BuildWarning>,
144
145 pub errors: Vec<super::error::BuildError>,
147
148 pub statistics: BuildStatistics,
150
151 pub canonical_hash: Option<String>,
153
154 pub reproducibility_banner: Option<String>,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct BuildWarning {
161 pub code: String,
162 pub message: String,
163 pub location: Option<String>,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct BuildStatistics {
169 pub releases: usize,
170 pub tracks: usize,
171 pub deals: usize,
172 pub generation_time_ms: u64,
173 pub xml_size_bytes: usize,
174}
175
176impl Default for BuildStatistics {
177 fn default() -> Self {
178 Self {
179 releases: 0,
180 tracks: 0,
181 deals: 0,
182 generation_time_ms: 0,
183 xml_size_bytes: 0,
184 }
185 }
186}
187
188pub struct DDEXBuilder {
190 inner: super::Builder,
191}
192
193impl DDEXBuilder {
194 pub fn new() -> Self {
196 Self {
197 inner: super::Builder::new(),
198 }
199 }
200
201 pub fn build(&self, mut request: BuildRequest, options: BuildOptions) -> Result<BuildResult, super::error::BuildError> {
203 let start = std::time::Instant::now();
204 let mut warnings = Vec::new();
205
206 let validator = super::preflight::PreflightValidator::new(
208 super::preflight::ValidationConfig {
209 level: options.preflight_level,
210 profile: request.profile.clone(),
211 validate_identifiers: true,
212 validate_checksums: true,
213 check_required_fields: true,
214 validate_dates: true,
215 validate_references: true,
216 }
217 );
218
219 let validation_result = validator.validate(&request)?;
220
221 for warning in validation_result.warnings {
223 warnings.push(BuildWarning {
224 code: warning.code,
225 message: warning.message,
226 location: Some(warning.location),
227 });
228 }
229
230 if !validation_result.passed {
232 if options.preflight_level == super::preflight::PreflightLevel::Strict {
233 return Err(super::error::BuildError::ValidationFailed {
234 errors: validation_result.errors.iter()
235 .map(|e| format!("{}: {}", e.code, e.message))
236 .collect(),
237 });
238 }
239 }
240
241 self.generate_ids(&mut request, &options)?;
243
244 let mut generator = ASTGenerator::new(request.version.clone());
246 let ast = generator.generate(&request)?;
247
248 let config = options.determinism.unwrap_or_default();
250
251 let writer = XmlWriter::new(config.clone());
253 let xml = writer.write(&ast)?;
254
255 let (final_xml, canonical_hash) = if config.canon_mode == super::determinism::CanonMode::DbC14n {
257 let canonicalizer = super::canonical::DB_C14N::new(config.clone());
258 let canonical = canonicalizer.canonicalize(&xml)?;
259 let hash = Some(canonicalizer.canonical_hash(&canonical)?);
260 (canonical, hash)
261 } else {
262 (xml, None)
263 };
264
265 let reproducibility_banner = if config.emit_reproducibility_banner {
267 Some(format!(
268 "Generated by DDEX Builder v{} with DB-C14N/{}",
269 env!("CARGO_PKG_VERSION"),
270 super::DB_C14N_VERSION
271 ))
272 } else {
273 None
274 };
275
276 let elapsed = start.elapsed();
277
278 Ok(BuildResult {
279 xml: final_xml.clone(),
280 warnings,
281 errors: Vec::new(),
282 statistics: BuildStatistics {
283 releases: request.releases.len(),
284 tracks: request.releases.iter().map(|r| r.tracks.len()).sum(),
285 deals: request.deals.len(),
286 generation_time_ms: elapsed.as_millis() as u64,
287 xml_size_bytes: final_xml.len(),
288 },
289 canonical_hash,
290 reproducibility_banner,
291 })
292 }
293
294 fn generate_ids(&self, request: &mut BuildRequest, options: &BuildOptions) -> Result<(), super::error::BuildError> {
296 match options.id_strategy {
297 IdStrategy::UUID => {
298 self.generate_uuid_ids(request)?;
299 },
300 IdStrategy::UUIDv7 => {
301 self.generate_uuidv7_ids(request)?;
302 },
303 IdStrategy::Sequential => {
304 self.generate_sequential_ids(request)?;
305 },
306 IdStrategy::StableHash => {
307 self.generate_stable_hash_ids(request, options)?;
308 },
309 }
310 Ok(())
311 }
312
313 fn generate_uuid_ids(&self, request: &mut BuildRequest) -> Result<(), super::error::BuildError> {
315 use uuid::Uuid;
316
317 if request.header.message_id.is_none() {
319 request.header.message_id = Some(format!("MSG_{}", Uuid::new_v4()));
320 }
321
322 for release in &mut request.releases {
324 if release.release_reference.is_none() {
325 release.release_reference = Some(format!("R{}", Uuid::new_v4().simple()));
326 }
327
328 for track in &mut release.tracks {
330 if track.resource_reference.is_none() {
331 track.resource_reference = Some(format!("A{}", Uuid::new_v4().simple()));
332 }
333 }
334 }
335
336 for (idx, deal) in request.deals.iter_mut().enumerate() {
338 if deal.deal_reference.is_none() {
339 deal.deal_reference = Some(format!("D{}", idx + 1));
340 }
341 }
342
343 Ok(())
344 }
345
346 fn generate_uuidv7_ids(&self, request: &mut BuildRequest) -> Result<(), super::error::BuildError> {
348 self.generate_uuid_ids(request)
351 }
352
353 fn generate_sequential_ids(&self, request: &mut BuildRequest) -> Result<(), super::error::BuildError> {
355 if request.header.message_id.is_none() {
357 request.header.message_id = Some(format!("MSG_{}", chrono::Utc::now().timestamp()));
358 }
359
360 for (idx, release) in request.releases.iter_mut().enumerate() {
362 if release.release_reference.is_none() {
363 release.release_reference = Some(format!("R{}", idx + 1));
364 }
365
366 for (track_idx, track) in release.tracks.iter_mut().enumerate() {
368 if track.resource_reference.is_none() {
369 track.resource_reference = Some(format!("A{}", (idx * 1000) + track_idx + 1));
370 }
371 }
372 }
373
374 for (idx, deal) in request.deals.iter_mut().enumerate() {
376 if deal.deal_reference.is_none() {
377 deal.deal_reference = Some(format!("D{}", idx + 1));
378 }
379 }
380
381 Ok(())
382 }
383
384 fn generate_stable_hash_ids(&self, request: &mut BuildRequest, options: &BuildOptions) -> Result<(), super::error::BuildError> {
386 let config = options.stable_hash_config.clone()
387 .unwrap_or_default();
388 let mut id_gen = super::id_generator::StableHashGenerator::new(config);
389
390 if request.header.message_id.is_none() {
392 let sender_name = request.header.message_sender.party_name
394 .first()
395 .map(|s| s.text.clone())
396 .unwrap_or_default();
397 let recipient_name = request.header.message_recipient.party_name
398 .first()
399 .map(|s| s.text.clone())
400 .unwrap_or_default();
401
402 let msg_id = id_gen.generate_party_id(
403 &format!("{}-{}", sender_name, recipient_name),
404 "MessageHeader",
405 &[chrono::Utc::now().format("%Y%m%d").to_string()],
406 )?;
407 request.header.message_id = Some(msg_id);
408 }
409
410 for release in &mut request.releases {
412 if release.release_reference.is_none() {
413 let id = id_gen.generate_release_id(
414 release.upc.as_deref().unwrap_or(&release.release_id),
415 "Album",
416 &release.tracks.iter()
417 .map(|t| t.isrc.clone())
418 .collect::<Vec<_>>(),
419 &[], )?;
421 release.release_reference = Some(id);
422 }
423
424 for track in &mut release.tracks {
426 if track.resource_reference.is_none() {
427 let duration_seconds = self.parse_duration_to_seconds(&track.duration)
429 .unwrap_or(0);
430
431 let id = id_gen.generate_resource_id(
432 &track.isrc,
433 duration_seconds,
434 None, )?;
436 track.resource_reference = Some(id);
437 }
438 }
439 }
440
441 for (_idx, deal) in request.deals.iter_mut().enumerate() {
443 if deal.deal_reference.is_none() {
444 let territories = deal.deal_terms.territory_code.join(",");
446 deal.deal_reference = Some(format!("DEAL_{}_{}",
447 deal.deal_terms.commercial_model_type,
448 territories));
449 }
450 }
451
452 Ok(())
453 }
454
455 fn parse_duration_to_seconds(&self, duration: &str) -> Option<u32> {
457 if !duration.starts_with("PT") {
459 return None;
460 }
461
462 let mut seconds = 0u32;
463 let mut current_num = String::new();
464
465 for ch in duration[2..].chars() {
466 match ch {
467 '0'..='9' => current_num.push(ch),
468 'H' => {
469 if let Ok(hours) = current_num.parse::<u32>() {
470 seconds += hours * 3600;
471 }
472 current_num.clear();
473 },
474 'M' => {
475 if let Ok(minutes) = current_num.parse::<u32>() {
476 seconds += minutes * 60;
477 }
478 current_num.clear();
479 },
480 'S' => {
481 if let Ok(secs) = current_num.parse::<u32>() {
482 seconds += secs;
483 }
484 current_num.clear();
485 },
486 _ => {}
487 }
488 }
489
490 Some(seconds)
491 }
492
493 fn preflight(&self, request: &BuildRequest, level: super::preflight::PreflightLevel) -> Result<Vec<BuildWarning>, super::error::BuildError> {
495 let mut warnings = Vec::new();
496
497 if level == super::preflight::PreflightLevel::None {
498 return Ok(warnings);
499 }
500
501 if request.releases.is_empty() {
503 warnings.push(BuildWarning {
504 code: "NO_RELEASES".to_string(),
505 message: "No releases in request".to_string(),
506 location: Some("/releases".to_string()),
507 });
508 }
509
510 if level == super::preflight::PreflightLevel::Strict && !warnings.is_empty() {
511 return Err(super::error::BuildError::InvalidFormat {
512 field: "request".to_string(),
513 message: format!("{} validation warnings in strict mode", warnings.len()),
514 });
515 }
516
517 Ok(warnings)
518 }
519
520 pub fn diff_xml(&self, old_xml: &str, new_xml: &str) -> Result<super::diff::types::ChangeSet, super::error::BuildError> {
525 self.diff_xml_with_config(old_xml, new_xml, super::diff::DiffConfig::default())
526 }
527
528 pub fn diff_xml_with_config(
530 &self,
531 old_xml: &str,
532 new_xml: &str,
533 config: super::diff::DiffConfig
534 ) -> Result<super::diff::types::ChangeSet, super::error::BuildError> {
535 let old_ast = self.parse_xml_to_ast(old_xml)?;
537 let new_ast = self.parse_xml_to_ast(new_xml)?;
538
539 let mut diff_engine = super::diff::DiffEngine::new_with_config(config);
541 diff_engine.diff(&old_ast, &new_ast)
542 }
543
544 pub fn diff_request_with_xml(
546 &self,
547 request: &BuildRequest,
548 existing_xml: &str
549 ) -> Result<super::diff::types::ChangeSet, super::error::BuildError> {
550 let build_result = self.build(request.clone(), BuildOptions::default())?;
552
553 self.diff_xml(existing_xml, &build_result.xml)
555 }
556
557 fn parse_xml_to_ast(&self, xml: &str) -> Result<super::ast::AST, super::error::BuildError> {
559 use quick_xml::Reader;
560
561 let mut reader = Reader::from_str(xml);
562 reader.config_mut().trim_text(true);
563
564 let mut root_element = super::ast::Element::new("Root");
567 let namespace_map = indexmap::IndexMap::new();
568
569 root_element = root_element.with_text(xml);
572
573 Ok(super::ast::AST {
574 root: root_element,
575 namespaces: namespace_map,
576 schema_location: None,
577 })
578 }
579
580 pub fn create_update(
585 &self,
586 original_xml: &str,
587 updated_xml: &str,
588 original_message_id: &str,
589 ) -> Result<super::messages::UpdateReleaseMessage, super::error::BuildError> {
590 let mut update_generator = super::messages::UpdateGenerator::new();
591 update_generator.create_update(original_xml, updated_xml, original_message_id)
592 }
593
594 pub fn create_update_with_config(
596 &self,
597 original_xml: &str,
598 updated_xml: &str,
599 original_message_id: &str,
600 config: super::messages::UpdateConfig,
601 ) -> Result<super::messages::UpdateReleaseMessage, super::error::BuildError> {
602 let mut update_generator = super::messages::UpdateGenerator::new_with_config(config);
603 update_generator.create_update(original_xml, updated_xml, original_message_id)
604 }
605
606 pub fn apply_update(
611 &self,
612 base_xml: &str,
613 update: &super::messages::UpdateReleaseMessage,
614 ) -> Result<String, super::error::BuildError> {
615 let update_generator = super::messages::UpdateGenerator::new();
616 update_generator.apply_update(base_xml, update)
617 }
618
619 pub fn create_update_from_request(
624 &self,
625 existing_xml: &str,
626 request: &BuildRequest,
627 original_message_id: &str,
628 ) -> Result<super::messages::UpdateReleaseMessage, super::error::BuildError> {
629 let build_result = self.build(request.clone(), BuildOptions::default())?;
631
632 self.create_update(existing_xml, &build_result.xml, original_message_id)
634 }
635
636 pub fn validate_update(
638 &self,
639 update: &super::messages::UpdateReleaseMessage,
640 ) -> Result<super::messages::ValidationStatus, super::error::BuildError> {
641 let update_generator = super::messages::UpdateGenerator::new();
642 update_generator.validate_update(update)
643 }
644
645 pub fn serialize_update(
647 &self,
648 update: &super::messages::UpdateReleaseMessage,
649 ) -> Result<String, super::error::BuildError> {
650 self.serialize_update_message_to_xml(update)
651 }
652
653 fn serialize_update_message_to_xml(
656 &self,
657 update: &super::messages::UpdateReleaseMessage,
658 ) -> Result<String, super::error::BuildError> {
659 let mut xml = String::new();
660
661 xml.push_str(r#"<?xml version="1.0" encoding="UTF-8"?>"#);
663 xml.push('\n');
664 xml.push_str(r#"<UpdateReleaseMessage xmlns="http://ddex.net/xml/ern/43" MessageSchemaVersionId="ern/43">"#);
665 xml.push('\n');
666
667 self.serialize_update_header(&mut xml, &update.header)?;
669
670 self.serialize_update_metadata(&mut xml, &update.update_metadata)?;
672
673 self.serialize_update_list(&mut xml, &update.update_list)?;
675
676 if !update.resource_updates.is_empty() {
678 self.serialize_resource_updates(&mut xml, &update.resource_updates)?;
679 }
680
681 if !update.release_updates.is_empty() {
683 self.serialize_release_updates(&mut xml, &update.release_updates)?;
684 }
685
686 if !update.deal_updates.is_empty() {
688 self.serialize_deal_updates(&mut xml, &update.deal_updates)?;
689 }
690
691 xml.push_str("</UpdateReleaseMessage>\n");
693
694 Ok(xml)
695 }
696
697 fn serialize_update_header(
698 &self,
699 xml: &mut String,
700 header: &MessageHeaderRequest,
701 ) -> Result<(), super::error::BuildError> {
702 xml.push_str(" <MessageHeader>\n");
703
704 if let Some(ref message_id) = header.message_id {
705 xml.push_str(&format!(" <MessageId>{}</MessageId>\n", self.escape_xml(message_id)));
706 }
707
708 xml.push_str(" <MessageSender>\n");
710 if !header.message_sender.party_name.is_empty() {
711 xml.push_str(&format!(" <PartyName>{}</PartyName>\n",
712 self.escape_xml(&header.message_sender.party_name[0].text)));
713 }
714 xml.push_str(" </MessageSender>\n");
715
716 xml.push_str(" <MessageRecipient>\n");
718 if !header.message_recipient.party_name.is_empty() {
719 xml.push_str(&format!(" <PartyName>{}</PartyName>\n",
720 self.escape_xml(&header.message_recipient.party_name[0].text)));
721 }
722 xml.push_str(" </MessageRecipient>\n");
723
724 if let Some(ref created_time) = header.message_created_date_time {
726 xml.push_str(&format!(" <MessageCreatedDateTime>{}</MessageCreatedDateTime>\n",
727 self.escape_xml(created_time)));
728 } else {
729 let default_time = chrono::Utc::now().to_rfc3339();
730 xml.push_str(&format!(" <MessageCreatedDateTime>{}</MessageCreatedDateTime>\n",
731 self.escape_xml(&default_time)));
732 }
733
734 xml.push_str(" </MessageHeader>\n");
735 Ok(())
736 }
737
738 fn serialize_update_metadata(
739 &self,
740 xml: &mut String,
741 metadata: &super::messages::UpdateMetadata,
742 ) -> Result<(), super::error::BuildError> {
743 xml.push_str(" <UpdateMetadata>\n");
744 xml.push_str(&format!(" <OriginalMessageId>{}</OriginalMessageId>\n",
745 self.escape_xml(&metadata.original_message_id)));
746 xml.push_str(&format!(" <UpdateSequence>{}</UpdateSequence>\n", metadata.update_sequence));
747 xml.push_str(&format!(" <TotalOperations>{}</TotalOperations>\n", metadata.total_operations));
748 xml.push_str(&format!(" <ImpactLevel>{}</ImpactLevel>\n",
749 self.escape_xml(&metadata.impact_level)));
750 xml.push_str(&format!(" <ValidationStatus>{}</ValidationStatus>\n", metadata.validation_status));
751 xml.push_str(&format!(" <UpdateCreatedDateTime>{}</UpdateCreatedDateTime>\n",
752 metadata.update_created_timestamp.to_rfc3339()));
753 xml.push_str(" </UpdateMetadata>\n");
754 Ok(())
755 }
756
757 fn serialize_update_list(
758 &self,
759 xml: &mut String,
760 operations: &[super::messages::UpdateOperation],
761 ) -> Result<(), super::error::BuildError> {
762 xml.push_str(" <UpdateList>\n");
763
764 for operation in operations {
765 xml.push_str(" <UpdateOperation>\n");
766 xml.push_str(&format!(" <OperationId>{}</OperationId>\n",
767 self.escape_xml(&operation.operation_id)));
768 xml.push_str(&format!(" <Action>{}</Action>\n", operation.action));
769 xml.push_str(&format!(" <TargetPath>{}</TargetPath>\n",
770 self.escape_xml(&operation.target_path)));
771 xml.push_str(&format!(" <EntityType>{}</EntityType>\n", operation.entity_type));
772 xml.push_str(&format!(" <EntityId>{}</EntityId>\n",
773 self.escape_xml(&operation.entity_id)));
774
775 if let Some(ref old_value) = operation.old_value {
776 xml.push_str(&format!(" <OldValue>{}</OldValue>\n",
777 self.escape_xml(old_value)));
778 }
779
780 if let Some(ref new_value) = operation.new_value {
781 xml.push_str(&format!(" <NewValue>{}</NewValue>\n",
782 self.escape_xml(new_value)));
783 }
784
785 xml.push_str(&format!(" <IsCritical>{}</IsCritical>\n", operation.is_critical));
786 xml.push_str(&format!(" <Description>{}</Description>\n",
787 self.escape_xml(&operation.description)));
788
789 if !operation.dependencies.is_empty() {
790 xml.push_str(" <Dependencies>\n");
791 for dependency in &operation.dependencies {
792 xml.push_str(&format!(" <Dependency>{}</Dependency>\n",
793 self.escape_xml(dependency)));
794 }
795 xml.push_str(" </Dependencies>\n");
796 }
797
798 xml.push_str(" </UpdateOperation>\n");
799 }
800
801 xml.push_str(" </UpdateList>\n");
802 Ok(())
803 }
804
805 fn serialize_resource_updates(
806 &self,
807 xml: &mut String,
808 resource_updates: &indexmap::IndexMap<String, super::messages::ResourceUpdate>,
809 ) -> Result<(), super::error::BuildError> {
810 xml.push_str(" <ResourceUpdates>\n");
811
812 for (resource_id, update) in resource_updates {
813 xml.push_str(" <ResourceUpdate>\n");
814 xml.push_str(&format!(" <ResourceId>{}</ResourceId>\n",
815 self.escape_xml(resource_id)));
816 xml.push_str(&format!(" <ResourceReference>{}</ResourceReference>\n",
817 self.escape_xml(&update.resource_reference)));
818 xml.push_str(&format!(" <Action>{}</Action>\n", update.action));
819
820 if let Some(ref data) = update.resource_data {
822 xml.push_str(" <ResourceData>\n");
823 xml.push_str(&format!(" <Type>{}</Type>\n",
824 self.escape_xml(&data.resource_type)));
825 xml.push_str(&format!(" <Title>{}</Title>\n",
826 self.escape_xml(&data.title)));
827 xml.push_str(&format!(" <Artist>{}</Artist>\n",
828 self.escape_xml(&data.artist)));
829
830 if let Some(ref isrc) = data.isrc {
831 xml.push_str(&format!(" <ISRC>{}</ISRC>\n",
832 self.escape_xml(isrc)));
833 }
834
835 if let Some(ref duration) = data.duration {
836 xml.push_str(&format!(" <Duration>{}</Duration>\n",
837 self.escape_xml(duration)));
838 }
839
840 xml.push_str(" </ResourceData>\n");
841 }
842
843 xml.push_str(" </ResourceUpdate>\n");
844 }
845
846 xml.push_str(" </ResourceUpdates>\n");
847 Ok(())
848 }
849
850 fn serialize_release_updates(
851 &self,
852 xml: &mut String,
853 release_updates: &indexmap::IndexMap<String, super::messages::ReleaseUpdate>,
854 ) -> Result<(), super::error::BuildError> {
855 xml.push_str(" <ReleaseUpdates>\n");
856
857 for (release_id, update) in release_updates {
858 xml.push_str(" <ReleaseUpdate>\n");
859 xml.push_str(&format!(" <ReleaseId>{}</ReleaseId>\n",
860 self.escape_xml(release_id)));
861 xml.push_str(&format!(" <ReleaseReference>{}</ReleaseReference>\n",
862 self.escape_xml(&update.release_reference)));
863 xml.push_str(&format!(" <Action>{}</Action>\n", update.action));
864
865 if let Some(ref data) = update.release_data {
867 xml.push_str(" <ReleaseData>\n");
868 xml.push_str(&format!(" <Type>{}</Type>\n",
869 self.escape_xml(&data.release_type)));
870 xml.push_str(&format!(" <Title>{}</Title>\n",
871 self.escape_xml(&data.title)));
872 xml.push_str(&format!(" <Artist>{}</Artist>\n",
873 self.escape_xml(&data.artist)));
874
875 if let Some(ref label) = data.label {
876 xml.push_str(&format!(" <Label>{}</Label>\n",
877 self.escape_xml(label)));
878 }
879
880 if let Some(ref upc) = data.upc {
881 xml.push_str(&format!(" <UPC>{}</UPC>\n",
882 self.escape_xml(upc)));
883 }
884
885 xml.push_str(" </ReleaseData>\n");
886 }
887
888 xml.push_str(" </ReleaseUpdate>\n");
889 }
890
891 xml.push_str(" </ReleaseUpdates>\n");
892 Ok(())
893 }
894
895 fn serialize_deal_updates(
896 &self,
897 xml: &mut String,
898 deal_updates: &indexmap::IndexMap<String, super::messages::DealUpdate>,
899 ) -> Result<(), super::error::BuildError> {
900 xml.push_str(" <DealUpdates>\n");
901
902 for (deal_id, update) in deal_updates {
903 xml.push_str(" <DealUpdate>\n");
904 xml.push_str(&format!(" <DealId>{}</DealId>\n",
905 self.escape_xml(deal_id)));
906 xml.push_str(&format!(" <DealReference>{}</DealReference>\n",
907 self.escape_xml(&update.deal_reference)));
908 xml.push_str(&format!(" <Action>{}</Action>\n", update.action));
909
910 xml.push_str(" </DealUpdate>\n");
911 }
912
913 xml.push_str(" </DealUpdates>\n");
914 Ok(())
915 }
916
917 fn escape_xml(&self, text: &str) -> String {
918 text.replace('&', "&")
919 .replace('<', "<")
920 .replace('>', ">")
921 .replace('"', """)
922 .replace('\'', "'")
923 }
924}
925
926impl Default for DDEXBuilder {
927 fn default() -> Self {
928 Self::new()
929 }
930}