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 optimized_xml_writer;
75pub mod xml_writer;
76
77use crate::ast::{Element, AST}; // Removed unused Node import
78use crate::builder::{BuildRequest, ReleaseRequest};
79use crate::error::BuildError;
80use indexmap::IndexMap;
81
82/// AST generator for converting build requests to abstract syntax trees
83pub struct ASTGenerator {
84    version: String,
85}
86
87impl ASTGenerator {
88    /// Create a new AST generator for the specified version
89    pub fn new(version: String) -> Self {
90        Self { version }
91    }
92
93    /// Generate an AST from a build request
94    pub fn generate(&mut self, request: &BuildRequest) -> Result<AST, BuildError> {
95        // Create root element based on version
96        let mut root = Element::new("NewReleaseMessage");
97        root.namespace = Some("ern".to_string());
98
99        // Add version attributes
100        root.attributes.insert(
101            "MessageSchemaVersionId".to_string(),
102            format!("ern/{}", self.version),
103        );
104
105        // Add MessageHeader
106        root.add_child(self.generate_message_header(request)?);
107
108        // Add ResourceList
109        root.add_child(self.generate_resource_list(&request.releases)?);
110
111        // Add ReleaseList
112        root.add_child(self.generate_release_list(&request.releases)?);
113
114        // Create namespaces map
115        let mut namespaces = IndexMap::new();
116        namespaces.insert(
117            "ern".to_string(),
118            format!("http://ddex.net/xml/ern/{}", self.version.replace('.', "")),
119        );
120        namespaces.insert(
121            "xsi".to_string(),
122            "http://www.w3.org/2001/XMLSchema-instance".to_string(),
123        );
124
125        Ok(AST {
126            root,
127            namespaces,
128            schema_location: None,
129        })
130    }
131
132    fn generate_message_header(&self, request: &BuildRequest) -> Result<Element, BuildError> {
133        let mut header = Element::new("MessageHeader");
134
135        // Add MessageThreadId (using MessageId for now)
136        if let Some(ref msg_id) = request.header.message_id {
137            header.add_child(Element::new("MessageThreadId").with_text(msg_id));
138            header.add_child(Element::new("MessageId").with_text(msg_id));
139        }
140
141        // Add MessageCreatedDateTime - use provided timestamp or current time
142        let created_time = request
143            .header
144            .message_created_date_time
145            .as_ref()
146            .map(|t| t.clone())
147            .unwrap_or_else(|| chrono::Utc::now().to_rfc3339());
148
149        header.add_child(Element::new("MessageCreatedDateTime").with_text(created_time));
150
151        // Add MessageSender
152        header.add_child(self.generate_party("MessageSender", &request.header.message_sender)?);
153
154        // Add MessageRecipient
155        header
156            .add_child(self.generate_party("MessageRecipient", &request.header.message_recipient)?);
157
158        Ok(header)
159    }
160
161    fn generate_party(
162        &self,
163        element_name: &str,
164        party: &crate::builder::PartyRequest,
165    ) -> Result<Element, BuildError> {
166        let mut party_elem = Element::new(element_name);
167
168        // Add PartyId if present
169        if let Some(ref party_id) = party.party_id {
170            party_elem.add_child(Element::new("PartyId").with_text(party_id));
171        }
172
173        // Add PartyReference if present (for linker support)
174        if let Some(ref party_ref) = party.party_reference {
175            party_elem.add_child(Element::new("PartyReference").with_text(party_ref));
176        }
177
178        // Add PartyName
179        for party_name in &party.party_name {
180            let mut name_elem = Element::new("PartyName");
181            if let Some(ref lang) = party_name.language_code {
182                name_elem
183                    .attributes
184                    .insert("LanguageCode".to_string(), lang.clone());
185            }
186            name_elem.add_text(&party_name.text);
187            party_elem.add_child(name_elem);
188        }
189
190        Ok(party_elem)
191    }
192
193    fn generate_resource_list(&self, releases: &[ReleaseRequest]) -> Result<Element, BuildError> {
194        let mut resource_list = Element::new("ResourceList");
195
196        // Generate resources from all tracks in all releases
197        for release in releases {
198            for track in &release.tracks {
199                let mut sound_recording = Element::new("SoundRecording");
200
201                // Add ResourceReference (use generated reference or create one)
202                // FIX: Create owned string instead of temporary
203                let resource_ref = track
204                    .resource_reference
205                    .clone()
206                    .unwrap_or_else(|| format!("A{}", track.track_id));
207                sound_recording
208                    .add_child(Element::new("ResourceReference").with_text(&resource_ref));
209
210                // Add ResourceId with ISRC
211                let mut resource_id = Element::new("ResourceId");
212                resource_id.add_child(Element::new("ISRC").with_text(&track.isrc));
213                sound_recording.add_child(resource_id);
214
215                // Add ReferenceTitle
216                let mut ref_title = Element::new("ReferenceTitle");
217                ref_title.add_child(Element::new("TitleText").with_text(&track.title));
218                sound_recording.add_child(ref_title);
219
220                // Add Duration (already in ISO 8601 format as String)
221                sound_recording.add_child(Element::new("Duration").with_text(&track.duration));
222
223                resource_list.add_child(sound_recording);
224            }
225        }
226
227        Ok(resource_list)
228    }
229
230    fn generate_release_list(&self, releases: &[ReleaseRequest]) -> Result<Element, BuildError> {
231        let mut release_list = Element::new("ReleaseList");
232
233        for release in releases {
234            let mut release_elem = Element::new("Release");
235
236            // Add ReleaseReference (use generated reference or create one)
237            // FIX: Create owned string instead of temporary
238            let release_ref = release
239                .release_reference
240                .clone()
241                .unwrap_or_else(|| format!("R{}", release.release_id));
242            release_elem.add_child(Element::new("ReleaseReference").with_text(&release_ref));
243
244            // Add ReleaseId
245            let mut release_id = Element::new("ReleaseId");
246            release_id.add_child(Element::new("GRid").with_text(&release.release_id));
247            release_elem.add_child(release_id);
248
249            // Add Title(s)
250            if !release.title.is_empty() {
251                for title in &release.title {
252                    let mut title_elem = Element::new("ReferenceTitle");
253                    let mut title_text = Element::new("TitleText").with_text(&title.text);
254                    if let Some(ref lang) = title.language_code {
255                        title_text
256                            .attributes
257                            .insert("LanguageAndScriptCode".to_string(), lang.clone());
258                    }
259                    title_elem.add_child(title_text);
260                    release_elem.add_child(title_elem);
261                }
262            }
263
264            // Add DisplayArtist
265            let mut display_artist_name = Element::new("DisplayArtistName");
266            display_artist_name.add_child(Element::new("FullName").with_text(&release.artist));
267            release_elem.add_child(display_artist_name);
268
269            // Add Label if present
270            if let Some(ref label) = release.label {
271                let mut label_name = Element::new("LabelName");
272                label_name.add_child(Element::new("LabelName").with_text(label));
273                release_elem.add_child(label_name);
274            }
275
276            // Add UPC if present
277            if let Some(ref upc) = release.upc {
278                let mut release_id_upc = Element::new("ReleaseId");
279                release_id_upc.add_child(Element::new("ICPN").with_text(upc));
280                release_elem.add_child(release_id_upc);
281            }
282
283            // Add ReleaseDate if present
284            if let Some(ref release_date) = release.release_date {
285                release_elem.add_child(Element::new("ReleaseDate").with_text(release_date));
286            }
287
288            // Add ReleaseResourceReferences
289            if let Some(ref resource_refs) = release.resource_references {
290                for resource_ref in resource_refs {
291                    release_elem.add_child(
292                        Element::new("ReleaseResourceReference").with_text(resource_ref),
293                    );
294                }
295            } else {
296                // Auto-generate from tracks if not provided
297                for track in &release.tracks {
298                    // FIX: Create owned string instead of temporary
299                    let resource_ref = track
300                        .resource_reference
301                        .clone()
302                        .unwrap_or_else(|| format!("A{}", track.track_id));
303                    release_elem.add_child(
304                        Element::new("ReleaseResourceReference").with_text(&resource_ref),
305                    );
306                }
307            }
308
309            release_list.add_child(release_elem);
310        }
311
312        Ok(release_list)
313    }
314
315    #[allow(dead_code)]
316    fn generate_deal_list(
317        &self,
318        deals: &[crate::builder::DealRequest],
319    ) -> Result<Element, BuildError> {
320        let mut deal_list = Element::new("DealList");
321
322        for deal in deals {
323            let mut deal_elem = Element::new("ReleaseDeal");
324
325            // Add DealReference if present
326            if let Some(ref deal_ref) = deal.deal_reference {
327                deal_elem.add_child(Element::new("DealReference").with_text(deal_ref));
328            }
329
330            // Add Deal terms (simplified for now)
331            let mut deal_terms = Element::new("Deal");
332            deal_terms.add_child(
333                Element::new("CommercialModelType")
334                    .with_text(&deal.deal_terms.commercial_model_type),
335            );
336
337            // Add territories
338            for territory in &deal.deal_terms.territory_code {
339                deal_terms.add_child(Element::new("TerritoryCode").with_text(territory));
340            }
341
342            deal_elem.add_child(deal_terms);
343
344            // Add DealReleaseReferences
345            for release_ref in &deal.release_references {
346                deal_elem.add_child(Element::new("DealReleaseReference").with_text(release_ref));
347            }
348
349            deal_list.add_child(deal_elem);
350        }
351
352        Ok(deal_list)
353    }
354}