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/382".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
35 element_mappings.insert("ResourceList".to_string(), "ResourceList".to_string());
37 element_mappings.insert("SoundRecording".to_string(), "SoundRecording".to_string());
38 element_mappings.insert(
39 "ResourceReference".to_string(),
40 "ResourceReference".to_string(),
41 );
42 element_mappings.insert("ResourceId".to_string(), "ResourceId".to_string());
43 element_mappings.insert("ReferenceTitle".to_string(), "ReferenceTitle".to_string());
44 element_mappings.insert("DisplayArtist".to_string(), "DisplayArtist".to_string());
45 element_mappings.insert("ISRC".to_string(), "ISRC".to_string());
46 element_mappings.insert("Duration".to_string(), "Duration".to_string());
47
48 element_mappings.insert("ReleaseList".to_string(), "ReleaseList".to_string());
50 element_mappings.insert("Release".to_string(), "Release".to_string());
51 element_mappings.insert("ReleaseId".to_string(), "ReleaseId".to_string());
52 element_mappings.insert("Title".to_string(), "Title".to_string());
53 element_mappings.insert("DisplayArtist".to_string(), "DisplayArtist".to_string());
54 element_mappings.insert("LabelName".to_string(), "LabelName".to_string());
55 element_mappings.insert("UPC".to_string(), "UPC".to_string());
56 element_mappings.insert("ReleaseDate".to_string(), "ReleaseDate".to_string());
57 element_mappings.insert("Genre".to_string(), "Genre".to_string());
58
59 element_mappings.insert("DealList".to_string(), "DealList".to_string());
61 element_mappings.insert("ReleaseDeal".to_string(), "ReleaseDeal".to_string());
62 element_mappings.insert("DealTerms".to_string(), "DealTerms".to_string());
63 element_mappings.insert(
64 "CommercialModelType".to_string(),
65 "CommercialModelType".to_string(),
66 );
67 element_mappings.insert("TerritoryCode".to_string(), "TerritoryCode".to_string());
68
69 element_mappings.insert(
71 "LegacyTechnicalDetails".to_string(),
72 "TechnicalResourceDetails".to_string(),
73 );
74 element_mappings.insert("BasicPrice".to_string(), "Price".to_string());
75 element_mappings.insert("SimpleTerritory".to_string(), "Territory".to_string());
76
77 VersionSpec {
78 version: DdexVersion::Ern382,
79 namespace: "http://ddex.net/xml/ern/382".to_string(),
80 schema_location: Some(
81 "http://ddex.net/xml/ern/382 http://ddex.net/xml/ern/382/release-notification.xsd"
82 .to_string(),
83 ),
84 message_schema_version_id: "ern/382".to_string(),
85 supported_message_types: vec![
86 "NewReleaseMessage".to_string(),
87 "CatalogListMessage".to_string(),
88 ],
89 element_mappings,
90 required_elements: vec![
91 "MessageId".to_string(),
92 "MessageSender".to_string(),
93 "MessageRecipient".to_string(),
94 "MessageCreatedDateTime".to_string(),
95 "ResourceList".to_string(),
96 "ReleaseList".to_string(),
97 ],
98 deprecated_elements: vec![
99 "LegacyTechnicalDetails".to_string(),
101 "BasicPrice".to_string(),
102 "SimpleTerritory".to_string(),
103 "OldStyleGenre".to_string(),
104 ],
105 new_elements: vec![
106 ],
108 namespace_prefixes,
109 }
110}
111
112pub struct Ern382Constraints {
114 pub max_resources_per_release: usize,
116 pub supported_image_formats: Vec<String>,
118 pub supported_audio_formats: Vec<String>,
120 pub max_deal_terms: usize,
122}
123
124impl Default for Ern382Constraints {
125 fn default() -> Self {
126 Self {
127 max_resources_per_release: 100,
128 supported_image_formats: vec!["JPEG".to_string(), "PNG".to_string()],
129 supported_audio_formats: vec!["MP3".to_string(), "WAV".to_string(), "FLAC".to_string()],
130 max_deal_terms: 10,
131 }
132 }
133}
134
135pub fn get_validation_rules() -> IndexMap<String, ValidationRule> {
137 let mut rules = IndexMap::new();
138
139 rules.insert(
141 "MessageId".to_string(),
142 ValidationRule {
143 rule_type: ValidationRuleType::Pattern,
144 pattern: Some("^[A-Za-z0-9_-]{1,50}$".to_string()),
145 enum_values: None,
146 required: true,
147 description: "Message identifier format for ERN 3.8.2".to_string(),
148 },
149 );
150
151 rules.insert(
153 "ISRC".to_string(),
154 ValidationRule {
155 rule_type: ValidationRuleType::Pattern,
156 pattern: Some("^[A-Z]{2}[A-Z0-9]{3}\\d{7}$".to_string()),
157 enum_values: None,
158 required: true,
159 description: "ISRC format validation".to_string(),
160 },
161 );
162
163 rules.insert(
165 "UPC".to_string(),
166 ValidationRule {
167 rule_type: ValidationRuleType::Pattern,
168 pattern: Some("^\\d{12}$".to_string()),
169 enum_values: None,
170 required: false,
171 description: "UPC format validation".to_string(),
172 },
173 );
174
175 rules.insert(
177 "Duration".to_string(),
178 ValidationRule {
179 rule_type: ValidationRuleType::Pattern,
180 pattern: Some("^PT\\d+M\\d+S$".to_string()),
181 enum_values: None,
182 required: false,
183 description: "Duration in PTnMnS format".to_string(),
184 },
185 );
186
187 rules.insert(
189 "ReleaseDate".to_string(),
190 ValidationRule {
191 rule_type: ValidationRuleType::Pattern,
192 pattern: Some("^\\d{4}-\\d{2}-\\d{2}$".to_string()),
193 enum_values: None,
194 required: false,
195 description: "Release date in YYYY-MM-DD format".to_string(),
196 },
197 );
198
199 rules.insert(
201 "TerritoryCode".to_string(),
202 ValidationRule {
203 rule_type: ValidationRuleType::Enum,
204 pattern: None,
205 enum_values: Some(vec![
206 "US".to_string(),
207 "GB".to_string(),
208 "DE".to_string(),
209 "FR".to_string(),
210 "JP".to_string(),
211 "Worldwide".to_string(),
212 ]),
213 required: true,
214 description: "Supported territory codes in ERN 3.8.2".to_string(),
215 },
216 );
217
218 rules
219}
220
221#[derive(Debug, Clone)]
223pub struct ValidationRule {
224 pub rule_type: ValidationRuleType,
226 pub pattern: Option<String>,
228 pub enum_values: Option<Vec<String>>,
230 pub required: bool,
232 pub description: String,
234}
235
236#[derive(Debug, Clone)]
238pub enum ValidationRuleType {
239 Pattern,
241 Enum,
243 Length {
245 min: Option<usize>,
247 max: Option<usize>,
249 },
250 Custom(String),
252}
253
254pub fn get_namespace_mappings() -> IndexMap<String, String> {
256 let mut mappings = IndexMap::new();
257
258 mappings.insert("ern".to_string(), "http://ddex.net/xml/ern/382".to_string());
259 mappings.insert("avs".to_string(), "http://ddex.net/xml/avs/avs".to_string());
260 mappings.insert("drm".to_string(), "http://ddex.net/xml/drm/drm".to_string());
261
262 mappings
263}
264
265pub fn get_xml_template() -> &'static str {
267 r#"<?xml version="1.0" encoding="UTF-8"?>
268<NewReleaseMessage xmlns="http://ddex.net/xml/ern/382"
269 xmlns:avs="http://ddex.net/xml/avs/avs"
270 MessageSchemaVersionId="ern/382">
271 <MessageHeader>
272 <MessageId>{message_id}</MessageId>
273 <MessageSender>
274 <PartyName>{sender_name}</PartyName>
275 </MessageSender>
276 <MessageRecipient>
277 <PartyName>{recipient_name}</PartyName>
278 </MessageRecipient>
279 <MessageCreatedDateTime>{created_datetime}</MessageCreatedDateTime>
280 </MessageHeader>
281
282 <ResourceList>
283 <!-- Resources will be populated here -->
284 </ResourceList>
285
286 <ReleaseList>
287 <!-- Releases will be populated here -->
288 </ReleaseList>
289
290 <DealList>
291 <!-- Deals will be populated here -->
292 </DealList>
293</NewReleaseMessage>"#
294}
295
296pub mod builders {
298
299 use crate::ast::Element;
300
301 pub fn build_message_header(
303 message_id: &str,
304 sender_name: &str,
305 recipient_name: &str,
306 created_datetime: &str,
307 ) -> Element {
308 let mut header = Element::new("MessageHeader");
309
310 let mut msg_id = Element::new("MessageId");
312 msg_id.add_text(message_id);
313 header.add_child(msg_id);
314
315 let mut sender = Element::new("MessageSender");
317 let mut sender_party = Element::new("PartyName");
318 sender_party.add_text(sender_name);
319 sender.add_child(sender_party);
320 header.add_child(sender);
321
322 let mut recipient = Element::new("MessageRecipient");
324 let mut recipient_party = Element::new("PartyName");
325 recipient_party.add_text(recipient_name);
326 recipient.add_child(recipient_party);
327 header.add_child(recipient);
328
329 let mut created = Element::new("MessageCreatedDateTime");
331 created.add_text(created_datetime);
332 header.add_child(created);
333
334 header
335 }
336
337 pub fn build_sound_recording(
339 resource_ref: &str,
340 resource_id: &str,
341 title: &str,
342 artist: &str,
343 isrc: &str,
344 duration: &str,
345 ) -> Element {
346 let mut sound_recording = Element::new("SoundRecording");
347
348 let mut res_ref = Element::new("ResourceReference");
350 res_ref.add_text(resource_ref);
351 sound_recording.add_child(res_ref);
352
353 let mut res_type = Element::new("Type");
355 res_type.add_text("SoundRecording");
356 sound_recording.add_child(res_type);
357
358 let mut res_id = Element::new("ResourceId");
360 res_id.add_text(resource_id);
361 sound_recording.add_child(res_id);
362
363 let mut ref_title = Element::new("ReferenceTitle");
365 ref_title.add_text(title);
366 sound_recording.add_child(ref_title);
367
368 let mut display_artist = Element::new("DisplayArtist");
370 display_artist.add_text(artist);
371 sound_recording.add_child(display_artist);
372
373 let mut isrc_elem = Element::new("ISRC");
375 isrc_elem.add_text(isrc);
376 sound_recording.add_child(isrc_elem);
377
378 let mut duration_elem = Element::new("Duration");
380 duration_elem.add_text(duration);
381 sound_recording.add_child(duration_elem);
382
383 sound_recording
384 }
385
386 pub fn build_release(
388 release_ref: &str,
389 release_id: &str,
390 title: &str,
391 artist: &str,
392 label: &str,
393 upc: Option<&str>,
394 release_date: Option<&str>,
395 genre: Option<&str>,
396 resource_refs: &[String],
397 ) -> Element {
398 let mut release = Element::new("Release");
399
400 let mut rel_ref = Element::new("ReleaseReference");
402 rel_ref.add_text(release_ref);
403 release.add_child(rel_ref);
404
405 let mut rel_id = Element::new("ReleaseId");
407 rel_id.add_text(release_id);
408 release.add_child(rel_id);
409
410 let mut rel_type = Element::new("ReleaseType");
412 rel_type.add_text("Album");
413 release.add_child(rel_type);
414
415 let mut title_elem = Element::new("Title");
417 title_elem.add_text(title);
418 release.add_child(title_elem);
419
420 let mut artist_elem = Element::new("DisplayArtist");
422 artist_elem.add_text(artist);
423 release.add_child(artist_elem);
424
425 let mut label_elem = Element::new("LabelName");
427 label_elem.add_text(label);
428 release.add_child(label_elem);
429
430 if let Some(upc_val) = upc {
432 let mut upc_elem = Element::new("UPC");
433 upc_elem.add_text(upc_val);
434 release.add_child(upc_elem);
435 }
436
437 if let Some(date) = release_date {
439 let mut date_elem = Element::new("ReleaseDate");
440 date_elem.add_text(date);
441 release.add_child(date_elem);
442 }
443
444 if let Some(genre_val) = genre {
446 let mut genre_elem = Element::new("Genre");
447 genre_elem.add_text(genre_val);
448 release.add_child(genre_elem);
449 }
450
451 if !resource_refs.is_empty() {
453 let mut resource_group = Element::new("ResourceGroup");
454 for res_ref in resource_refs {
455 let mut ref_elem = Element::new("ResourceReference");
456 ref_elem.add_text(res_ref);
457 resource_group.add_child(ref_elem);
458 }
459 release.add_child(resource_group);
460 }
461
462 release
463 }
464
465 pub fn build_simple_deal(
467 deal_ref: &str,
468 commercial_model: &str,
469 territory: &str,
470 start_date: Option<&str>,
471 price: Option<f64>,
472 currency: Option<&str>,
473 release_refs: &[String],
474 ) -> Element {
475 let mut deal = Element::new("ReleaseDeal");
476
477 let mut deal_ref_elem = Element::new("DealReference");
479 deal_ref_elem.add_text(deal_ref);
480 deal.add_child(deal_ref_elem);
481
482 let mut deal_terms = Element::new("DealTerms");
484
485 let mut model_elem = Element::new("CommercialModelType");
487 model_elem.add_text(commercial_model);
488 deal_terms.add_child(model_elem);
489
490 let mut territory_elem = Element::new("TerritoryCode");
492 territory_elem.add_text(territory);
493 deal_terms.add_child(territory_elem);
494
495 if let Some(start) = start_date {
497 let mut validity = Element::new("ValidityPeriod");
498 let mut start_elem = Element::new("StartDate");
499 start_elem.add_text(start);
500 validity.add_child(start_elem);
501 deal_terms.add_child(validity);
502 }
503
504 if let Some(price_val) = price {
506 let mut price_elem = Element::new("Price");
507 let mut amount_elem = Element::new("PriceAmount");
508 amount_elem.add_text(&price_val.to_string());
509 price_elem.add_child(amount_elem);
510
511 if let Some(currency_code) = currency {
512 let mut currency_elem = Element::new("PriceCurrencyCode");
513 currency_elem.add_text(currency_code);
514 price_elem.add_child(currency_elem);
515 }
516
517 deal_terms.add_child(price_elem);
518 }
519
520 deal.add_child(deal_terms);
521
522 for rel_ref in release_refs {
524 let mut ref_elem = Element::new("ReleaseReference");
525 ref_elem.add_text(rel_ref);
526 deal.add_child(ref_elem);
527 }
528
529 deal
530 }
531}
532
533pub mod validation {
535
536 use once_cell::sync::Lazy;
537 use regex::Regex;
538
539 static ISRC_PATTERN_382: Lazy<Regex> =
541 Lazy::new(|| Regex::new(r"^[A-Z]{2}[A-Z0-9]{3}\d{7}$").unwrap());
542
543 static UPC_PATTERN_382: Lazy<Regex> = Lazy::new(|| Regex::new(r"^\d{12}$").unwrap());
544
545 static DURATION_PATTERN_382: Lazy<Regex> = Lazy::new(|| Regex::new(r"^PT\d+M\d+S$").unwrap());
546
547 static DATE_PATTERN_382: Lazy<Regex> =
548 Lazy::new(|| Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap());
549
550 pub fn validate_isrc(isrc: &str) -> bool {
552 ISRC_PATTERN_382.is_match(isrc)
553 }
554
555 pub fn validate_upc(upc: &str) -> bool {
557 UPC_PATTERN_382.is_match(upc)
558 }
559
560 pub fn validate_duration(duration: &str) -> bool {
562 DURATION_PATTERN_382.is_match(duration)
563 }
564
565 pub fn validate_date(date: &str) -> bool {
567 DATE_PATTERN_382.is_match(date)
568 }
569
570 pub fn validate_territory_code(territory: &str) -> bool {
572 matches!(territory, "US" | "GB" | "DE" | "FR" | "JP" | "Worldwide")
573 }
574
575 pub fn validate_commercial_model(model: &str) -> bool {
577 matches!(
578 model,
579 "SubscriptionModel" | "PurchaseModel" | "AdSupportedModel" | "FreeOfChargeModel"
580 )
581 }
582
583 pub fn validate_ern_382_message(xml_content: &str) -> Vec<String> {
585 let mut errors = Vec::new();
586
587 if !xml_content.contains("http://ddex.net/xml/ern/382") {
589 errors.push("Missing ERN 3.8.2 namespace".to_string());
590 }
591
592 if !xml_content.contains("ern/382") {
594 errors.push("Missing or incorrect MessageSchemaVersionId".to_string());
595 }
596
597 let required_elements = [
599 "MessageId",
600 "MessageSender",
601 "MessageRecipient",
602 "MessageCreatedDateTime",
603 "ResourceList",
604 "ReleaseList",
605 ];
606
607 for element in &required_elements {
608 if !xml_content.contains(&format!("<{}", element)) {
609 errors.push(format!("Missing required element: {}", element));
610 }
611 }
612
613 errors
614 }
615}