ddex_builder/versions/
ern_382.rs

1//! ERN 3.8.2 (Legacy) version specification and handling
2//!
3//! ERN 3.8.2 is a legacy DDEX version that predates many modern features.
4//! This module handles the specific requirements and constraints of ERN 3.8.2.
5
6use super::*;
7use crate::presets::DdexVersion;
8
9/// Get ERN 3.8.2 version specification
10pub fn get_version_spec() -> VersionSpec {
11    let mut element_mappings = IndexMap::new();
12    let mut namespace_prefixes = IndexMap::new();
13
14    // Legacy namespace mappings
15    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    // Common element mappings from 3.8.2 to modern versions
19    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    // Resource-related mappings
36    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    // Release-related mappings
49    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    // Deal-related mappings (limited in 3.8.2)
60    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    // Legacy-specific elements that don't exist in newer versions
70    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            // These elements exist in 3.8.2 but are deprecated in newer versions
100            "LegacyTechnicalDetails".to_string(),
101            "BasicPrice".to_string(),
102            "SimpleTerritory".to_string(),
103            "OldStyleGenre".to_string(),
104        ],
105        new_elements: vec![
106            // Elements that are new in 3.8.2 (none, as this is the baseline)
107        ],
108        namespace_prefixes,
109    }
110}
111
112/// ERN 3.8.2 specific constraints and validation rules
113pub struct Ern382Constraints {
114    /// Maximum allowed resources per release
115    pub max_resources_per_release: usize,
116    /// Supported image formats
117    pub supported_image_formats: Vec<String>,
118    /// Supported audio formats
119    pub supported_audio_formats: Vec<String>,
120    /// Maximum deal complexity
121    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
135/// Get ERN 3.8.2 specific element validation rules
136pub fn get_validation_rules() -> IndexMap<String, ValidationRule> {
137    let mut rules = IndexMap::new();
138
139    // Message ID format (simpler in 3.8.2)
140    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    // ISRC format (standard)
152    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    // UPC format (12 digits)
164    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    // Duration format (simpler in 3.8.2)
176    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    // Release date format
188    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    // Territory code (simpler set in 3.8.2)
200    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/// Validation rule definition
222#[derive(Debug, Clone)]
223pub struct ValidationRule {
224    /// Type of validation rule
225    pub rule_type: ValidationRuleType,
226    /// Regex pattern for pattern validation
227    pub pattern: Option<String>,
228    /// Enum values for enum validation
229    pub enum_values: Option<Vec<String>>,
230    /// Whether field is required
231    pub required: bool,
232    /// Description of the rule
233    pub description: String,
234}
235
236/// Type of validation rule
237#[derive(Debug, Clone)]
238pub enum ValidationRuleType {
239    /// Pattern-based validation
240    Pattern,
241    /// Enum value validation
242    Enum,
243    /// Length validation constraint
244    Length {
245        /// Minimum length (inclusive)
246        min: Option<usize>,
247        /// Maximum length (inclusive)
248        max: Option<usize>,
249    },
250    /// Custom validation function
251    Custom(String),
252}
253
254/// Get ERN 3.8.2 namespace mappings
255pub 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
265/// Get ERN 3.8.2 specific XML template
266pub 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
296/// ERN 3.8.2 specific element builders
297pub mod builders {
298
299    use crate::ast::Element;
300
301    /// Build ERN 3.8.2 message header
302    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        // Message ID
311        let mut msg_id = Element::new("MessageId");
312        msg_id.add_text(message_id);
313        header.add_child(msg_id);
314
315        // Message Sender
316        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        // Message Recipient
323        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        // Created DateTime
330        let mut created = Element::new("MessageCreatedDateTime");
331        created.add_text(created_datetime);
332        header.add_child(created);
333
334        header
335    }
336
337    /// Build ERN 3.8.2 sound recording resource
338    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        // Resource Reference
349        let mut res_ref = Element::new("ResourceReference");
350        res_ref.add_text(resource_ref);
351        sound_recording.add_child(res_ref);
352
353        // Type (always SoundRecording in this context)
354        let mut res_type = Element::new("Type");
355        res_type.add_text("SoundRecording");
356        sound_recording.add_child(res_type);
357
358        // Resource ID
359        let mut res_id = Element::new("ResourceId");
360        res_id.add_text(resource_id);
361        sound_recording.add_child(res_id);
362
363        // Reference Title
364        let mut ref_title = Element::new("ReferenceTitle");
365        ref_title.add_text(title);
366        sound_recording.add_child(ref_title);
367
368        // Display Artist
369        let mut display_artist = Element::new("DisplayArtist");
370        display_artist.add_text(artist);
371        sound_recording.add_child(display_artist);
372
373        // ISRC
374        let mut isrc_elem = Element::new("ISRC");
375        isrc_elem.add_text(isrc);
376        sound_recording.add_child(isrc_elem);
377
378        // Duration
379        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    /// Build ERN 3.8.2 release
387    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        // Release Reference
401        let mut rel_ref = Element::new("ReleaseReference");
402        rel_ref.add_text(release_ref);
403        release.add_child(rel_ref);
404
405        // Release ID
406        let mut rel_id = Element::new("ReleaseId");
407        rel_id.add_text(release_id);
408        release.add_child(rel_id);
409
410        // Release Type (default to Album for ERN 3.8.2)
411        let mut rel_type = Element::new("ReleaseType");
412        rel_type.add_text("Album");
413        release.add_child(rel_type);
414
415        // Title
416        let mut title_elem = Element::new("Title");
417        title_elem.add_text(title);
418        release.add_child(title_elem);
419
420        // Display Artist
421        let mut artist_elem = Element::new("DisplayArtist");
422        artist_elem.add_text(artist);
423        release.add_child(artist_elem);
424
425        // Label Name
426        let mut label_elem = Element::new("LabelName");
427        label_elem.add_text(label);
428        release.add_child(label_elem);
429
430        // UPC (optional)
431        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        // Release Date (optional)
438        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        // Genre (optional)
445        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        // Resource Group (simplified in 3.8.2)
452        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    /// Build ERN 3.8.2 simple deal
466    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        // Deal Reference
478        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        // Deal Terms (simplified in 3.8.2)
483        let mut deal_terms = Element::new("DealTerms");
484
485        // Commercial Model Type
486        let mut model_elem = Element::new("CommercialModelType");
487        model_elem.add_text(commercial_model);
488        deal_terms.add_child(model_elem);
489
490        // Territory Code
491        let mut territory_elem = Element::new("TerritoryCode");
492        territory_elem.add_text(territory);
493        deal_terms.add_child(territory_elem);
494
495        // Validity Period (if start date provided)
496        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        // Price (if provided)
505        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        // Release References
523        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
533/// ERN 3.8.2 validation functions
534pub mod validation {
535
536    use once_cell::sync::Lazy;
537    use regex::Regex;
538
539    // Precompiled regex patterns for ERN 3.8.2
540    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    /// Validate ISRC format for ERN 3.8.2
551    pub fn validate_isrc(isrc: &str) -> bool {
552        ISRC_PATTERN_382.is_match(isrc)
553    }
554
555    /// Validate UPC format for ERN 3.8.2
556    pub fn validate_upc(upc: &str) -> bool {
557        UPC_PATTERN_382.is_match(upc)
558    }
559
560    /// Validate duration format for ERN 3.8.2
561    pub fn validate_duration(duration: &str) -> bool {
562        DURATION_PATTERN_382.is_match(duration)
563    }
564
565    /// Validate date format for ERN 3.8.2
566    pub fn validate_date(date: &str) -> bool {
567        DATE_PATTERN_382.is_match(date)
568    }
569
570    /// Validate territory code for ERN 3.8.2
571    pub fn validate_territory_code(territory: &str) -> bool {
572        matches!(territory, "US" | "GB" | "DE" | "FR" | "JP" | "Worldwide")
573    }
574
575    /// Validate commercial model type for ERN 3.8.2
576    pub fn validate_commercial_model(model: &str) -> bool {
577        matches!(
578            model,
579            "SubscriptionModel" | "PurchaseModel" | "AdSupportedModel" | "FreeOfChargeModel"
580        )
581    }
582
583    /// Get all validation errors for an ERN 3.8.2 message
584    pub fn validate_ern_382_message(xml_content: &str) -> Vec<String> {
585        let mut errors = Vec::new();
586
587        // Check required namespace
588        if !xml_content.contains("http://ddex.net/xml/ern/382") {
589            errors.push("Missing ERN 3.8.2 namespace".to_string());
590        }
591
592        // Check message schema version ID
593        if !xml_content.contains("ern/382") {
594            errors.push("Missing or incorrect MessageSchemaVersionId".to_string());
595        }
596
597        // Check for required elements
598        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}