ddex_core/models/
validation.rs

1//! # Comprehensive Attribute Validation System
2//!
3//! This module provides validation for XML attributes including:
4//! - Required attribute validation
5//! - Format validation (URIs, dates, enums, etc.)
6//! - Custom attribute policy enforcement
7//! - DDEX-specific validation rules
8//! - Cross-attribute validation
9//! - Namespace-aware validation
10
11use crate::models::{AttributeMap, AttributeType, AttributeValue, QName};
12use chrono::{DateTime, NaiveDate, Utc};
13use indexmap::{IndexMap, IndexSet};
14use regex::Regex;
15use serde::{Deserialize, Serialize};
16use std::collections::HashMap;
17use thiserror::Error;
18use url::Url;
19
20/// Type alias for custom validation functions
21type ValidationFunction = fn(&AttributeValue) -> Result<(), String>;
22
23/// Comprehensive validation errors for attributes
24#[derive(Debug, Error, Clone, PartialEq, Eq, Serialize, Deserialize)]
25pub enum AttributeValidationError {
26    #[error("Missing required attribute: {attribute}")]
27    MissingRequired { attribute: QName },
28
29    #[error("Invalid format for attribute {attribute}: {reason}")]
30    InvalidFormat { attribute: QName, reason: String },
31
32    #[error("Invalid enum value for attribute {attribute}: '{value}', expected one of: {allowed_values:?}")]
33    InvalidEnumValue {
34        attribute: QName,
35        value: String,
36        allowed_values: Vec<String>,
37    },
38
39    #[error("Value out of range for attribute {attribute}: {value}, expected {range}")]
40    ValueOutOfRange {
41        attribute: QName,
42        value: String,
43        range: String,
44    },
45
46    #[error("Invalid type for attribute {attribute}: expected {expected}, got {actual}")]
47    InvalidType {
48        attribute: QName,
49        expected: AttributeType,
50        actual: AttributeType,
51    },
52
53    #[error("Custom validation failed for attribute {attribute}: {rule}")]
54    CustomValidationFailed { attribute: QName, rule: String },
55
56    #[error("Cross-attribute validation failed: {message}")]
57    CrossAttributeValidationFailed { message: String },
58
59    #[error("Namespace validation failed for attribute {attribute}: {reason}")]
60    NamespaceValidationFailed { attribute: QName, reason: String },
61
62    #[error("Policy violation for attribute {attribute}: {policy}")]
63    PolicyViolation { attribute: QName, policy: String },
64}
65
66/// Validation result containing errors and warnings
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68pub struct ValidationResult {
69    pub errors: Vec<AttributeValidationError>,
70    pub warnings: Vec<String>,
71    pub is_valid: bool,
72}
73
74impl ValidationResult {
75    pub fn new() -> Self {
76        Self {
77            errors: Vec::new(),
78            warnings: Vec::new(),
79            is_valid: true,
80        }
81    }
82
83    pub fn add_error(&mut self, error: AttributeValidationError) {
84        self.errors.push(error);
85        self.is_valid = false;
86    }
87
88    pub fn add_warning(&mut self, warning: String) {
89        self.warnings.push(warning);
90    }
91
92    pub fn merge(&mut self, other: ValidationResult) {
93        self.errors.extend(other.errors);
94        self.warnings.extend(other.warnings);
95        if !other.is_valid {
96            self.is_valid = false;
97        }
98    }
99}
100
101impl Default for ValidationResult {
102    fn default() -> Self {
103        Self::new()
104    }
105}
106
107/// Attribute validation rule definition
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109pub enum ValidationRule {
110    /// Attribute is required
111    Required,
112    /// Must match specific type
113    Type(AttributeType),
114    /// Must be one of specified enum values
115    Enum(Vec<String>),
116    /// Must match regular expression
117    Regex(String),
118    /// Must be valid URI
119    Uri,
120    /// Must be valid email address
121    Email,
122    /// Must be valid ISO date (YYYY-MM-DD)
123    Date,
124    /// Must be valid ISO datetime
125    DateTime,
126    /// Numeric range validation (using i64 for Eq compliance)
127    Range { min: Option<i64>, max: Option<i64> },
128    /// String length validation
129    Length {
130        min: Option<usize>,
131        max: Option<usize>,
132    },
133    /// Custom validation function (by name)
134    Custom(String),
135    /// Cross-attribute dependency
136    Dependency {
137        depends_on: QName,
138        condition: DependencyCondition,
139    },
140}
141
142/// Dependency condition for cross-attribute validation
143#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
144pub enum DependencyCondition {
145    /// Required when dependent attribute exists
146    RequiredWhenExists,
147    /// Required when dependent attribute has specific value
148    RequiredWhenEquals(String),
149    /// Forbidden when dependent attribute exists
150    ForbiddenWhenExists,
151    /// Must have same value as dependent attribute
152    MustMatch,
153    /// Must have different value from dependent attribute
154    MustDiffer,
155}
156
157/// Validation policy for controlling attribute validation behavior
158#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
159pub struct ValidationPolicy {
160    /// Whether to allow unknown attributes
161    pub allow_unknown_attributes: bool,
162    /// Whether to treat warnings as errors
163    pub strict_mode: bool,
164    /// Whether to validate namespace declarations
165    pub validate_namespaces: bool,
166    /// Custom policies by namespace
167    pub namespace_policies: IndexMap<String, NamespacePolicy>,
168    /// Element-specific validation overrides
169    pub element_overrides: IndexMap<QName, ElementValidationConfig>,
170}
171
172impl Default for ValidationPolicy {
173    fn default() -> Self {
174        Self {
175            allow_unknown_attributes: true,
176            strict_mode: false,
177            validate_namespaces: true,
178            namespace_policies: IndexMap::new(),
179            element_overrides: IndexMap::new(),
180        }
181    }
182}
183
184/// Namespace-specific validation policy
185#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
186pub struct NamespacePolicy {
187    /// Whether attributes in this namespace are allowed
188    pub allowed: bool,
189    /// Whether to validate format for this namespace
190    pub validate_format: bool,
191    /// Custom validation rules for this namespace
192    pub custom_rules: IndexMap<String, Vec<ValidationRule>>,
193}
194
195/// Element-specific validation configuration
196#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
197pub struct ElementValidationConfig {
198    /// Required attributes for this element
199    pub required_attributes: IndexSet<QName>,
200    /// Forbidden attributes for this element
201    pub forbidden_attributes: IndexSet<QName>,
202    /// Custom validation rules
203    pub custom_rules: IndexMap<QName, Vec<ValidationRule>>,
204}
205
206/// Comprehensive attribute validator
207#[derive(Debug, Clone)]
208pub struct AttributeValidator {
209    /// Global validation rules by attribute name
210    global_rules: IndexMap<QName, Vec<ValidationRule>>,
211    /// Element-specific validation rules
212    element_rules: IndexMap<QName, IndexMap<QName, Vec<ValidationRule>>>,
213    /// Validation policy
214    policy: ValidationPolicy,
215    /// Custom validation functions
216    custom_validators: HashMap<String, ValidationFunction>,
217    /// Compiled regex cache
218    regex_cache: HashMap<String, Regex>,
219}
220
221impl AttributeValidator {
222    pub fn new() -> Self {
223        let mut validator = Self {
224            global_rules: IndexMap::new(),
225            element_rules: IndexMap::new(),
226            policy: ValidationPolicy::default(),
227            custom_validators: HashMap::new(),
228            regex_cache: HashMap::new(),
229        };
230
231        validator.setup_ddex_rules();
232        validator.setup_xml_schema_rules();
233        validator.setup_custom_validators();
234        validator
235    }
236
237    /// Create validator with specific policy
238    pub fn with_policy(policy: ValidationPolicy) -> Self {
239        let mut validator = Self::new();
240        validator.policy = policy;
241        validator
242    }
243
244    /// Add global validation rule for an attribute
245    pub fn add_global_rule(&mut self, attribute: QName, rule: ValidationRule) {
246        self.global_rules.entry(attribute).or_default().push(rule);
247    }
248
249    /// Add element-specific validation rule
250    pub fn add_element_rule(&mut self, element: QName, attribute: QName, rule: ValidationRule) {
251        self.element_rules
252            .entry(element)
253            .or_default()
254            .entry(attribute)
255            .or_default()
256            .push(rule);
257    }
258
259    /// Add custom validation function
260    pub fn add_custom_validator<F>(&mut self, name: String, _validator: F)
261    where
262        F: Fn(&AttributeValue) -> Result<(), String> + 'static,
263    {
264        // For now, we'll store the name and implement specific validators
265        // In a real implementation, we'd need a more complex system for dynamic functions
266        match name.as_str() {
267            "ddex_territory_code" => {
268                self.custom_validators
269                    .insert(name, Self::validate_territory_code);
270            }
271            "ddex_language_code" => {
272                self.custom_validators
273                    .insert(name, Self::validate_language_code);
274            }
275            "ddex_currency_code" => {
276                self.custom_validators
277                    .insert(name, Self::validate_currency_code);
278            }
279            _ => {
280                // Store placeholder - in real implementation would store actual function
281            }
282        }
283    }
284
285    /// Validate attributes for a specific element
286    pub fn validate_element_attributes(
287        &mut self,
288        element_name: &QName,
289        attributes: &AttributeMap,
290    ) -> ValidationResult {
291        let mut result = ValidationResult::new();
292
293        // Get applicable rules
294        let element_rules = self
295            .element_rules
296            .get(element_name)
297            .cloned()
298            .unwrap_or_default();
299
300        // Check required attributes first
301        self.validate_required_attributes(&element_rules, attributes, &mut result);
302
303        // Validate each attribute
304        for (attr_qname, attr_value) in attributes {
305            self.validate_single_attribute(
306                element_name,
307                attr_qname,
308                attr_value,
309                &element_rules,
310                &mut result,
311            );
312        }
313
314        // Cross-attribute validation
315        self.validate_cross_attributes(element_name, attributes, &mut result);
316
317        // Policy validation
318        self.validate_policy_compliance(element_name, attributes, &mut result);
319
320        result
321    }
322
323    /// Validate all attributes globally (no element context)
324    pub fn validate_global_attributes(&mut self, attributes: &AttributeMap) -> ValidationResult {
325        let mut result = ValidationResult::new();
326
327        for (attr_qname, attr_value) in attributes {
328            if let Some(rules) = self.global_rules.get(attr_qname).cloned() {
329                for rule in &rules {
330                    if let Err(error) = self.apply_validation_rule(attr_qname, attr_value, rule) {
331                        result.add_error(error);
332                    }
333                }
334            }
335        }
336
337        result
338    }
339
340    fn validate_required_attributes(
341        &self,
342        element_rules: &IndexMap<QName, Vec<ValidationRule>>,
343        attributes: &AttributeMap,
344        result: &mut ValidationResult,
345    ) {
346        for (attr_qname, rules) in element_rules {
347            if rules.contains(&ValidationRule::Required) && !attributes.contains_key(attr_qname) {
348                result.add_error(AttributeValidationError::MissingRequired {
349                    attribute: attr_qname.clone(),
350                });
351            }
352        }
353    }
354
355    fn validate_single_attribute(
356        &mut self,
357        _element_name: &QName,
358        attr_qname: &QName,
359        attr_value: &AttributeValue,
360        element_rules: &IndexMap<QName, Vec<ValidationRule>>,
361        result: &mut ValidationResult,
362    ) {
363        // Apply element-specific rules
364        if let Some(rules) = element_rules.get(attr_qname).cloned() {
365            for rule in &rules {
366                if let Err(error) = self.apply_validation_rule(attr_qname, attr_value, rule) {
367                    result.add_error(error);
368                }
369            }
370        }
371
372        // Apply global rules
373        if let Some(rules) = self.global_rules.get(attr_qname).cloned() {
374            for rule in &rules {
375                if let Err(error) = self.apply_validation_rule(attr_qname, attr_value, rule) {
376                    result.add_error(error);
377                }
378            }
379        }
380
381        // Namespace validation
382        if self.policy.validate_namespaces {
383            self.validate_namespace_compliance(attr_qname, result);
384        }
385    }
386
387    fn apply_validation_rule(
388        &mut self,
389        attr_qname: &QName,
390        attr_value: &AttributeValue,
391        rule: &ValidationRule,
392    ) -> Result<(), AttributeValidationError> {
393        match rule {
394            ValidationRule::Required => {
395                // This is handled separately in validate_required_attributes
396                Ok(())
397            }
398            ValidationRule::Type(expected_type) => {
399                let actual_type = self.get_attribute_type(attr_value);
400                if actual_type != *expected_type {
401                    Err(AttributeValidationError::InvalidType {
402                        attribute: attr_qname.clone(),
403                        expected: *expected_type,
404                        actual: actual_type,
405                    })
406                } else {
407                    Ok(())
408                }
409            }
410            ValidationRule::Enum(allowed_values) => {
411                let value_str = attr_value.to_string();
412                if !allowed_values.contains(&value_str) {
413                    Err(AttributeValidationError::InvalidEnumValue {
414                        attribute: attr_qname.clone(),
415                        value: value_str,
416                        allowed_values: allowed_values.clone(),
417                    })
418                } else {
419                    Ok(())
420                }
421            }
422            ValidationRule::Regex(pattern) => {
423                let regex = self.get_or_compile_regex(pattern)?;
424                let value_str = attr_value.to_string();
425                if !regex.is_match(&value_str) {
426                    Err(AttributeValidationError::InvalidFormat {
427                        attribute: attr_qname.clone(),
428                        reason: format!("does not match pattern: {}", pattern),
429                    })
430                } else {
431                    Ok(())
432                }
433            }
434            ValidationRule::Uri => {
435                let value_str = attr_value.to_string();
436                if Url::parse(&value_str).is_err() {
437                    Err(AttributeValidationError::InvalidFormat {
438                        attribute: attr_qname.clone(),
439                        reason: "invalid URI format".to_string(),
440                    })
441                } else {
442                    Ok(())
443                }
444            }
445            ValidationRule::Email => {
446                let value_str = attr_value.to_string();
447                let email_regex = self.get_or_compile_regex(r"^[^\s@]+@[^\s@]+\.[^\s@]+$")?;
448                if !email_regex.is_match(&value_str) {
449                    Err(AttributeValidationError::InvalidFormat {
450                        attribute: attr_qname.clone(),
451                        reason: "invalid email format".to_string(),
452                    })
453                } else {
454                    Ok(())
455                }
456            }
457            ValidationRule::Date => {
458                let value_str = attr_value.to_string();
459                if NaiveDate::parse_from_str(&value_str, "%Y-%m-%d").is_err() {
460                    Err(AttributeValidationError::InvalidFormat {
461                        attribute: attr_qname.clone(),
462                        reason: "invalid date format, expected YYYY-MM-DD".to_string(),
463                    })
464                } else {
465                    Ok(())
466                }
467            }
468            ValidationRule::DateTime => {
469                let value_str = attr_value.to_string();
470                if value_str.parse::<DateTime<Utc>>().is_err() {
471                    Err(AttributeValidationError::InvalidFormat {
472                        attribute: attr_qname.clone(),
473                        reason: "invalid datetime format, expected RFC3339".to_string(),
474                    })
475                } else {
476                    Ok(())
477                }
478            }
479            ValidationRule::Range { min, max } => {
480                self.validate_numeric_range(attr_qname, attr_value, *min, *max)
481            }
482            ValidationRule::Length { min, max } => {
483                self.validate_string_length(attr_qname, attr_value, *min, *max)
484            }
485            ValidationRule::Custom(validator_name) => {
486                if let Some(validator) = self.custom_validators.get(validator_name) {
487                    match validator(attr_value) {
488                        Ok(()) => Ok(()),
489                        Err(reason) => Err(AttributeValidationError::CustomValidationFailed {
490                            attribute: attr_qname.clone(),
491                            rule: reason,
492                        }),
493                    }
494                } else {
495                    Err(AttributeValidationError::CustomValidationFailed {
496                        attribute: attr_qname.clone(),
497                        rule: format!("Unknown validator: {}", validator_name),
498                    })
499                }
500            }
501            ValidationRule::Dependency {
502                depends_on: _,
503                condition: _,
504            } => {
505                // Dependencies are handled in validate_cross_attributes
506                Ok(())
507            }
508        }
509    }
510
511    fn validate_cross_attributes(
512        &self,
513        _element_name: &QName,
514        attributes: &AttributeMap,
515        result: &mut ValidationResult,
516    ) {
517        // Collect all dependency rules
518        let mut dependencies = Vec::new();
519
520        for rules in self.global_rules.values() {
521            for rule in rules {
522                if let ValidationRule::Dependency {
523                    depends_on,
524                    condition,
525                } = rule
526                {
527                    dependencies.push((depends_on, condition));
528                }
529            }
530        }
531
532        // Apply dependency rules
533        for (attr_qname, attr_rules) in &self.global_rules {
534            for rule in attr_rules {
535                if let ValidationRule::Dependency {
536                    depends_on,
537                    condition,
538                } = rule
539                {
540                    if let Err(error) =
541                        self.validate_dependency(attr_qname, depends_on, condition, attributes)
542                    {
543                        result.add_error(error);
544                    }
545                }
546            }
547        }
548    }
549
550    fn validate_dependency(
551        &self,
552        attr_qname: &QName,
553        depends_on: &QName,
554        condition: &DependencyCondition,
555        attributes: &AttributeMap,
556    ) -> Result<(), AttributeValidationError> {
557        let has_attr = attributes.contains_key(attr_qname);
558        let has_dependency = attributes.contains_key(depends_on);
559
560        match condition {
561            DependencyCondition::RequiredWhenExists => {
562                if has_dependency && !has_attr {
563                    Err(AttributeValidationError::CrossAttributeValidationFailed {
564                        message: format!(
565                            "Attribute {} is required when {} exists",
566                            attr_qname.local_name, depends_on.local_name
567                        ),
568                    })
569                } else {
570                    Ok(())
571                }
572            }
573            DependencyCondition::RequiredWhenEquals(value) => {
574                if let Some(dep_value) = attributes.get(depends_on) {
575                    if dep_value.to_string() == *value && !has_attr {
576                        Err(AttributeValidationError::CrossAttributeValidationFailed {
577                            message: format!(
578                                "Attribute {} is required when {} equals '{}'",
579                                attr_qname.local_name, depends_on.local_name, value
580                            ),
581                        })
582                    } else {
583                        Ok(())
584                    }
585                } else {
586                    Ok(())
587                }
588            }
589            DependencyCondition::ForbiddenWhenExists => {
590                if has_dependency && has_attr {
591                    Err(AttributeValidationError::CrossAttributeValidationFailed {
592                        message: format!(
593                            "Attribute {} is forbidden when {} exists",
594                            attr_qname.local_name, depends_on.local_name
595                        ),
596                    })
597                } else {
598                    Ok(())
599                }
600            }
601            DependencyCondition::MustMatch => {
602                if let (Some(attr_value), Some(dep_value)) =
603                    (attributes.get(attr_qname), attributes.get(depends_on))
604                {
605                    if attr_value.to_string() != dep_value.to_string() {
606                        Err(AttributeValidationError::CrossAttributeValidationFailed {
607                            message: format!(
608                                "Attribute {} must match {}",
609                                attr_qname.local_name, depends_on.local_name
610                            ),
611                        })
612                    } else {
613                        Ok(())
614                    }
615                } else {
616                    Ok(())
617                }
618            }
619            DependencyCondition::MustDiffer => {
620                if let (Some(attr_value), Some(dep_value)) =
621                    (attributes.get(attr_qname), attributes.get(depends_on))
622                {
623                    if attr_value.to_string() == dep_value.to_string() {
624                        Err(AttributeValidationError::CrossAttributeValidationFailed {
625                            message: format!(
626                                "Attribute {} must differ from {}",
627                                attr_qname.local_name, depends_on.local_name
628                            ),
629                        })
630                    } else {
631                        Ok(())
632                    }
633                } else {
634                    Ok(())
635                }
636            }
637        }
638    }
639
640    fn validate_policy_compliance(
641        &self,
642        _element_name: &QName,
643        attributes: &AttributeMap,
644        result: &mut ValidationResult,
645    ) {
646        if !self.policy.allow_unknown_attributes {
647            for attr_qname in attributes.keys() {
648                if !self.is_known_attribute(attr_qname) {
649                    let message =
650                        format!("Unknown attribute not allowed: {}", attr_qname.local_name);
651                    if self.policy.strict_mode {
652                        result.add_error(AttributeValidationError::PolicyViolation {
653                            attribute: attr_qname.clone(),
654                            policy: message.clone(),
655                        });
656                    } else {
657                        result.add_warning(message);
658                    }
659                }
660            }
661        }
662    }
663
664    fn validate_namespace_compliance(&self, attr_qname: &QName, result: &mut ValidationResult) {
665        if let Some(namespace_uri) = &attr_qname.namespace_uri {
666            if let Some(policy) = self.policy.namespace_policies.get(namespace_uri) {
667                if !policy.allowed {
668                    result.add_error(AttributeValidationError::NamespaceValidationFailed {
669                        attribute: attr_qname.clone(),
670                        reason: format!("Namespace {} not allowed", namespace_uri),
671                    });
672                }
673            }
674        }
675    }
676
677    // Helper methods
678
679    fn get_attribute_type(&self, value: &AttributeValue) -> AttributeType {
680        match value {
681            AttributeValue::String(_) => AttributeType::String,
682            AttributeValue::Boolean(_) => AttributeType::Boolean,
683            AttributeValue::Integer(_) => AttributeType::Integer,
684            AttributeValue::Decimal(_) => AttributeType::Decimal,
685            AttributeValue::Date(_) => AttributeType::Date,
686            AttributeValue::DateTime(_) => AttributeType::DateTime,
687            AttributeValue::Uri(_) => AttributeType::Uri,
688            AttributeValue::Language(_) => AttributeType::Language,
689            AttributeValue::Token(_) => AttributeType::Token,
690            AttributeValue::Duration(_) => AttributeType::Raw,
691            AttributeValue::Enum(_, _) => AttributeType::String,
692            AttributeValue::Raw(_) => AttributeType::Raw,
693        }
694    }
695
696    fn get_or_compile_regex(&mut self, pattern: &str) -> Result<&Regex, AttributeValidationError> {
697        if !self.regex_cache.contains_key(pattern) {
698            let regex =
699                Regex::new(pattern).map_err(|e| AttributeValidationError::InvalidFormat {
700                    attribute: QName::new("regex".to_string()),
701                    reason: format!("Invalid regex pattern: {}", e),
702                })?;
703            self.regex_cache.insert(pattern.to_string(), regex);
704        }
705        Ok(self.regex_cache.get(pattern).unwrap())
706    }
707
708    fn validate_numeric_range(
709        &self,
710        attr_qname: &QName,
711        attr_value: &AttributeValue,
712        min: Option<i64>,
713        max: Option<i64>,
714    ) -> Result<(), AttributeValidationError> {
715        let numeric_value = match attr_value {
716            AttributeValue::Integer(i) => *i,
717            AttributeValue::Decimal(d) => *d as i64,
718            _ => {
719                return Err(AttributeValidationError::InvalidFormat {
720                    attribute: attr_qname.clone(),
721                    reason: "not a numeric value".to_string(),
722                })
723            }
724        };
725
726        if let Some(min_val) = min {
727            if numeric_value < min_val {
728                return Err(AttributeValidationError::ValueOutOfRange {
729                    attribute: attr_qname.clone(),
730                    value: numeric_value.to_string(),
731                    range: format!(">= {}", min_val),
732                });
733            }
734        }
735
736        if let Some(max_val) = max {
737            if numeric_value > max_val {
738                return Err(AttributeValidationError::ValueOutOfRange {
739                    attribute: attr_qname.clone(),
740                    value: numeric_value.to_string(),
741                    range: format!("<= {}", max_val),
742                });
743            }
744        }
745
746        Ok(())
747    }
748
749    fn validate_string_length(
750        &self,
751        attr_qname: &QName,
752        attr_value: &AttributeValue,
753        min: Option<usize>,
754        max: Option<usize>,
755    ) -> Result<(), AttributeValidationError> {
756        let string_value = attr_value.to_string();
757        let length = string_value.len();
758
759        if let Some(min_len) = min {
760            if length < min_len {
761                return Err(AttributeValidationError::ValueOutOfRange {
762                    attribute: attr_qname.clone(),
763                    value: length.to_string(),
764                    range: format!(">= {} characters", min_len),
765                });
766            }
767        }
768
769        if let Some(max_len) = max {
770            if length > max_len {
771                return Err(AttributeValidationError::ValueOutOfRange {
772                    attribute: attr_qname.clone(),
773                    value: length.to_string(),
774                    range: format!("<= {} characters", max_len),
775                });
776            }
777        }
778
779        Ok(())
780    }
781
782    fn is_known_attribute(&self, attr_qname: &QName) -> bool {
783        self.global_rules.contains_key(attr_qname)
784            || self
785                .element_rules
786                .values()
787                .any(|element_rules| element_rules.contains_key(attr_qname))
788    }
789
790    // Setup methods for DDEX and XML Schema rules
791
792    fn setup_ddex_rules(&mut self) {
793        // DDEX-specific validation rules
794        self.add_global_rule(
795            QName::new("TerritoryCode".to_string()),
796            ValidationRule::Custom("ddex_territory_code".to_string()),
797        );
798
799        self.add_global_rule(
800            QName::new("LanguageAndScriptCode".to_string()),
801            ValidationRule::Custom("ddex_language_code".to_string()),
802        );
803
804        self.add_global_rule(
805            QName::new("CurrencyCode".to_string()),
806            ValidationRule::Custom("ddex_currency_code".to_string()),
807        );
808
809        // Sequence number validation
810        self.add_global_rule(
811            QName::new("SequenceNumber".to_string()),
812            ValidationRule::Range {
813                min: Some(1),
814                max: Some(999999),
815            },
816        );
817
818        // DDEX identifier patterns
819        self.add_global_rule(
820            QName::new("ISRC".to_string()),
821            ValidationRule::Regex(r"^[A-Z]{2}[A-Z0-9]{3}[0-9]{7}$".to_string()),
822        );
823
824        self.add_global_rule(
825            QName::new("ISWC".to_string()),
826            ValidationRule::Regex(r"^T-[0-9]{9}-[0-9]$".to_string()),
827        );
828    }
829
830    fn setup_xml_schema_rules(&mut self) {
831        // XML Schema instance attributes
832        let xsi_ns = "http://www.w3.org/2001/XMLSchema-instance";
833
834        self.add_global_rule(
835            QName::with_namespace("type".to_string(), xsi_ns.to_string()),
836            ValidationRule::Type(AttributeType::Token),
837        );
838
839        self.add_global_rule(
840            QName::with_namespace("nil".to_string(), xsi_ns.to_string()),
841            ValidationRule::Type(AttributeType::Boolean),
842        );
843
844        self.add_global_rule(
845            QName::with_namespace("schemaLocation".to_string(), xsi_ns.to_string()),
846            ValidationRule::Type(AttributeType::Uri),
847        );
848    }
849
850    fn setup_custom_validators(&mut self) {
851        self.add_custom_validator(
852            "ddex_territory_code".to_string(),
853            Self::validate_territory_code,
854        );
855        self.add_custom_validator(
856            "ddex_language_code".to_string(),
857            Self::validate_language_code,
858        );
859        self.add_custom_validator(
860            "ddex_currency_code".to_string(),
861            Self::validate_currency_code,
862        );
863    }
864
865    // Custom validation functions
866
867    fn validate_territory_code(value: &AttributeValue) -> Result<(), String> {
868        let code = value.to_string();
869        // ISO 3166-1 alpha-2 country codes (simplified validation)
870        if code.len() != 2 || !code.chars().all(|c| c.is_ascii_uppercase()) {
871            Err("Invalid territory code format, expected 2 uppercase letters".to_string())
872        } else {
873            Ok(())
874        }
875    }
876
877    fn validate_language_code(value: &AttributeValue) -> Result<(), String> {
878        let code = value.to_string();
879        // Simplified language code validation (ISO 639-1)
880        if code.len() < 2 || code.len() > 8 {
881            Err("Invalid language code format".to_string())
882        } else {
883            Ok(())
884        }
885    }
886
887    fn validate_currency_code(value: &AttributeValue) -> Result<(), String> {
888        let code = value.to_string();
889        // ISO 4217 currency codes
890        if code.len() != 3 || !code.chars().all(|c| c.is_ascii_uppercase()) {
891            Err("Invalid currency code format, expected 3 uppercase letters".to_string())
892        } else {
893            Ok(())
894        }
895    }
896}
897
898impl Default for AttributeValidator {
899    fn default() -> Self {
900        Self::new()
901    }
902}
903
904#[cfg(test)]
905mod tests {
906    use super::*;
907
908    #[test]
909    fn test_basic_validation() {
910        let mut validator = AttributeValidator::new();
911
912        // Add required attribute rule
913        let attr_name = QName::new("required_attr".to_string());
914        validator.add_global_rule(attr_name.clone(), ValidationRule::Required);
915
916        let attributes = AttributeMap::new();
917        let result = validator.validate_global_attributes(&attributes);
918
919        // Should not fail since we're not testing element-specific validation
920        assert!(result.is_valid);
921    }
922
923    #[test]
924    fn test_enum_validation() {
925        let mut validator = AttributeValidator::new();
926
927        let attr_name = QName::new("enum_attr".to_string());
928        validator.add_global_rule(
929            attr_name.clone(),
930            ValidationRule::Enum(vec!["value1".to_string(), "value2".to_string()]),
931        );
932
933        let mut attributes = AttributeMap::new();
934        attributes.insert(
935            attr_name.clone(),
936            AttributeValue::String("value1".to_string()),
937        );
938
939        let result = validator.validate_global_attributes(&attributes);
940        assert!(result.is_valid);
941
942        // Test invalid enum value
943        attributes.insert(
944            attr_name.clone(),
945            AttributeValue::String("invalid".to_string()),
946        );
947        let result = validator.validate_global_attributes(&attributes);
948        assert!(!result.is_valid);
949        assert_eq!(result.errors.len(), 1);
950    }
951
952    #[test]
953    fn test_regex_validation() {
954        let mut validator = AttributeValidator::new();
955
956        let attr_name = QName::new("pattern_attr".to_string());
957        validator.add_global_rule(
958            attr_name.clone(),
959            ValidationRule::Regex(r"^\d{4}-\d{2}-\d{2}$".to_string()),
960        );
961
962        let mut attributes = AttributeMap::new();
963        attributes.insert(
964            attr_name.clone(),
965            AttributeValue::String("2023-12-25".to_string()),
966        );
967
968        let result = validator.validate_global_attributes(&attributes);
969        assert!(result.is_valid);
970
971        // Test invalid pattern
972        attributes.insert(
973            attr_name.clone(),
974            AttributeValue::String("invalid-date".to_string()),
975        );
976        let result = validator.validate_global_attributes(&attributes);
977        assert!(!result.is_valid);
978    }
979
980    #[test]
981    fn test_uri_validation() {
982        let mut validator = AttributeValidator::new();
983
984        let attr_name = QName::new("uri_attr".to_string());
985        validator.add_global_rule(attr_name.clone(), ValidationRule::Uri);
986
987        let mut attributes = AttributeMap::new();
988        attributes.insert(
989            attr_name.clone(),
990            AttributeValue::String("https://example.com".to_string()),
991        );
992
993        let result = validator.validate_global_attributes(&attributes);
994        assert!(result.is_valid);
995
996        // Test invalid URI
997        attributes.insert(
998            attr_name.clone(),
999            AttributeValue::String("not-a-uri".to_string()),
1000        );
1001        let result = validator.validate_global_attributes(&attributes);
1002        assert!(!result.is_valid);
1003    }
1004
1005    #[test]
1006    fn test_range_validation() {
1007        let mut validator = AttributeValidator::new();
1008
1009        let attr_name = QName::new("numeric_attr".to_string());
1010        validator.add_global_rule(
1011            attr_name.clone(),
1012            ValidationRule::Range {
1013                min: Some(1),
1014                max: Some(100),
1015            },
1016        );
1017
1018        let mut attributes = AttributeMap::new();
1019        attributes.insert(attr_name.clone(), AttributeValue::Integer(50));
1020
1021        let result = validator.validate_global_attributes(&attributes);
1022        assert!(result.is_valid);
1023
1024        // Test out of range
1025        attributes.insert(attr_name.clone(), AttributeValue::Integer(150));
1026        let result = validator.validate_global_attributes(&attributes);
1027        assert!(!result.is_valid);
1028    }
1029
1030    #[test]
1031    fn test_custom_validation() {
1032        let mut validator = AttributeValidator::new();
1033
1034        let attr_name = QName::new("TerritoryCode".to_string());
1035        // Rule already added in setup_ddex_rules
1036
1037        let mut attributes = AttributeMap::new();
1038        attributes.insert(attr_name.clone(), AttributeValue::String("US".to_string()));
1039
1040        let result = validator.validate_global_attributes(&attributes);
1041        assert!(result.is_valid);
1042
1043        // Test invalid territory code
1044        attributes.insert(
1045            attr_name.clone(),
1046            AttributeValue::String("invalid".to_string()),
1047        );
1048        let result = validator.validate_global_attributes(&attributes);
1049        assert!(!result.is_valid);
1050    }
1051}