ddex_builder/
builder.rs

1//! Main builder implementation
2
3pub use super::preflight::PreflightLevel;
4use crate::generator::{xml_writer::XmlWriter, ASTGenerator};
5use indexmap::IndexMap;
6use serde::{Deserialize, Serialize};
7
8/// Build request for generating DDEX messages
9///
10/// Contains all the information needed to generate a complete DDEX XML message,
11/// including message header, releases, deals, and any extensions.
12/// The structure is designed for deterministic output with stable ordering.
13///
14/// # Example
15/// ```
16/// use ddex_builder::builder::{BuildRequest, MessageHeaderRequest, PartyRequest, LocalizedStringRequest};
17///
18/// let request = BuildRequest {
19///     header: MessageHeaderRequest {
20///         message_id: Some("MSG123".to_string()),
21///         message_sender: PartyRequest {
22///             party_name: vec![LocalizedStringRequest {
23///                 text: "My Label".to_string(),
24///                 language_code: Some("en".to_string()),
25///             }],
26///             party_id: Some("PADPIDA2014120301K".to_string()),
27///             party_reference: None,
28///         },
29///         // ... other fields
30///         message_recipient: PartyRequest { /* ... */ },
31///         message_control_type: Some("NewReleaseMessage".to_string()),
32///         message_created_date_time: None, // Will be auto-generated
33///     },
34///     version: "4.3".to_string(),
35///     profile: None,
36///     releases: vec![/* ReleaseRequest items */],
37///     deals: vec![/* DealRequest items */],
38///     extensions: None,
39/// };
40/// ```
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct BuildRequest {
43    /// Message header containing sender, recipient, and message metadata
44    pub header: MessageHeaderRequest,
45
46    /// ERN version (e.g., "3.8.2", "4.2", "4.3")
47    pub version: String,
48
49    /// DDEX profile identifier (optional)
50    pub profile: Option<String>,
51
52    /// List of releases in this message
53    /// Uses Vec to maintain order while allowing duplicates if needed
54    pub releases: Vec<ReleaseRequest>,
55
56    /// List of commercial deals
57    pub deals: Vec<DealRequest>,
58
59    /// Custom extensions (uses IndexMap for deterministic ordering)
60    pub extensions: Option<IndexMap<String, String>>,
61}
62
63/// Message header information for DDEX messages
64///
65/// Contains metadata about the message including sender, recipient,
66/// message type, and creation timestamp.
67///
68/// # Example
69/// ```
70/// use ddex_builder::builder::{MessageHeaderRequest, PartyRequest, LocalizedStringRequest};
71///
72/// let header = MessageHeaderRequest {
73///     message_id: Some("MSG_20241215_001".to_string()),
74///     message_sender: PartyRequest {
75///         party_name: vec![LocalizedStringRequest {
76///             text: "Warner Music Group".to_string(),
77///             language_code: Some("en".to_string()),
78///         }],
79///         party_id: Some("PADPIDA2014120301K".to_string()),
80///         party_reference: None,
81///     },
82///     message_recipient: PartyRequest { /* similar structure */ },
83///     message_control_type: Some("NewReleaseMessage".to_string()),
84///     message_created_date_time: None, // Auto-generated if None
85/// };
86/// ```
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct MessageHeaderRequest {
89    /// Unique message identifier (auto-generated if None)
90    pub message_id: Option<String>,
91    /// Party sending the message
92    pub message_sender: PartyRequest,
93    /// Party receiving the message
94    pub message_recipient: PartyRequest,
95    /// Type of message control (e.g., "NewReleaseMessage", "PurgeReleaseMessage")
96    pub message_control_type: Option<String>,
97    /// Message creation timestamp in ISO 8601 format (auto-generated if None)
98    pub message_created_date_time: Option<String>,
99}
100
101/// Party information request
102///
103/// Represents a party (sender, recipient, or other entity) in the DDEX message.
104/// Supports multiple localized names and various identifier types.
105///
106/// # Example
107/// ```
108/// use ddex_builder::builder::{PartyRequest, LocalizedStringRequest};
109///
110/// let party = PartyRequest {
111///     party_name: vec![
112///         LocalizedStringRequest {
113///             text: "Universal Music Group".to_string(),
114///             language_code: Some("en".to_string()),
115///         },
116///         LocalizedStringRequest {
117///             text: "Universal Music Group".to_string(),
118///             language_code: Some("es".to_string()),
119///         },
120///     ],
121///     party_id: Some("PADPIDA2014120301K".to_string()), // DPID
122///     party_reference: Some("PARTY_REF_001".to_string()),
123/// };
124/// ```
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct PartyRequest {
127    /// Party names in multiple languages
128    pub party_name: Vec<LocalizedStringRequest>,
129    /// Party identifier (e.g., DPID, ISNI, Proprietary ID)
130    pub party_id: Option<String>,
131    /// Reference identifier for this party within the message
132    pub party_reference: Option<String>,
133}
134
135/// Localized string with language code
136///
137/// Represents text content with an optional language identifier.
138/// Used for titles, names, and other textual content that may need
139/// to be provided in multiple languages.
140///
141/// # Example
142/// ```
143/// use ddex_builder::builder::LocalizedStringRequest;
144///
145/// let english_title = LocalizedStringRequest {
146///     text: "My Song Title".to_string(),
147///     language_code: Some("en".to_string()),
148/// };
149///
150/// let spanish_title = LocalizedStringRequest {
151///     text: "Mi Título de Canción".to_string(),
152///     language_code: Some("es".to_string()),
153/// };
154/// ```
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct LocalizedStringRequest {
157    /// Text content
158    pub text: String,
159    /// ISO 639-1 language code (e.g., "en", "es", "fr")
160    pub language_code: Option<String>,
161}
162
163/// Release information request
164///
165/// Represents a music release (album, single, EP, etc.) with all its metadata,
166/// tracks, and identifiers. This is the core content of most DDEX messages.
167///
168/// # Example
169/// ```
170/// use ddex_builder::builder::{ReleaseRequest, LocalizedStringRequest, TrackRequest};
171///
172/// let release = ReleaseRequest {
173///     release_id: "GRid:A1-12345678901234567890123456789012".to_string(),
174///     release_reference: Some("REL_001".to_string()),
175///     title: vec![LocalizedStringRequest {
176///         text: "Greatest Hits".to_string(),
177///         language_code: Some("en".to_string()),
178///     }],
179///     artist: "The Beatles".to_string(),
180///     label: Some("Apple Records".to_string()),
181///     release_date: Some("2024-01-15".to_string()),
182///     upc: Some("123456789012".to_string()),
183///     tracks: vec![
184///         TrackRequest {
185///             track_id: "T001".to_string(),
186///             resource_reference: Some("RES_001".to_string()),
187///             isrc: "GBUM71505078".to_string(),
188///             title: "Here Comes The Sun".to_string(),
189///             duration: "PT3M5S".to_string(),
190///             artist: "The Beatles".to_string(),
191///         }
192///     ],
193///     resource_references: Some(vec!["RES_001".to_string()]),
194/// };
195/// ```
196#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct ReleaseRequest {
198    /// Release identifier (e.g., GRid, Proprietary ID)
199    pub release_id: String,
200    /// Internal reference for this release within the message
201    pub release_reference: Option<String>,
202    /// Release titles in multiple languages
203    pub title: Vec<LocalizedStringRequest>,
204    /// Main artist name for the release
205    pub artist: String,
206    /// Record label name
207    pub label: Option<String>,
208    /// Release date in YYYY-MM-DD format
209    pub release_date: Option<String>,
210    /// Universal Product Code for the release (12-digit barcode)
211    pub upc: Option<String>,
212    /// List of tracks/resources in this release
213    pub tracks: Vec<TrackRequest>,
214    /// References to resources for linking purposes
215    pub resource_references: Option<Vec<String>>,
216}
217
218/// Track information request
219///
220/// Represents a single track/sound recording within a release.
221/// Contains all the metadata needed for proper DDEX representation.
222///
223/// # Example
224/// ```
225/// use ddex_builder::builder::TrackRequest;
226///
227/// let track = TrackRequest {
228///     track_id: "T001".to_string(),
229///     resource_reference: Some("A12345".to_string()),
230///     isrc: "USUM71504847".to_string(),
231///     title: "Bohemian Rhapsody".to_string(),
232///     duration: "PT5M55S".to_string(), // 5 minutes 55 seconds
233///     artist: "Queen".to_string(),
234/// };
235/// ```
236#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct TrackRequest {
238    /// Unique identifier for this track within the message
239    pub track_id: String,
240    /// Reference to the sound recording resource
241    pub resource_reference: Option<String>,
242    /// International Standard Recording Code (12-character alphanumeric)
243    pub isrc: String,
244    /// Track title
245    pub title: String,
246    /// Duration in ISO 8601 format (e.g., "PT3M45S" for 3 minutes 45 seconds)
247    pub duration: String,
248    /// Track artist name (may differ from release artist for compilations)
249    pub artist: String,
250}
251
252/// Commercial deal request
253///
254/// Represents the commercial terms and licensing information for releases.
255/// Defines how and where the music can be distributed and monetized.
256///
257/// # Example
258/// ```
259/// use ddex_builder::builder::{DealRequest, DealTerms};
260///
261/// let deal = DealRequest {
262///     deal_reference: Some("DEAL_001".to_string()),
263///     deal_terms: DealTerms {
264///         commercial_model_type: "PayAsYouGoModel".to_string(),
265///         territory_code: vec!["Worldwide".to_string()],
266///         start_date: Some("2024-01-01".to_string()),
267///     },
268///     release_references: vec!["REL_001".to_string()],
269/// };
270/// ```
271#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct DealRequest {
273    /// Reference identifier for this deal within the message
274    pub deal_reference: Option<String>,
275    /// Commercial terms and licensing conditions
276    pub deal_terms: DealTerms,
277    /// References to releases covered by this deal
278    pub release_references: Vec<String>,
279}
280
281/// Commercial deal terms
282///
283/// Defines the specific commercial and territorial terms of a licensing deal.
284/// These terms control how the music can be distributed and monetized.
285///
286/// # Example
287/// ```
288/// use ddex_builder::builder::DealTerms;
289///
290/// let terms = DealTerms {
291///     commercial_model_type: "SubscriptionModel".to_string(),
292///     territory_code: vec!["US".to_string(), "CA".to_string(), "MX".to_string()],
293///     start_date: Some("2024-01-01".to_string()),
294/// };
295/// ```
296#[derive(Debug, Clone, Serialize, Deserialize)]
297pub struct DealTerms {
298    /// Type of commercial model (e.g., "PayAsYouGoModel", "SubscriptionModel", "FreeOfChargeModel")
299    pub commercial_model_type: String,
300    /// Territory codes where deal applies (ISO 3166-1 alpha-2 codes or "Worldwide")
301    pub territory_code: Vec<String>,
302    /// Deal start date in YYYY-MM-DD format (optional)
303    pub start_date: Option<String>,
304}
305
306/// Build options
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct BuildOptions {
309    /// Determinism configuration
310    pub determinism: Option<super::determinism::DeterminismConfig>,
311
312    /// Validation level
313    pub preflight_level: super::preflight::PreflightLevel,
314
315    /// ID generation strategy
316    pub id_strategy: IdStrategy,
317
318    /// Stable hash configuration (when using StableHash strategy)
319    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/// ID generation strategy
334#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
335pub enum IdStrategy {
336    /// UUID v4
337    UUID,
338    /// UUID v7 (time-ordered)
339    UUIDv7,
340    /// Sequential
341    Sequential,
342    /// Stable hash-based
343    StableHash,
344}
345
346/// Build result
347#[derive(Debug, Clone, Serialize, Deserialize)]
348pub struct BuildResult {
349    /// Generated XML
350    pub xml: String,
351
352    /// Warnings
353    pub warnings: Vec<BuildWarning>,
354
355    /// Errors (if any)
356    pub errors: Vec<super::error::BuildError>,
357
358    /// Statistics
359    pub statistics: BuildStatistics,
360
361    /// Canonical hash (if deterministic)
362    pub canonical_hash: Option<String>,
363
364    /// Reproducibility banner (if requested)
365    pub reproducibility_banner: Option<String>,
366}
367
368/// Build warning
369#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct BuildWarning {
371    /// Warning code for programmatic handling
372    pub code: String,
373    /// Human-readable warning message
374    pub message: String,
375    /// Location in the structure where warning occurred
376    pub location: Option<String>,
377}
378
379/// Build statistics
380#[derive(Debug, Clone, Serialize, Deserialize)]
381pub struct BuildStatistics {
382    /// Number of releases built
383    pub releases: usize,
384    /// Total number of tracks processed
385    pub tracks: usize,
386    /// Number of deals processed
387    pub deals: usize,
388    /// Time taken to generate in milliseconds
389    pub generation_time_ms: u64,
390    /// Size of generated XML in bytes
391    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
406/// Main DDEX Builder
407pub struct DDEXBuilder {
408    _inner: super::Builder,
409}
410
411impl DDEXBuilder {
412    /// Create new builder
413    pub fn new() -> Self {
414        Self {
415            _inner: super::Builder::new(),
416        }
417    }
418
419    /// Build DDEX XML from request
420    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        // 1. Enhanced preflight checks with new validator
429        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        // Convert validation warnings to build warnings
443        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        // Fail if validation didn't pass
452        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        // 2. Generate IDs based on strategy
465        self.generate_ids(&mut request, &options)?;
466
467        // 3. Generate AST
468        let mut generator = ASTGenerator::new(request.version.clone());
469        let ast = generator.generate(&request)?;
470
471        // 4. Apply determinism config
472        let config = options.determinism.unwrap_or_default();
473
474        // 5. Generate XML
475        let writer = XmlWriter::new(config.clone());
476        let xml = writer.write(&ast)?;
477
478        // 6. Apply canonicalization if requested
479        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        // 7. Generate reproducibility banner if requested
490        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    /// Generate IDs based on the selected strategy
519    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    /// Generate UUID v4 IDs
542    fn generate_uuid_ids(
543        &self,
544        request: &mut BuildRequest,
545    ) -> Result<(), super::error::BuildError> {
546        use uuid::Uuid;
547
548        // Generate message ID if missing
549        if request.header.message_id.is_none() {
550            request.header.message_id = Some(format!("MSG_{}", Uuid::new_v4()));
551        }
552
553        // Generate release references if missing
554        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            // Generate resource references for tracks
560            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        // Generate deal references if missing
568        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    /// Generate UUID v7 IDs (time-ordered)
578    fn generate_uuidv7_ids(
579        &self,
580        request: &mut BuildRequest,
581    ) -> Result<(), super::error::BuildError> {
582        // For now, fall back to UUID v4
583        // TODO: Implement proper UUID v7 generation
584        self.generate_uuid_ids(request)
585    }
586
587    /// Generate sequential IDs
588    fn generate_sequential_ids(
589        &self,
590        request: &mut BuildRequest,
591    ) -> Result<(), super::error::BuildError> {
592        // Generate message ID if missing
593        if request.header.message_id.is_none() {
594            request.header.message_id = Some(format!("MSG_{}", chrono::Utc::now().timestamp()));
595        }
596
597        // Generate release references if missing
598        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            // Generate resource references for tracks
604            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        // Generate deal references if missing
612        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    /// Generate stable hash-based IDs
622    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        // Generate message ID if missing
631        if request.header.message_id.is_none() {
632            // Use sender/recipient info for stable message ID
633            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        // Generate stable IDs for releases
657        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                    &[], // Empty territory set for now
668                )?;
669                release.release_reference = Some(id);
670            }
671
672            // Generate stable IDs for tracks/resources
673            for track in &mut release.tracks {
674                if track.resource_reference.is_none() {
675                    // Parse duration to seconds for stable hash
676                    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, // No file hash available
683                    )?;
684                    track.resource_reference = Some(id);
685                }
686            }
687        }
688
689        // Generate deal references if missing
690        for (_idx, deal) in request.deals.iter_mut().enumerate() {
691            if deal.deal_reference.is_none() {
692                // Create stable deal ID based on terms
693                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    /// Parse ISO 8601 duration to seconds
705    fn parse_duration_to_seconds(&self, duration: &str) -> Option<u32> {
706        // Simple parser for PT3M45S format
707        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    /// Legacy preflight check method (kept for compatibility)
743    #[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        // Basic checks (enhanced validation is done in main build method)
756        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    /// Compare two DDEX XML documents and return semantic differences
775    ///
776    /// This method performs semantic diffing that understands DDEX business logic,
777    /// not just XML structure differences.
778    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    /// Compare two DDEX XML documents with custom diff configuration
787    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        // Parse both XML documents to AST
794        let old_ast = self.parse_xml_to_ast(old_xml)?;
795        let new_ast = self.parse_xml_to_ast(new_xml)?;
796
797        // Create diff engine and compare
798        let mut diff_engine = super::diff::DiffEngine::new_with_config(config);
799        diff_engine.diff(&old_ast, &new_ast)
800    }
801
802    /// Compare a BuildRequest with existing XML to see what would change
803    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        // Build new XML from request
809        let build_result = self.build(request.clone(), BuildOptions::default())?;
810
811        // Compare existing XML with newly built XML
812        self.diff_xml(existing_xml, &build_result.xml)
813    }
814
815    /// Helper to parse XML string to AST
816    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        // This is a simplified XML->AST parser
823        // In a production system, you'd want to use the actual ddex-parser
824        let mut root_element = super::ast::Element::new("Root");
825        let namespace_map = indexmap::IndexMap::new();
826
827        // For now, create a basic AST structure
828        // TODO: Implement proper XML parsing or integrate with ddex-parser
829        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    /// Create an UpdateReleaseMessage from two DDEX messages
839    ///
840    /// This method compares an original DDEX message with an updated version and
841    /// generates a minimal UpdateReleaseMessage containing only the differences.
842    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    /// Create an UpdateReleaseMessage with custom configuration
853    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    /// Apply an UpdateReleaseMessage to a base DDEX message
865    ///
866    /// This method takes a base DDEX message and applies the operations from an
867    /// UpdateReleaseMessage to produce a new complete DDEX message.
868    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    /// Create an update from a BuildRequest compared to existing XML
878    ///
879    /// This is useful for generating updates when you have a new BuildRequest
880    /// that represents the desired state and need to update an existing message.
881    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        // Build new XML from request
888        let build_result = self.build(request.clone(), BuildOptions::default())?;
889
890        // Create update between existing and new XML
891        self.create_update(existing_xml, &build_result.xml, original_message_id)
892    }
893
894    /// Validate an UpdateReleaseMessage for safety and consistency
895    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    /// Generate an UpdateReleaseMessage as XML
904    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    // Helper methods for update serialization
912
913    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 declaration and root element
920        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        // Message header
926        self.serialize_update_header(&mut xml, &update.header)?;
927
928        // Update metadata
929        self.serialize_update_metadata(&mut xml, &update.update_metadata)?;
930
931        // Update list
932        self.serialize_update_list(&mut xml, &update.update_list)?;
933
934        // Resource updates
935        if !update.resource_updates.is_empty() {
936            self.serialize_resource_updates(&mut xml, &update.resource_updates)?;
937        }
938
939        // Release updates
940        if !update.release_updates.is_empty() {
941            self.serialize_release_updates(&mut xml, &update.release_updates)?;
942        }
943
944        // Deal updates
945        if !update.deal_updates.is_empty() {
946            self.serialize_deal_updates(&mut xml, &update.deal_updates)?;
947        }
948
949        // Close root element
950        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        // Message sender
970        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        // Message recipient
980        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        // Created date time
990        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            // Add resource data if present
1129            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            // Add release data if present
1185            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('&', "&amp;")
1249            .replace('<', "&lt;")
1250            .replace('>', "&gt;")
1251            .replace('"', "&quot;")
1252            .replace('\'', "&apos;")
1253    }
1254}
1255
1256impl Default for DDEXBuilder {
1257    fn default() -> Self {
1258        Self::new()
1259    }
1260}