1use 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
20type ValidationFunction = fn(&AttributeValue) -> Result<(), String>;
22
23#[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#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109pub enum ValidationRule {
110 Required,
112 Type(AttributeType),
114 Enum(Vec<String>),
116 Regex(String),
118 Uri,
120 Email,
122 Date,
124 DateTime,
126 Range { min: Option<i64>, max: Option<i64> },
128 Length {
130 min: Option<usize>,
131 max: Option<usize>,
132 },
133 Custom(String),
135 Dependency {
137 depends_on: QName,
138 condition: DependencyCondition,
139 },
140}
141
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
144pub enum DependencyCondition {
145 RequiredWhenExists,
147 RequiredWhenEquals(String),
149 ForbiddenWhenExists,
151 MustMatch,
153 MustDiffer,
155}
156
157#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
159pub struct ValidationPolicy {
160 pub allow_unknown_attributes: bool,
162 pub strict_mode: bool,
164 pub validate_namespaces: bool,
166 pub namespace_policies: IndexMap<String, NamespacePolicy>,
168 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
186pub struct NamespacePolicy {
187 pub allowed: bool,
189 pub validate_format: bool,
191 pub custom_rules: IndexMap<String, Vec<ValidationRule>>,
193}
194
195#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
197pub struct ElementValidationConfig {
198 pub required_attributes: IndexSet<QName>,
200 pub forbidden_attributes: IndexSet<QName>,
202 pub custom_rules: IndexMap<QName, Vec<ValidationRule>>,
204}
205
206#[derive(Debug, Clone)]
208pub struct AttributeValidator {
209 global_rules: IndexMap<QName, Vec<ValidationRule>>,
211 element_rules: IndexMap<QName, IndexMap<QName, Vec<ValidationRule>>>,
213 policy: ValidationPolicy,
215 custom_validators: HashMap<String, ValidationFunction>,
217 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 pub fn with_policy(policy: ValidationPolicy) -> Self {
239 let mut validator = Self::new();
240 validator.policy = policy;
241 validator
242 }
243
244 pub fn add_global_rule(&mut self, attribute: QName, rule: ValidationRule) {
246 self.global_rules.entry(attribute).or_default().push(rule);
247 }
248
249 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 pub fn add_custom_validator<F>(&mut self, name: String, _validator: F)
261 where
262 F: Fn(&AttributeValue) -> Result<(), String> + 'static,
263 {
264 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 }
282 }
283 }
284
285 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 let element_rules = self
295 .element_rules
296 .get(element_name)
297 .cloned()
298 .unwrap_or_default();
299
300 self.validate_required_attributes(&element_rules, attributes, &mut result);
302
303 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 self.validate_cross_attributes(element_name, attributes, &mut result);
316
317 self.validate_policy_compliance(element_name, attributes, &mut result);
319
320 result
321 }
322
323 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 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 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 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 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 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 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 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 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 fn setup_ddex_rules(&mut self) {
793 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 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 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 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 fn validate_territory_code(value: &AttributeValue) -> Result<(), String> {
868 let code = value.to_string();
869 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 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 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 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 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 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 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 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 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 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 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}