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