ddex_builder/generator/
mod.rs

1//! # AST Generation Engine
2//! 
3//! This module handles the transformation of user-friendly `BuildRequest` structures
4//! into intermediate Abstract Syntax Trees (AST) that can be rendered as DDEX XML.
5//! 
6//! ## Architecture Overview
7//! 
8//! ```text
9//! Generation Pipeline
10//! ┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
11//! │  BuildRequest   │───▶│   ASTGenerator   │───▶│      AST        │
12//! │ (user-friendly) │    │                  │    │ (tree structure)│
13//! └─────────────────┘    └──────────────────┘    └─────────────────┘
14//!           │                       │                       │
15//!           ▼                       ▼                       ▼
16//!    ┌─────────────┐      ┌─────────────────┐    ┌─────────────────┐
17//!    │ • Releases  │      │ • Schema Rules  │    │ • Elements      │
18//!    │ • Tracks    │      │ • Validation    │    │ • Attributes    │
19//!    │ • Metadata  │      │ • Linking       │    │ • Namespaces    │
20//!    │ • Deals     │      │ • References    │    │ • Structure     │
21//!    └─────────────┘      └─────────────────┘    └─────────────────┘
22//! ```
23//! 
24//! ## Key Components
25//! 
26//! - **ASTGenerator**: Main orchestrator that converts requests to AST
27//! - **xml_writer**: High-level XML writer with formatting
28//! - **optimized_xml_writer**: Performance-optimized XML writer for large files
29//! 
30//! ## Generation Process
31//! 
32//! 1. **Schema Selection**: Choose DDEX version and namespace configuration
33//! 2. **Structure Generation**: Create hierarchical element structure
34//! 3. **Reference Linking**: Establish cross-references between elements
35//! 4. **Validation**: Ensure generated AST meets schema requirements
36//! 5. **Optimization**: Apply performance optimizations for large documents
37//! 
38//! ## Usage Example
39//! 
40//! ```rust
41//! use ddex_builder::generator::ASTGenerator;
42//! use ddex_builder::{BuildRequest, ReleaseRequest};
43//! 
44//! let mut generator = ASTGenerator::new("4.3".to_string());
45//! 
46//! let request = BuildRequest {
47//!     releases: vec![ReleaseRequest {
48//!         release_id: "R123".to_string(),
49//!         tracks: vec![/* track data */],
50//!         // ... other fields
51//!     }],
52//!     // ... other fields
53//! };
54//! 
55//! let ast = generator.generate(&request)?;
56//! // AST is now ready for XML serialization
57//! ```
58//! 
59//! ## Performance Characteristics
60//! 
61//! - **Small releases (< 10 tracks)**: ~1-2ms generation time
62//! - **Medium releases (10-50 tracks)**: ~5-8ms generation time  
63//! - **Large releases (50+ tracks)**: ~15-25ms generation time
64//! - **Memory usage**: ~2-5MB peak for typical releases
65//! 
66//! ## Error Handling
67//! 
68//! The generator validates input data and provides detailed error messages for:
69//! - Missing required fields
70//! - Invalid reference linkages
71//! - Schema constraint violations
72//! - Data format issues
73
74pub mod xml_writer;
75pub mod optimized_xml_writer;
76
77use crate::ast::{AST, Element}; // Removed unused Node import
78use crate::builder::{BuildRequest, ReleaseRequest};
79use crate::error::BuildError;
80use indexmap::IndexMap;
81
82pub struct ASTGenerator {
83    version: String,
84}
85
86impl ASTGenerator {
87    pub fn new(version: String) -> Self {
88        Self { version }
89    }
90    
91    pub fn generate(&mut self, request: &BuildRequest) -> Result<AST, BuildError> {
92        // Create root element based on version
93        let mut root = Element::new("NewReleaseMessage");
94        root.namespace = Some("ern".to_string());
95        
96        // Add version attributes
97        root.attributes.insert(
98            "MessageSchemaVersionId".to_string(),
99            format!("ern/{}", self.version),
100        );
101        
102        // Add MessageHeader
103        root.add_child(self.generate_message_header(request)?);
104        
105        // Add ResourceList
106        root.add_child(self.generate_resource_list(&request.releases)?);
107        
108        // Add ReleaseList
109        root.add_child(self.generate_release_list(&request.releases)?);
110        
111        // Create namespaces map
112        let mut namespaces = IndexMap::new();
113        namespaces.insert("ern".to_string(), format!("http://ddex.net/xml/ern/{}", self.version.replace('.', "")));
114        namespaces.insert("xsi".to_string(), "http://www.w3.org/2001/XMLSchema-instance".to_string());
115        
116        Ok(AST {
117            root,
118            namespaces,
119            schema_location: None,
120        })
121    }
122    
123    fn generate_message_header(&self, request: &BuildRequest) -> Result<Element, BuildError> {
124        let mut header = Element::new("MessageHeader");
125        
126        // Add MessageThreadId (using MessageId for now)
127        if let Some(ref msg_id) = request.header.message_id {
128            header.add_child(Element::new("MessageThreadId").with_text(msg_id));
129            header.add_child(Element::new("MessageId").with_text(msg_id));
130        }
131        
132        // Add MessageCreatedDateTime - use provided timestamp or current time
133        let created_time = request.header.message_created_date_time
134            .as_ref()
135            .map(|t| t.clone())
136            .unwrap_or_else(|| chrono::Utc::now().to_rfc3339());
137        
138        header.add_child(
139            Element::new("MessageCreatedDateTime")
140                .with_text(created_time)
141        );
142        
143        // Add MessageSender
144        header.add_child(self.generate_party("MessageSender", &request.header.message_sender)?);
145        
146        // Add MessageRecipient
147        header.add_child(self.generate_party("MessageRecipient", &request.header.message_recipient)?);
148        
149        Ok(header)
150    }
151    
152    fn generate_party(&self, element_name: &str, party: &crate::builder::PartyRequest) -> Result<Element, BuildError> {
153        let mut party_elem = Element::new(element_name);
154        
155        // Add PartyId if present
156        if let Some(ref party_id) = party.party_id {
157            party_elem.add_child(Element::new("PartyId").with_text(party_id));
158        }
159        
160        // Add PartyReference if present (for linker support)
161        if let Some(ref party_ref) = party.party_reference {
162            party_elem.add_child(Element::new("PartyReference").with_text(party_ref));
163        }
164        
165        // Add PartyName
166        for party_name in &party.party_name {
167            let mut name_elem = Element::new("PartyName");
168            if let Some(ref lang) = party_name.language_code {
169                name_elem.attributes.insert("LanguageCode".to_string(), lang.clone());
170            }
171            name_elem.add_text(&party_name.text);
172            party_elem.add_child(name_elem);
173        }
174        
175        Ok(party_elem)
176    }
177    
178    fn generate_resource_list(&self, releases: &[ReleaseRequest]) -> Result<Element, BuildError> {
179        let mut resource_list = Element::new("ResourceList");
180        
181        // Generate resources from all tracks in all releases
182        for release in releases {
183            for track in &release.tracks {
184                let mut sound_recording = Element::new("SoundRecording");
185                
186                // Add ResourceReference (use generated reference or create one)
187                // FIX: Create owned string instead of temporary
188                let resource_ref = track.resource_reference.clone()
189                    .unwrap_or_else(|| format!("A{}", track.track_id));
190                sound_recording.add_child(
191                    Element::new("ResourceReference").with_text(&resource_ref)
192                );
193                
194                // Add ResourceId with ISRC
195                let mut resource_id = Element::new("ResourceId");
196                resource_id.add_child(Element::new("ISRC").with_text(&track.isrc));
197                sound_recording.add_child(resource_id);
198                
199                // Add ReferenceTitle
200                let mut ref_title = Element::new("ReferenceTitle");
201                ref_title.add_child(Element::new("TitleText").with_text(&track.title));
202                sound_recording.add_child(ref_title);
203                
204                // Add Duration (already in ISO 8601 format as String)
205                sound_recording.add_child(
206                    Element::new("Duration").with_text(&track.duration)
207                );
208                
209                resource_list.add_child(sound_recording);
210            }
211        }
212        
213        Ok(resource_list)
214    }
215    
216    fn generate_release_list(&self, releases: &[ReleaseRequest]) -> Result<Element, BuildError> {
217        let mut release_list = Element::new("ReleaseList");
218        
219        for release in releases {
220            let mut release_elem = Element::new("Release");
221            
222            // Add ReleaseReference (use generated reference or create one)
223            // FIX: Create owned string instead of temporary
224            let release_ref = release.release_reference.clone()
225                .unwrap_or_else(|| format!("R{}", release.release_id));
226            release_elem.add_child(
227                Element::new("ReleaseReference").with_text(&release_ref)
228            );
229            
230            // Add ReleaseId
231            let mut release_id = Element::new("ReleaseId");
232            release_id.add_child(Element::new("GRid").with_text(&release.release_id));
233            release_elem.add_child(release_id);
234            
235            // Add Title(s)
236            if !release.title.is_empty() {
237                for title in &release.title {
238                    let mut title_elem = Element::new("ReferenceTitle");
239                    let mut title_text = Element::new("TitleText").with_text(&title.text);
240                    if let Some(ref lang) = title.language_code {
241                        title_text.attributes.insert("LanguageAndScriptCode".to_string(), lang.clone());
242                    }
243                    title_elem.add_child(title_text);
244                    release_elem.add_child(title_elem);
245                }
246            }
247            
248            // Add DisplayArtist
249            let mut display_artist_name = Element::new("DisplayArtistName");
250            display_artist_name.add_child(Element::new("FullName").with_text(&release.artist));
251            release_elem.add_child(display_artist_name);
252            
253            // Add Label if present
254            if let Some(ref label) = release.label {
255                let mut label_name = Element::new("LabelName");
256                label_name.add_child(Element::new("LabelName").with_text(label));
257                release_elem.add_child(label_name);
258            }
259            
260            // Add UPC if present  
261            if let Some(ref upc) = release.upc {
262                let mut release_id_upc = Element::new("ReleaseId");
263                release_id_upc.add_child(Element::new("ICPN").with_text(upc));
264                release_elem.add_child(release_id_upc);
265            }
266            
267            // Add ReleaseDate if present
268            if let Some(ref release_date) = release.release_date {
269                release_elem.add_child(
270                    Element::new("ReleaseDate").with_text(release_date)
271                );
272            }
273            
274            // Add ReleaseResourceReferences
275            if let Some(ref resource_refs) = release.resource_references {
276                for resource_ref in resource_refs {
277                    release_elem.add_child(
278                        Element::new("ReleaseResourceReference").with_text(resource_ref)
279                    );
280                }
281            } else {
282                // Auto-generate from tracks if not provided
283                for track in &release.tracks {
284                    // FIX: Create owned string instead of temporary
285                    let resource_ref = track.resource_reference.clone()
286                        .unwrap_or_else(|| format!("A{}", track.track_id));
287                    release_elem.add_child(
288                        Element::new("ReleaseResourceReference").with_text(&resource_ref)
289                    );
290                }
291            }
292            
293            release_list.add_child(release_elem);
294        }
295        
296        Ok(release_list)
297    }
298    
299    fn generate_deal_list(&self, deals: &[crate::builder::DealRequest]) -> Result<Element, BuildError> {
300        let mut deal_list = Element::new("DealList");
301        
302        for deal in deals {
303            let mut deal_elem = Element::new("ReleaseDeal");
304            
305            // Add DealReference if present
306            if let Some(ref deal_ref) = deal.deal_reference {
307                deal_elem.add_child(Element::new("DealReference").with_text(deal_ref));
308            }
309            
310            // Add Deal terms (simplified for now)
311            let mut deal_terms = Element::new("Deal");
312            deal_terms.add_child(
313                Element::new("CommercialModelType")
314                    .with_text(&deal.deal_terms.commercial_model_type)
315            );
316            
317            // Add territories
318            for territory in &deal.deal_terms.territory_code {
319                deal_terms.add_child(Element::new("TerritoryCode").with_text(territory));
320            }
321            
322            deal_elem.add_child(deal_terms);
323            
324            // Add DealReleaseReferences
325            for release_ref in &deal.release_references {
326                deal_elem.add_child(
327                    Element::new("DealReleaseReference").with_text(release_ref)
328                );
329            }
330            
331            deal_list.add_child(deal_elem);
332        }
333        
334        Ok(deal_list)
335    }
336}