pub use super::preflight::PreflightLevel;
use crate::generator::{xml_writer::XmlWriter, ASTGenerator};
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuildRequest {
pub header: MessageHeaderRequest,
pub version: String,
pub profile: Option<String>,
pub releases: Vec<ReleaseRequest>,
pub deals: Vec<DealRequest>,
pub extensions: Option<IndexMap<String, String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MessageHeaderRequest {
pub message_id: Option<String>,
pub message_sender: PartyRequest,
pub message_recipient: PartyRequest,
pub message_control_type: Option<String>,
pub message_created_date_time: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PartyRequest {
pub party_name: Vec<LocalizedStringRequest>,
pub party_id: Option<String>,
pub party_reference: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LocalizedStringRequest {
pub text: String,
pub language_code: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReleaseRequest {
pub release_id: String,
pub release_reference: Option<String>,
pub title: Vec<LocalizedStringRequest>,
pub artist: String,
pub label: Option<String>,
pub release_date: Option<String>,
pub upc: Option<String>,
pub tracks: Vec<TrackRequest>,
pub resource_references: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrackRequest {
pub track_id: String,
pub resource_reference: Option<String>,
pub isrc: String,
pub title: String,
pub duration: String,
pub artist: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DealRequest {
pub deal_reference: Option<String>,
pub deal_terms: DealTerms,
pub release_references: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DealTerms {
pub commercial_model_type: String,
pub territory_code: Vec<String>,
pub start_date: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuildOptions {
pub determinism: Option<super::determinism::DeterminismConfig>,
pub preflight_level: super::preflight::PreflightLevel,
pub id_strategy: IdStrategy,
pub stable_hash_config: Option<super::id_generator::StableHashConfig>,
}
impl Default for BuildOptions {
fn default() -> Self {
Self {
determinism: None,
preflight_level: super::preflight::PreflightLevel::Warn,
id_strategy: IdStrategy::UUID,
stable_hash_config: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum IdStrategy {
UUID,
UUIDv7,
Sequential,
StableHash,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuildResult {
pub xml: String,
pub warnings: Vec<BuildWarning>,
pub errors: Vec<super::error::BuildError>,
pub statistics: BuildStatistics,
pub canonical_hash: Option<String>,
pub reproducibility_banner: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuildWarning {
pub code: String,
pub message: String,
pub location: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuildStatistics {
pub releases: usize,
pub tracks: usize,
pub deals: usize,
pub generation_time_ms: u64,
pub xml_size_bytes: usize,
}
impl Default for BuildStatistics {
fn default() -> Self {
Self {
releases: 0,
tracks: 0,
deals: 0,
generation_time_ms: 0,
xml_size_bytes: 0,
}
}
}
pub struct DDEXBuilder {
_inner: super::Builder,
}
impl DDEXBuilder {
pub fn new() -> Self {
Self {
_inner: super::Builder::new(),
}
}
pub fn build(
&self,
mut request: BuildRequest,
options: BuildOptions,
) -> Result<BuildResult, super::error::BuildError> {
let start = std::time::Instant::now();
let mut warnings = Vec::new();
let validator =
super::preflight::PreflightValidator::new(super::preflight::ValidationConfig {
level: options.preflight_level,
profile: request.profile.clone(),
validate_identifiers: true,
validate_checksums: true,
check_required_fields: true,
validate_dates: true,
validate_references: true,
});
let validation_result = validator.validate(&request)?;
for warning in validation_result.warnings {
warnings.push(BuildWarning {
code: warning.code,
message: warning.message,
location: Some(warning.location),
});
}
if !validation_result.passed {
if options.preflight_level == super::preflight::PreflightLevel::Strict {
return Err(super::error::BuildError::ValidationFailed {
errors: validation_result
.errors
.iter()
.map(|e| format!("{}: {}", e.code, e.message))
.collect(),
});
}
}
self.generate_ids(&mut request, &options)?;
let mut generator = ASTGenerator::new(request.version.clone());
let ast = generator.generate(&request)?;
let config = options.determinism.unwrap_or_default();
let writer = XmlWriter::new(config.clone());
let xml = writer.write(&ast)?;
let (final_xml, canonical_hash) =
if config.canon_mode == super::determinism::CanonMode::DbC14n {
let canonicalizer = super::canonical::DB_C14N::new(config.clone());
let canonical = canonicalizer.canonicalize(&xml)?;
let hash = Some(canonicalizer.canonical_hash(&canonical)?);
(canonical, hash)
} else {
(xml, None)
};
let reproducibility_banner = if config.emit_reproducibility_banner {
Some(format!(
"Generated by DDEX Builder v{} with DB-C14N/{}",
env!("CARGO_PKG_VERSION"),
super::DB_C14N_VERSION
))
} else {
None
};
let elapsed = start.elapsed();
Ok(BuildResult {
xml: final_xml.clone(),
warnings,
errors: Vec::new(),
statistics: BuildStatistics {
releases: request.releases.len(),
tracks: request.releases.iter().map(|r| r.tracks.len()).sum(),
deals: request.deals.len(),
generation_time_ms: elapsed.as_millis() as u64,
xml_size_bytes: final_xml.len(),
},
canonical_hash,
reproducibility_banner,
})
}
fn generate_ids(
&self,
request: &mut BuildRequest,
options: &BuildOptions,
) -> Result<(), super::error::BuildError> {
match options.id_strategy {
IdStrategy::UUID => {
self.generate_uuid_ids(request)?;
}
IdStrategy::UUIDv7 => {
self.generate_uuidv7_ids(request)?;
}
IdStrategy::Sequential => {
self.generate_sequential_ids(request)?;
}
IdStrategy::StableHash => {
self.generate_stable_hash_ids(request, options)?;
}
}
Ok(())
}
fn generate_uuid_ids(
&self,
request: &mut BuildRequest,
) -> Result<(), super::error::BuildError> {
use uuid::Uuid;
if request.header.message_id.is_none() {
request.header.message_id = Some(format!("MSG_{}", Uuid::new_v4()));
}
for release in &mut request.releases {
if release.release_reference.is_none() {
release.release_reference = Some(format!("R{}", Uuid::new_v4().simple()));
}
for track in &mut release.tracks {
if track.resource_reference.is_none() {
track.resource_reference = Some(format!("A{}", Uuid::new_v4().simple()));
}
}
}
for (idx, deal) in request.deals.iter_mut().enumerate() {
if deal.deal_reference.is_none() {
deal.deal_reference = Some(format!("D{}", idx + 1));
}
}
Ok(())
}
fn generate_uuidv7_ids(
&self,
request: &mut BuildRequest,
) -> Result<(), super::error::BuildError> {
self.generate_uuid_ids(request)
}
fn generate_sequential_ids(
&self,
request: &mut BuildRequest,
) -> Result<(), super::error::BuildError> {
if request.header.message_id.is_none() {
request.header.message_id = Some(format!("MSG_{}", chrono::Utc::now().timestamp()));
}
for (idx, release) in request.releases.iter_mut().enumerate() {
if release.release_reference.is_none() {
release.release_reference = Some(format!("R{}", idx + 1));
}
for (track_idx, track) in release.tracks.iter_mut().enumerate() {
if track.resource_reference.is_none() {
track.resource_reference = Some(format!("A{}", (idx * 1000) + track_idx + 1));
}
}
}
for (idx, deal) in request.deals.iter_mut().enumerate() {
if deal.deal_reference.is_none() {
deal.deal_reference = Some(format!("D{}", idx + 1));
}
}
Ok(())
}
fn generate_stable_hash_ids(
&self,
request: &mut BuildRequest,
options: &BuildOptions,
) -> Result<(), super::error::BuildError> {
let config = options.stable_hash_config.clone().unwrap_or_default();
let mut id_gen = super::id_generator::StableHashGenerator::new(config);
if request.header.message_id.is_none() {
let sender_name = request
.header
.message_sender
.party_name
.first()
.map(|s| s.text.clone())
.unwrap_or_default();
let recipient_name = request
.header
.message_recipient
.party_name
.first()
.map(|s| s.text.clone())
.unwrap_or_default();
let msg_id = id_gen.generate_party_id(
&format!("{}-{}", sender_name, recipient_name),
"MessageHeader",
&[chrono::Utc::now().format("%Y%m%d").to_string()],
)?;
request.header.message_id = Some(msg_id);
}
for release in &mut request.releases {
if release.release_reference.is_none() {
let id = id_gen.generate_release_id(
release.upc.as_deref().unwrap_or(&release.release_id),
"Album",
&release
.tracks
.iter()
.map(|t| t.isrc.clone())
.collect::<Vec<_>>(),
&[], )?;
release.release_reference = Some(id);
}
for track in &mut release.tracks {
if track.resource_reference.is_none() {
let duration_seconds =
self.parse_duration_to_seconds(&track.duration).unwrap_or(0);
let id = id_gen.generate_resource_id(
&track.isrc,
duration_seconds,
None, )?;
track.resource_reference = Some(id);
}
}
}
for (_idx, deal) in request.deals.iter_mut().enumerate() {
if deal.deal_reference.is_none() {
let territories = deal.deal_terms.territory_code.join(",");
deal.deal_reference = Some(format!(
"DEAL_{}_{}",
deal.deal_terms.commercial_model_type, territories
));
}
}
Ok(())
}
fn parse_duration_to_seconds(&self, duration: &str) -> Option<u32> {
if !duration.starts_with("PT") {
return None;
}
let mut seconds = 0u32;
let mut current_num = String::new();
for ch in duration[2..].chars() {
match ch {
'0'..='9' => current_num.push(ch),
'H' => {
if let Ok(hours) = current_num.parse::<u32>() {
seconds += hours * 3600;
}
current_num.clear();
}
'M' => {
if let Ok(minutes) = current_num.parse::<u32>() {
seconds += minutes * 60;
}
current_num.clear();
}
'S' => {
if let Ok(secs) = current_num.parse::<u32>() {
seconds += secs;
}
current_num.clear();
}
_ => {}
}
}
Some(seconds)
}
#[allow(dead_code)]
fn preflight(
&self,
request: &BuildRequest,
level: super::preflight::PreflightLevel,
) -> Result<Vec<BuildWarning>, super::error::BuildError> {
let mut warnings = Vec::new();
if level == super::preflight::PreflightLevel::None {
return Ok(warnings);
}
if request.releases.is_empty() {
warnings.push(BuildWarning {
code: "NO_RELEASES".to_string(),
message: "No releases in request".to_string(),
location: Some("/releases".to_string()),
});
}
if level == super::preflight::PreflightLevel::Strict && !warnings.is_empty() {
return Err(super::error::BuildError::InvalidFormat {
field: "request".to_string(),
message: format!("{} validation warnings in strict mode", warnings.len()),
});
}
Ok(warnings)
}
pub fn diff_xml(
&self,
old_xml: &str,
new_xml: &str,
) -> Result<super::diff::types::ChangeSet, super::error::BuildError> {
self.diff_xml_with_config(old_xml, new_xml, super::diff::DiffConfig::default())
}
pub fn diff_xml_with_config(
&self,
old_xml: &str,
new_xml: &str,
config: super::diff::DiffConfig,
) -> Result<super::diff::types::ChangeSet, super::error::BuildError> {
let old_ast = self.parse_xml_to_ast(old_xml)?;
let new_ast = self.parse_xml_to_ast(new_xml)?;
let mut diff_engine = super::diff::DiffEngine::new_with_config(config);
diff_engine.diff(&old_ast, &new_ast)
}
pub fn diff_request_with_xml(
&self,
request: &BuildRequest,
existing_xml: &str,
) -> Result<super::diff::types::ChangeSet, super::error::BuildError> {
let build_result = self.build(request.clone(), BuildOptions::default())?;
self.diff_xml(existing_xml, &build_result.xml)
}
fn parse_xml_to_ast(&self, xml: &str) -> Result<super::ast::AST, super::error::BuildError> {
use quick_xml::Reader;
let mut reader = Reader::from_str(xml);
reader.config_mut().trim_text(true);
let mut root_element = super::ast::Element::new("Root");
let namespace_map = indexmap::IndexMap::new();
root_element = root_element.with_text(xml);
Ok(super::ast::AST {
root: root_element,
namespaces: namespace_map,
schema_location: None,
})
}
pub fn create_update(
&self,
original_xml: &str,
updated_xml: &str,
original_message_id: &str,
) -> Result<super::messages::UpdateReleaseMessage, super::error::BuildError> {
let mut update_generator = super::messages::UpdateGenerator::new();
update_generator.create_update(original_xml, updated_xml, original_message_id)
}
pub fn create_update_with_config(
&self,
original_xml: &str,
updated_xml: &str,
original_message_id: &str,
config: super::messages::UpdateConfig,
) -> Result<super::messages::UpdateReleaseMessage, super::error::BuildError> {
let mut update_generator = super::messages::UpdateGenerator::new_with_config(config);
update_generator.create_update(original_xml, updated_xml, original_message_id)
}
pub fn apply_update(
&self,
base_xml: &str,
update: &super::messages::UpdateReleaseMessage,
) -> Result<String, super::error::BuildError> {
let update_generator = super::messages::UpdateGenerator::new();
update_generator.apply_update(base_xml, update)
}
pub fn create_update_from_request(
&self,
existing_xml: &str,
request: &BuildRequest,
original_message_id: &str,
) -> Result<super::messages::UpdateReleaseMessage, super::error::BuildError> {
let build_result = self.build(request.clone(), BuildOptions::default())?;
self.create_update(existing_xml, &build_result.xml, original_message_id)
}
pub fn validate_update(
&self,
update: &super::messages::UpdateReleaseMessage,
) -> Result<super::messages::ValidationStatus, super::error::BuildError> {
let update_generator = super::messages::UpdateGenerator::new();
update_generator.validate_update(update)
}
pub fn serialize_update(
&self,
update: &super::messages::UpdateReleaseMessage,
) -> Result<String, super::error::BuildError> {
self.serialize_update_message_to_xml(update)
}
fn serialize_update_message_to_xml(
&self,
update: &super::messages::UpdateReleaseMessage,
) -> Result<String, super::error::BuildError> {
let mut xml = String::new();
xml.push_str(r#"<?xml version="1.0" encoding="UTF-8"?>"#);
xml.push('\n');
xml.push_str(r#"<UpdateReleaseMessage xmlns="http://ddex.net/xml/ern/43" MessageSchemaVersionId="ern/43">"#);
xml.push('\n');
self.serialize_update_header(&mut xml, &update.header)?;
self.serialize_update_metadata(&mut xml, &update.update_metadata)?;
self.serialize_update_list(&mut xml, &update.update_list)?;
if !update.resource_updates.is_empty() {
self.serialize_resource_updates(&mut xml, &update.resource_updates)?;
}
if !update.release_updates.is_empty() {
self.serialize_release_updates(&mut xml, &update.release_updates)?;
}
if !update.deal_updates.is_empty() {
self.serialize_deal_updates(&mut xml, &update.deal_updates)?;
}
xml.push_str("</UpdateReleaseMessage>\n");
Ok(xml)
}
fn serialize_update_header(
&self,
xml: &mut String,
header: &MessageHeaderRequest,
) -> Result<(), super::error::BuildError> {
xml.push_str(" <MessageHeader>\n");
if let Some(ref message_id) = header.message_id {
xml.push_str(&format!(
" <MessageId>{}</MessageId>\n",
self.escape_xml(message_id)
));
}
xml.push_str(" <MessageSender>\n");
if !header.message_sender.party_name.is_empty() {
xml.push_str(&format!(
" <PartyName>{}</PartyName>\n",
self.escape_xml(&header.message_sender.party_name[0].text)
));
}
xml.push_str(" </MessageSender>\n");
xml.push_str(" <MessageRecipient>\n");
if !header.message_recipient.party_name.is_empty() {
xml.push_str(&format!(
" <PartyName>{}</PartyName>\n",
self.escape_xml(&header.message_recipient.party_name[0].text)
));
}
xml.push_str(" </MessageRecipient>\n");
if let Some(ref created_time) = header.message_created_date_time {
xml.push_str(&format!(
" <MessageCreatedDateTime>{}</MessageCreatedDateTime>\n",
self.escape_xml(created_time)
));
} else {
let default_time = chrono::Utc::now().to_rfc3339();
xml.push_str(&format!(
" <MessageCreatedDateTime>{}</MessageCreatedDateTime>\n",
self.escape_xml(&default_time)
));
}
xml.push_str(" </MessageHeader>\n");
Ok(())
}
fn serialize_update_metadata(
&self,
xml: &mut String,
metadata: &super::messages::UpdateMetadata,
) -> Result<(), super::error::BuildError> {
xml.push_str(" <UpdateMetadata>\n");
xml.push_str(&format!(
" <OriginalMessageId>{}</OriginalMessageId>\n",
self.escape_xml(&metadata.original_message_id)
));
xml.push_str(&format!(
" <UpdateSequence>{}</UpdateSequence>\n",
metadata.update_sequence
));
xml.push_str(&format!(
" <TotalOperations>{}</TotalOperations>\n",
metadata.total_operations
));
xml.push_str(&format!(
" <ImpactLevel>{}</ImpactLevel>\n",
self.escape_xml(&metadata.impact_level)
));
xml.push_str(&format!(
" <ValidationStatus>{}</ValidationStatus>\n",
metadata.validation_status
));
xml.push_str(&format!(
" <UpdateCreatedDateTime>{}</UpdateCreatedDateTime>\n",
metadata.update_created_timestamp.to_rfc3339()
));
xml.push_str(" </UpdateMetadata>\n");
Ok(())
}
fn serialize_update_list(
&self,
xml: &mut String,
operations: &[super::messages::UpdateOperation],
) -> Result<(), super::error::BuildError> {
xml.push_str(" <UpdateList>\n");
for operation in operations {
xml.push_str(" <UpdateOperation>\n");
xml.push_str(&format!(
" <OperationId>{}</OperationId>\n",
self.escape_xml(&operation.operation_id)
));
xml.push_str(&format!(" <Action>{}</Action>\n", operation.action));
xml.push_str(&format!(
" <TargetPath>{}</TargetPath>\n",
self.escape_xml(&operation.target_path)
));
xml.push_str(&format!(
" <EntityType>{}</EntityType>\n",
operation.entity_type
));
xml.push_str(&format!(
" <EntityId>{}</EntityId>\n",
self.escape_xml(&operation.entity_id)
));
if let Some(ref old_value) = operation.old_value {
xml.push_str(&format!(
" <OldValue>{}</OldValue>\n",
self.escape_xml(old_value)
));
}
if let Some(ref new_value) = operation.new_value {
xml.push_str(&format!(
" <NewValue>{}</NewValue>\n",
self.escape_xml(new_value)
));
}
xml.push_str(&format!(
" <IsCritical>{}</IsCritical>\n",
operation.is_critical
));
xml.push_str(&format!(
" <Description>{}</Description>\n",
self.escape_xml(&operation.description)
));
if !operation.dependencies.is_empty() {
xml.push_str(" <Dependencies>\n");
for dependency in &operation.dependencies {
xml.push_str(&format!(
" <Dependency>{}</Dependency>\n",
self.escape_xml(dependency)
));
}
xml.push_str(" </Dependencies>\n");
}
xml.push_str(" </UpdateOperation>\n");
}
xml.push_str(" </UpdateList>\n");
Ok(())
}
fn serialize_resource_updates(
&self,
xml: &mut String,
resource_updates: &indexmap::IndexMap<String, super::messages::ResourceUpdate>,
) -> Result<(), super::error::BuildError> {
xml.push_str(" <ResourceUpdates>\n");
for (resource_id, update) in resource_updates {
xml.push_str(" <ResourceUpdate>\n");
xml.push_str(&format!(
" <ResourceId>{}</ResourceId>\n",
self.escape_xml(resource_id)
));
xml.push_str(&format!(
" <ResourceReference>{}</ResourceReference>\n",
self.escape_xml(&update.resource_reference)
));
xml.push_str(&format!(" <Action>{}</Action>\n", update.action));
if let Some(ref data) = update.resource_data {
xml.push_str(" <ResourceData>\n");
xml.push_str(&format!(
" <Type>{}</Type>\n",
self.escape_xml(&data.resource_type)
));
xml.push_str(&format!(
" <Title>{}</Title>\n",
self.escape_xml(&data.title)
));
xml.push_str(&format!(
" <Artist>{}</Artist>\n",
self.escape_xml(&data.artist)
));
if let Some(ref isrc) = data.isrc {
xml.push_str(&format!(" <ISRC>{}</ISRC>\n", self.escape_xml(isrc)));
}
if let Some(ref duration) = data.duration {
xml.push_str(&format!(
" <Duration>{}</Duration>\n",
self.escape_xml(duration)
));
}
xml.push_str(" </ResourceData>\n");
}
xml.push_str(" </ResourceUpdate>\n");
}
xml.push_str(" </ResourceUpdates>\n");
Ok(())
}
fn serialize_release_updates(
&self,
xml: &mut String,
release_updates: &indexmap::IndexMap<String, super::messages::ReleaseUpdate>,
) -> Result<(), super::error::BuildError> {
xml.push_str(" <ReleaseUpdates>\n");
for (release_id, update) in release_updates {
xml.push_str(" <ReleaseUpdate>\n");
xml.push_str(&format!(
" <ReleaseId>{}</ReleaseId>\n",
self.escape_xml(release_id)
));
xml.push_str(&format!(
" <ReleaseReference>{}</ReleaseReference>\n",
self.escape_xml(&update.release_reference)
));
xml.push_str(&format!(" <Action>{}</Action>\n", update.action));
if let Some(ref data) = update.release_data {
xml.push_str(" <ReleaseData>\n");
xml.push_str(&format!(
" <Type>{}</Type>\n",
self.escape_xml(&data.release_type)
));
xml.push_str(&format!(
" <Title>{}</Title>\n",
self.escape_xml(&data.title)
));
xml.push_str(&format!(
" <Artist>{}</Artist>\n",
self.escape_xml(&data.artist)
));
if let Some(ref label) = data.label {
xml.push_str(&format!(
" <Label>{}</Label>\n",
self.escape_xml(label)
));
}
if let Some(ref upc) = data.upc {
xml.push_str(&format!(" <UPC>{}</UPC>\n", self.escape_xml(upc)));
}
xml.push_str(" </ReleaseData>\n");
}
xml.push_str(" </ReleaseUpdate>\n");
}
xml.push_str(" </ReleaseUpdates>\n");
Ok(())
}
fn serialize_deal_updates(
&self,
xml: &mut String,
deal_updates: &indexmap::IndexMap<String, super::messages::DealUpdate>,
) -> Result<(), super::error::BuildError> {
xml.push_str(" <DealUpdates>\n");
for (deal_id, update) in deal_updates {
xml.push_str(" <DealUpdate>\n");
xml.push_str(&format!(
" <DealId>{}</DealId>\n",
self.escape_xml(deal_id)
));
xml.push_str(&format!(
" <DealReference>{}</DealReference>\n",
self.escape_xml(&update.deal_reference)
));
xml.push_str(&format!(" <Action>{}</Action>\n", update.action));
xml.push_str(" </DealUpdate>\n");
}
xml.push_str(" </DealUpdates>\n");
Ok(())
}
fn escape_xml(&self, text: &str) -> String {
text.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
}
impl Default for DDEXBuilder {
fn default() -> Self {
Self::new()
}
}