adf 0.5.0

Minimal-overhead Auto-lead Data Format XML parser and writer
Documentation
use crate::error::Error;
use crate::model::{Adf, Contact, Customer, Prospect, Provider, Vehicle, Vendor};
use crate::validate::{Severity, ValidationReport};

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) struct DocumentStats {
    pub prospects: usize,
    pub vehicles: usize,
    pub contacts: usize,
    pub addresses: usize,
    pub extensions: usize,
}

impl DocumentStats {
    pub(crate) fn from_adf(adf: &Adf<'_>) -> Self {
        let mut stats = Self {
            prospects: adf.prospects.len(),
            extensions: adf.extensions.len(),
            ..Self::default()
        };
        for prospect in &adf.prospects {
            stats.add_prospect(prospect);
        }
        stats
    }

    fn add_prospect(&mut self, prospect: &Prospect<'_>) {
        self.vehicles += prospect.vehicles.len();
        self.extensions += prospect.extensions.len();

        for vehicle in &prospect.vehicles {
            self.add_vehicle(vehicle);
        }
        if let Some(customer) = &prospect.customer {
            self.add_customer(customer);
        }
        if let Some(vendor) = &prospect.vendor {
            self.add_vendor(vendor);
        }
        if let Some(provider) = &prospect.provider {
            self.add_provider(provider);
        }
    }

    fn add_vehicle(&mut self, vehicle: &Vehicle<'_>) {
        self.extensions += vehicle.extensions.len();
        for colors in &vehicle.color_combinations {
            self.extensions += colors.extensions.len();
        }
        for option in &vehicle.options {
            self.extensions += option.extensions.len();
        }
        if let Some(finance) = &vehicle.finance {
            self.extensions += finance.extensions.len();
        }
    }

    fn add_customer(&mut self, customer: &Customer<'_>) {
        self.contacts += customer.contacts.len();
        self.extensions += customer.extensions.len();
        if let Some(timeframe) = &customer.timeframe {
            self.extensions += timeframe.extensions.len();
        }
        for contact in &customer.contacts {
            self.add_contact(contact);
        }
    }

    fn add_vendor(&mut self, vendor: &Vendor<'_>) {
        self.contacts += vendor.contacts.len();
        self.extensions += vendor.extensions.len();
        for contact in &vendor.contacts {
            self.add_contact(contact);
        }
    }

    fn add_provider(&mut self, provider: &Provider<'_>) {
        self.contacts += provider.contacts.len();
        self.extensions += provider.extensions.len();
        for contact in &provider.contacts {
            self.add_contact(contact);
        }
    }

    fn add_contact(&mut self, contact: &Contact<'_>) {
        self.addresses += contact.addresses.len();
        self.extensions += contact.extensions.len();
        for address in &contact.addresses {
            self.extensions += address.extensions.len();
        }
    }
}

pub(crate) fn dirty_prospect_count(dirty_prospects: &[bool]) -> usize {
    dirty_prospects.iter().filter(|dirty| **dirty).count()
}

pub(crate) fn validation_issue_counts(report: &ValidationReport<'_>) -> (usize, usize) {
    let mut warnings = 0;
    let mut errors = 0;
    for issue in &report.issues {
        match issue.severity {
            Severity::Warning => warnings += 1,
            Severity::Error => errors += 1,
        }
    }
    (warnings, errors)
}

pub(crate) fn error_kind(error: &Error) -> &'static str {
    match error {
        Error::Xml { .. } => "xml",
        Error::Attribute { .. } => "attribute",
        Error::Encoding { .. } => "encoding",
        Error::Utf8 { .. } => "utf8",
        Error::MismatchedEnd { .. } => "mismatched_end",
        Error::UnexpectedEnd { .. } => "unexpected_end",
        Error::ContentOutsideRoot { .. } => "content_outside_root",
        Error::UnexpectedRoot { .. } => "unexpected_root",
        Error::InvalidCharacterReference { .. } => "invalid_character_reference",
        Error::InvalidEntityReference { .. } => "invalid_entity_reference",
        Error::IllegalCharacter { .. } => "illegal_character",
        Error::InvalidName { .. } => "invalid_name",
        Error::InvalidXmlToken { .. } => "invalid_xml_token",
        Error::DocTypeForbidden { .. } => "doctype_forbidden",
        Error::DocTypeTooLong { .. } => "doctype_too_long",
        Error::MissingRoot => "missing_root",
        Error::MultipleRoots => "multiple_roots",
        Error::Io(_) => "io",
    }
}

pub(crate) fn error_position(error: &Error) -> Option<u64> {
    match error {
        Error::Xml { position, .. }
        | Error::Attribute { position, .. }
        | Error::Encoding { position, .. }
        | Error::Utf8 { position, .. }
        | Error::MismatchedEnd { position, .. }
        | Error::UnexpectedEnd { position, .. }
        | Error::ContentOutsideRoot { position }
        | Error::UnexpectedRoot { position, .. }
        | Error::InvalidCharacterReference { position, .. }
        | Error::InvalidEntityReference { position, .. }
        | Error::IllegalCharacter { position, .. }
        | Error::DocTypeForbidden { position }
        | Error::DocTypeTooLong { position, .. } => Some(*position),
        Error::MissingRoot
        | Error::MultipleRoots
        | Error::InvalidName { .. }
        | Error::InvalidXmlToken { .. }
        | Error::Io(_) => None,
    }
}

pub(crate) fn record_error(operation: &'static str, error: &Error) {
    tracing::debug!(
        operation,
        error_kind = error_kind(error),
        error_position = ?error_position(error),
        "ADF operation failed"
    );
}