1use super::*;
7use crate::presets::DdexVersion;
8
9pub fn get_version_spec() -> VersionSpec {
11 let mut element_mappings = IndexMap::new();
12 let mut namespace_prefixes = IndexMap::new();
13
14 namespace_prefixes.insert("ern".to_string(), "http://ddex.net/xml/ern/42".to_string());
16 namespace_prefixes.insert("avs".to_string(), "http://ddex.net/xml/avs/avs".to_string());
17
18 element_mappings.insert(
20 "NewReleaseMessage".to_string(),
21 "NewReleaseMessage".to_string(),
22 );
23 element_mappings.insert("MessageHeader".to_string(), "MessageHeader".to_string());
24 element_mappings.insert("MessageId".to_string(), "MessageId".to_string());
25 element_mappings.insert("MessageSender".to_string(), "MessageSender".to_string());
26 element_mappings.insert(
27 "MessageRecipient".to_string(),
28 "MessageRecipient".to_string(),
29 );
30 element_mappings.insert(
31 "MessageCreatedDateTime".to_string(),
32 "MessageCreatedDateTime".to_string(),
33 );
34 element_mappings.insert(
35 "MessageControlType".to_string(),
36 "MessageControlType".to_string(),
37 );
38
39 element_mappings.insert("ResourceList".to_string(), "ResourceList".to_string());
41 element_mappings.insert("SoundRecording".to_string(), "SoundRecording".to_string());
42 element_mappings.insert(
43 "ResourceReference".to_string(),
44 "ResourceReference".to_string(),
45 );
46 element_mappings.insert("ResourceId".to_string(), "ResourceId".to_string());
47 element_mappings.insert("ReferenceTitle".to_string(), "ReferenceTitle".to_string());
48 element_mappings.insert("DisplayArtist".to_string(), "DisplayArtist".to_string());
49 element_mappings.insert("ISRC".to_string(), "ISRC".to_string());
50 element_mappings.insert("Duration".to_string(), "Duration".to_string());
51 element_mappings.insert(
52 "TechnicalResourceDetails".to_string(),
53 "TechnicalResourceDetails".to_string(),
54 );
55 element_mappings.insert("AudioCodecType".to_string(), "AudioCodecType".to_string());
56 element_mappings.insert("BitRate".to_string(), "BitRate".to_string());
57 element_mappings.insert("FileName".to_string(), "FileName".to_string());
58
59 element_mappings.insert("ReleaseList".to_string(), "ReleaseList".to_string());
61 element_mappings.insert("Release".to_string(), "Release".to_string());
62 element_mappings.insert(
63 "ReleaseReference".to_string(),
64 "ReleaseReference".to_string(),
65 );
66 element_mappings.insert("ReleaseId".to_string(), "ReleaseId".to_string());
67 element_mappings.insert("ReleaseType".to_string(), "ReleaseType".to_string());
68 element_mappings.insert("Title".to_string(), "Title".to_string());
69 element_mappings.insert("DisplayArtist".to_string(), "DisplayArtist".to_string());
70 element_mappings.insert("LabelName".to_string(), "LabelName".to_string());
71 element_mappings.insert("UPC".to_string(), "UPC".to_string());
72 element_mappings.insert("ReleaseDate".to_string(), "ReleaseDate".to_string());
73 element_mappings.insert("Genre".to_string(), "Genre".to_string());
74 element_mappings.insert("ResourceGroup".to_string(), "ResourceGroup".to_string());
75
76 element_mappings.insert("DealList".to_string(), "DealList".to_string());
78 element_mappings.insert("ReleaseDeal".to_string(), "ReleaseDeal".to_string());
79 element_mappings.insert("DealReference".to_string(), "DealReference".to_string());
80 element_mappings.insert("DealTerms".to_string(), "DealTerms".to_string());
81 element_mappings.insert(
82 "CommercialModelType".to_string(),
83 "CommercialModelType".to_string(),
84 );
85 element_mappings.insert("Territory".to_string(), "Territory".to_string());
86 element_mappings.insert("TerritoryCode".to_string(), "TerritoryCode".to_string());
87 element_mappings.insert("ValidityPeriod".to_string(), "ValidityPeriod".to_string());
88 element_mappings.insert("StartDate".to_string(), "StartDate".to_string());
89 element_mappings.insert("EndDate".to_string(), "EndDate".to_string());
90 element_mappings.insert("Price".to_string(), "Price".to_string());
91 element_mappings.insert("PriceAmount".to_string(), "PriceAmount".to_string());
92 element_mappings.insert(
93 "PriceCurrencyCode".to_string(),
94 "PriceCurrencyCode".to_string(),
95 );
96
97 element_mappings.insert("PartyId".to_string(), "PartyId".to_string());
99 element_mappings.insert("PartyReference".to_string(), "PartyReference".to_string());
100 element_mappings.insert("DetailedHashSum".to_string(), "DetailedHashSum".to_string());
101 element_mappings.insert("PreviewDetails".to_string(), "PreviewDetails".to_string());
102 element_mappings.insert("UsageType".to_string(), "UsageType".to_string());
103
104 VersionSpec {
105 version: DdexVersion::Ern42,
106 namespace: "http://ddex.net/xml/ern/42".to_string(),
107 schema_location: Some(
108 "http://ddex.net/xml/ern/42 http://ddex.net/xml/ern/42/release-notification.xsd"
109 .to_string(),
110 ),
111 message_schema_version_id: "ern/42".to_string(),
112 supported_message_types: vec![
113 "NewReleaseMessage".to_string(),
114 "UpdateReleaseMessage".to_string(),
115 "CatalogTransferMessage".to_string(),
116 "PurgeReleaseMessage".to_string(),
117 ],
118 element_mappings,
119 required_elements: vec![
120 "MessageId".to_string(),
121 "MessageSender".to_string(),
122 "MessageRecipient".to_string(),
123 "MessageCreatedDateTime".to_string(),
124 "ResourceList".to_string(),
125 "ReleaseList".to_string(),
126 ],
127 deprecated_elements: vec![
128 "LegacyTechnicalDetails".to_string(),
130 "BasicPrice".to_string(),
131 "SimpleTerritory".to_string(),
132 ],
133 new_elements: vec![
134 "MessageControlType".to_string(),
136 "PartyId".to_string(),
137 "PartyReference".to_string(),
138 "TechnicalResourceDetails".to_string(),
139 "AudioCodecType".to_string(),
140 "BitRate".to_string(),
141 "FileName".to_string(),
142 "DetailedHashSum".to_string(),
143 "PreviewDetails".to_string(),
144 "UsageType".to_string(),
145 "ReleaseReference".to_string(),
146 "DealReference".to_string(),
147 "Territory".to_string(),
148 "EndDate".to_string(),
149 ],
150 namespace_prefixes,
151 }
152}
153
154pub struct Ern42Constraints {
156 pub max_resources_per_release: usize,
158 pub supported_image_formats: Vec<String>,
160 pub supported_audio_formats: Vec<String>,
162 pub max_deal_terms: usize,
164 pub enhanced_validation: bool,
166}
167
168impl Default for Ern42Constraints {
169 fn default() -> Self {
170 Self {
171 max_resources_per_release: 500,
172 supported_image_formats: vec![
173 "JPEG".to_string(),
174 "PNG".to_string(),
175 "GIF".to_string(),
176 "TIFF".to_string(),
177 ],
178 supported_audio_formats: vec![
179 "MP3".to_string(),
180 "WAV".to_string(),
181 "FLAC".to_string(),
182 "AAC".to_string(),
183 "OGG".to_string(),
184 ],
185 max_deal_terms: 50,
186 enhanced_validation: true,
187 }
188 }
189}
190
191pub fn get_namespace_mappings() -> IndexMap<String, String> {
193 let mut mappings = IndexMap::new();
194
195 mappings.insert("ern".to_string(), "http://ddex.net/xml/ern/42".to_string());
196 mappings.insert("avs".to_string(), "http://ddex.net/xml/avs/avs".to_string());
197 mappings.insert("drm".to_string(), "http://ddex.net/xml/drm/drm".to_string());
198 mappings.insert("mv".to_string(), "http://ddex.net/xml/mv/mv".to_string());
199
200 mappings
201}
202
203pub fn get_xml_template() -> &'static str {
205 r#"<?xml version="1.0" encoding="UTF-8"?>
206<NewReleaseMessage xmlns="http://ddex.net/xml/ern/42"
207 xmlns:avs="http://ddex.net/xml/avs/avs"
208 MessageSchemaVersionId="ern/42">
209 <MessageHeader>
210 <MessageId>{message_id}</MessageId>
211 <MessageSender>
212 <PartyName>{sender_name}</PartyName>
213 <PartyId>{sender_id}</PartyId>
214 </MessageSender>
215 <MessageRecipient>
216 <PartyName>{recipient_name}</PartyName>
217 <PartyId>{recipient_id}</PartyId>
218 </MessageRecipient>
219 <MessageControlType>{control_type}</MessageControlType>
220 <MessageCreatedDateTime>{created_datetime}</MessageCreatedDateTime>
221 </MessageHeader>
222
223 <ResourceList>
224 <!-- Enhanced resources will be populated here -->
225 </ResourceList>
226
227 <ReleaseList>
228 <!-- Enhanced releases will be populated here -->
229 </ReleaseList>
230
231 <DealList>
232 <!-- Enhanced deals will be populated here -->
233 </DealList>
234</NewReleaseMessage>"#
235}
236
237pub mod builders {
239
240 use crate::ast::Element;
241
242 pub fn build_enhanced_message_header(
244 message_id: &str,
245 sender_name: &str,
246 sender_id: Option<&str>,
247 recipient_name: &str,
248 recipient_id: Option<&str>,
249 control_type: Option<&str>,
250 created_datetime: &str,
251 ) -> Element {
252 let mut header = Element::new("MessageHeader");
253
254 let mut msg_id = Element::new("MessageId");
256 msg_id.add_text(message_id);
257 header.add_child(msg_id);
258
259 let mut sender = Element::new("MessageSender");
261 let mut sender_party = Element::new("PartyName");
262 sender_party.add_text(sender_name);
263 sender.add_child(sender_party);
264
265 if let Some(sid) = sender_id {
266 let mut sender_id_elem = Element::new("PartyId");
267 sender_id_elem.add_text(sid);
268 sender.add_child(sender_id_elem);
269 }
270 header.add_child(sender);
271
272 let mut recipient = Element::new("MessageRecipient");
274 let mut recipient_party = Element::new("PartyName");
275 recipient_party.add_text(recipient_name);
276 recipient.add_child(recipient_party);
277
278 if let Some(rid) = recipient_id {
279 let mut recipient_id_elem = Element::new("PartyId");
280 recipient_id_elem.add_text(rid);
281 recipient.add_child(recipient_id_elem);
282 }
283 header.add_child(recipient);
284
285 if let Some(control) = control_type {
287 let mut control_elem = Element::new("MessageControlType");
288 control_elem.add_text(control);
289 header.add_child(control_elem);
290 }
291
292 let mut created = Element::new("MessageCreatedDateTime");
294 created.add_text(created_datetime);
295 header.add_child(created);
296
297 header
298 }
299
300 pub fn build_enhanced_sound_recording(
302 resource_ref: &str,
303 resource_id: &str,
304 title: &str,
305 artist: &str,
306 isrc: &str,
307 duration: &str,
308 file_name: Option<&str>,
309 codec: Option<&str>,
310 bit_rate: Option<u32>,
311 ) -> Element {
312 let mut sound_recording = Element::new("SoundRecording");
313
314 let mut res_ref = Element::new("ResourceReference");
316 res_ref.add_text(resource_ref);
317 sound_recording.add_child(res_ref);
318
319 let mut res_type = Element::new("Type");
321 res_type.add_text("SoundRecording");
322 sound_recording.add_child(res_type);
323
324 let mut res_id = Element::new("ResourceId");
326 res_id.add_text(resource_id);
327 sound_recording.add_child(res_id);
328
329 let mut ref_title = Element::new("ReferenceTitle");
331 ref_title.add_text(title);
332 sound_recording.add_child(ref_title);
333
334 let mut display_artist = Element::new("DisplayArtist");
336 display_artist.add_text(artist);
337 sound_recording.add_child(display_artist);
338
339 let mut isrc_elem = Element::new("ISRC");
341 isrc_elem.add_text(isrc);
342 sound_recording.add_child(isrc_elem);
343
344 let mut duration_elem = Element::new("Duration");
346 duration_elem.add_text(duration);
347 sound_recording.add_child(duration_elem);
348
349 if file_name.is_some() || codec.is_some() || bit_rate.is_some() {
351 let mut tech_details = Element::new("TechnicalResourceDetails");
352
353 if let Some(fname) = file_name {
354 let mut file_elem = Element::new("FileName");
355 file_elem.add_text(fname);
356 tech_details.add_child(file_elem);
357 }
358
359 if let Some(codec_type) = codec {
360 let mut codec_elem = Element::new("AudioCodecType");
361 codec_elem.add_text(codec_type);
362 tech_details.add_child(codec_elem);
363 }
364
365 if let Some(bitrate) = bit_rate {
366 let mut bitrate_elem = Element::new("BitRate");
367 bitrate_elem.add_text(&bitrate.to_string());
368 tech_details.add_child(bitrate_elem);
369 }
370
371 sound_recording.add_child(tech_details);
372 }
373
374 sound_recording
375 }
376
377 pub fn build_enhanced_release(
379 release_ref: &str,
380 release_id: &str,
381 release_type: &str,
382 title: &str,
383 artist: &str,
384 label: &str,
385 upc: Option<&str>,
386 release_date: Option<&str>,
387 genre: Option<&str>,
388 resource_refs: &[String],
389 ) -> Element {
390 let mut release = Element::new("Release");
391
392 let mut rel_ref = Element::new("ReleaseReference");
394 rel_ref.add_text(release_ref);
395 release.add_child(rel_ref);
396
397 let mut rel_id = Element::new("ReleaseId");
399 rel_id.add_text(release_id);
400 release.add_child(rel_id);
401
402 let mut rel_type = Element::new("ReleaseType");
404 rel_type.add_text(release_type);
405 release.add_child(rel_type);
406
407 let mut title_elem = Element::new("Title");
409 title_elem.add_text(title);
410 release.add_child(title_elem);
411
412 let mut artist_elem = Element::new("DisplayArtist");
414 artist_elem.add_text(artist);
415 release.add_child(artist_elem);
416
417 let mut label_elem = Element::new("LabelName");
419 label_elem.add_text(label);
420 release.add_child(label_elem);
421
422 if let Some(upc_val) = upc {
424 let mut upc_elem = Element::new("UPC");
425 upc_elem.add_text(upc_val);
426 release.add_child(upc_elem);
427 }
428
429 if let Some(date) = release_date {
431 let mut date_elem = Element::new("ReleaseDate");
432 date_elem.add_text(date);
433 release.add_child(date_elem);
434 }
435
436 if let Some(genre_val) = genre {
438 let mut genre_elem = Element::new("Genre");
439 genre_elem.add_text(genre_val);
440 release.add_child(genre_elem);
441 }
442
443 if !resource_refs.is_empty() {
445 let mut resource_group = Element::new("ResourceGroup");
446 for res_ref in resource_refs {
447 let mut ref_elem = Element::new("ResourceReference");
448 ref_elem.add_text(res_ref);
449 resource_group.add_child(ref_elem);
450 }
451 release.add_child(resource_group);
452 }
453
454 release
455 }
456
457 pub fn build_enhanced_deal(
459 deal_ref: &str,
460 commercial_model: &str,
461 territories: &[String],
462 start_date: Option<&str>,
463 end_date: Option<&str>,
464 price: Option<f64>,
465 currency: Option<&str>,
466 usage_type: Option<&str>,
467 release_refs: &[String],
468 ) -> Element {
469 let mut deal = Element::new("ReleaseDeal");
470
471 let mut deal_ref_elem = Element::new("DealReference");
473 deal_ref_elem.add_text(deal_ref);
474 deal.add_child(deal_ref_elem);
475
476 let mut deal_terms = Element::new("DealTerms");
478
479 let mut model_elem = Element::new("CommercialModelType");
481 model_elem.add_text(commercial_model);
482 deal_terms.add_child(model_elem);
483
484 for territory_code in territories {
486 let mut territory = Element::new("Territory");
487 let mut territory_elem = Element::new("TerritoryCode");
488 territory_elem.add_text(territory_code);
489 territory.add_child(territory_elem);
490 deal_terms.add_child(territory);
491 }
492
493 if start_date.is_some() || end_date.is_some() {
495 let mut validity = Element::new("ValidityPeriod");
496
497 if let Some(start) = start_date {
498 let mut start_elem = Element::new("StartDate");
499 start_elem.add_text(start);
500 validity.add_child(start_elem);
501 }
502
503 if let Some(end) = end_date {
504 let mut end_elem = Element::new("EndDate");
505 end_elem.add_text(end);
506 validity.add_child(end_elem);
507 }
508
509 deal_terms.add_child(validity);
510 }
511
512 if let Some(price_val) = price {
514 let mut price_elem = Element::new("Price");
515 let mut amount_elem = Element::new("PriceAmount");
516 amount_elem.add_text(&price_val.to_string());
517 price_elem.add_child(amount_elem);
518
519 if let Some(currency_code) = currency {
520 let mut currency_elem = Element::new("PriceCurrencyCode");
521 currency_elem.add_text(currency_code);
522 price_elem.add_child(currency_elem);
523 }
524
525 deal_terms.add_child(price_elem);
526 }
527
528 if let Some(usage) = usage_type {
530 let mut usage_elem = Element::new("UsageType");
531 usage_elem.add_text(usage);
532 deal_terms.add_child(usage_elem);
533 }
534
535 deal.add_child(deal_terms);
536
537 for rel_ref in release_refs {
539 let mut ref_elem = Element::new("ReleaseReference");
540 ref_elem.add_text(rel_ref);
541 deal.add_child(ref_elem);
542 }
543
544 deal
545 }
546}
547
548pub mod validation {
550
551 use once_cell::sync::Lazy;
552 use regex::Regex;
553
554 static ISRC_PATTERN_42: Lazy<Regex> =
556 Lazy::new(|| Regex::new(r"^[A-Z]{2}[A-Z0-9]{3}\d{7}$").unwrap());
557
558 static UPC_PATTERN_42: Lazy<Regex> = Lazy::new(|| Regex::new(r"^\d{12}$").unwrap());
559
560 static DURATION_PATTERN_42: Lazy<Regex> =
561 Lazy::new(|| Regex::new(r"^PT(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?$").unwrap());
562
563 static DATE_PATTERN_42: Lazy<Regex> = Lazy::new(|| Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap());
564
565 static DATETIME_PATTERN_42: Lazy<Regex> = Lazy::new(|| {
566 Regex::new(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$").unwrap()
567 });
568
569 pub fn validate_isrc(isrc: &str) -> bool {
571 ISRC_PATTERN_42.is_match(isrc)
572 }
573
574 pub fn validate_upc(upc: &str) -> bool {
576 UPC_PATTERN_42.is_match(upc)
577 }
578
579 pub fn validate_duration(duration: &str) -> bool {
581 DURATION_PATTERN_42.is_match(duration)
582 }
583
584 pub fn validate_date(date: &str) -> bool {
586 DATE_PATTERN_42.is_match(date)
587 }
588
589 pub fn validate_datetime(datetime: &str) -> bool {
591 DATETIME_PATTERN_42.is_match(datetime)
592 }
593
594 pub fn validate_territory_code(territory: &str) -> bool {
596 matches!(
598 territory,
599 "US" | "GB"
600 | "DE"
601 | "FR"
602 | "JP"
603 | "CA"
604 | "AU"
605 | "IT"
606 | "ES"
607 | "NL"
608 | "SE"
609 | "NO"
610 | "DK"
611 | "FI"
612 | "BR"
613 | "MX"
614 | "AR"
615 | "IN"
616 | "CN"
617 | "KR"
618 | "Worldwide"
619 | "WorldwideExceptUS"
620 )
621 }
622
623 pub fn validate_commercial_model(model: &str) -> bool {
625 matches!(
626 model,
627 "SubscriptionModel"
628 | "PurchaseModel"
629 | "AdSupportedModel"
630 | "FreeOfChargeModel"
631 | "StreamingModel"
632 | "DownloadModel"
633 )
634 }
635
636 pub fn validate_audio_codec(codec: &str) -> bool {
638 matches!(
639 codec,
640 "MP3" | "AAC" | "FLAC" | "WAV" | "OGG" | "WMA" | "MP4" | "M4A"
641 )
642 }
643
644 pub fn validate_usage_type(usage: &str) -> bool {
646 matches!(
647 usage,
648 "Stream"
649 | "Download"
650 | "Preview"
651 | "ConditionalDownload"
652 | "DigitalPhonogramDelivery"
653 | "UserMadeClip"
654 )
655 }
656
657 pub fn validate_ern_42_message(xml_content: &str) -> Vec<String> {
659 let mut errors = Vec::new();
660
661 if !xml_content.contains("http://ddex.net/xml/ern/42") {
663 errors.push("Missing ERN 4.2 namespace".to_string());
664 }
665
666 if !xml_content.contains("ern/42") {
668 errors.push("Missing or incorrect MessageSchemaVersionId".to_string());
669 }
670
671 let required_elements = [
673 "MessageId",
674 "MessageSender",
675 "MessageRecipient",
676 "MessageCreatedDateTime",
677 "ResourceList",
678 "ReleaseList",
679 ];
680
681 for element in &required_elements {
682 if !xml_content.contains(&format!("<{}", element)) {
683 errors.push(format!("Missing required element: {}", element));
684 }
685 }
686
687 if xml_content.contains("TechnicalResourceDetails") {
689 if !xml_content.contains("AudioCodecType") && !xml_content.contains("FileName") {
690 errors.push(
691 "TechnicalResourceDetails should contain AudioCodecType or FileName"
692 .to_string(),
693 );
694 }
695 }
696
697 errors
698 }
699}