use std::collections::HashMap;
use std::sync::Arc;
use num_bigint::BigInt;
use once_cell::sync::Lazy;
use rust_decimal::Decimal;
use super::facets::{normalize_whitespace, FacetSet, WhitespaceMode};
use super::value::{
DateTimeValue, DateValue, DayTimeDurationValue, DurationValue, GDayValue, GMonthDayValue,
GMonthValue, GYearMonthValue, GYearValue, TimeValue, TimezoneOffset, XmlAtomicValue, XmlValue,
XmlValueKind, YearMonthDurationValue,
};
use super::{PrimitiveTypeCode, XmlTypeCode};
use crate::error::FacetError;
#[derive(Debug, Clone)]
pub enum ValidationError {
InvalidLexical {
value: String,
type_name: &'static str,
message: String,
},
FacetViolation(FacetError),
TypeError {
expected: XmlTypeCode,
actual: XmlTypeCode,
},
RangeError {
value: String,
type_name: &'static str,
},
}
impl std::fmt::Display for ValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidLexical {
value,
type_name,
message,
} => {
write!(f, "Invalid {} value '{}': {}", type_name, value, message)
}
Self::FacetViolation(e) => write!(f, "{}", e),
Self::TypeError { expected, actual } => {
write!(f, "Type error: expected {:?}, got {:?}", expected, actual)
}
Self::RangeError { value, type_name } => {
write!(f, "Value '{}' out of range for type {}", value, type_name)
}
}
}
}
impl std::error::Error for ValidationError {}
impl From<FacetError> for ValidationError {
fn from(e: FacetError) -> Self {
Self::FacetViolation(e)
}
}
pub type ValidationResult<T> = Result<T, ValidationError>;
macro_rules! enum_matches_value_space {
($typed:expr, $parse_fn:expr, $eq_fn:expr) => {
|s: &str| {
$parse_fn(&normalize_whitespace(s, WhitespaceMode::Collapse))
.ok()
.map(|e| $eq_fn($typed, e))
.unwrap_or(false)
}
};
}
pub trait TypeValidator: Send + Sync {
fn type_name(&self) -> &'static str;
fn type_code(&self) -> XmlTypeCode;
fn primitive_type(&self) -> PrimitiveTypeCode;
fn whitespace(&self) -> WhitespaceMode;
fn validate(&self, value: &str) -> ValidationResult<XmlValue>;
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue>;
fn facet_applicable(&self, facet: &str) -> bool;
}
pub struct ValidatorRegistry {
validators: HashMap<&'static str, Arc<dyn TypeValidator>>,
by_code: HashMap<XmlTypeCode, Arc<dyn TypeValidator>>,
}
impl ValidatorRegistry {
pub fn new() -> Self {
let mut registry = Self {
validators: HashMap::new(),
by_code: HashMap::new(),
};
registry.register(Arc::new(StringValidator));
registry.register(Arc::new(BooleanValidator));
registry.register(Arc::new(DecimalValidator));
registry.register(Arc::new(FloatValidator));
registry.register(Arc::new(DoubleValidator));
registry.register(Arc::new(IntegerValidator));
registry.register(Arc::new(DurationValidator));
registry.register(Arc::new(DateTimeValidator));
registry.register(Arc::new(DateValidator));
registry.register(Arc::new(TimeValidator));
registry.register(Arc::new(GYearMonthValidator));
registry.register(Arc::new(GYearValidator));
registry.register(Arc::new(GMonthDayValidator));
registry.register(Arc::new(GDayValidator));
registry.register(Arc::new(GMonthValidator));
registry.register(Arc::new(HexBinaryValidator));
registry.register(Arc::new(Base64BinaryValidator));
registry.register(Arc::new(AnyUriValidator));
registry.register(Arc::new(NormalizedStringValidator));
registry.register(Arc::new(TokenValidator));
registry.register(Arc::new(LanguageValidator));
registry.register(Arc::new(NmTokenValidator));
registry.register(Arc::new(NameValidator));
registry.register(Arc::new(NCNameValidator));
registry.register(Arc::new(IdValidator));
registry.register(Arc::new(IdRefValidator));
registry.register(Arc::new(EntityValidator));
registry.register(Arc::new(LongValidator));
registry.register(Arc::new(IntValidator));
registry.register(Arc::new(ShortValidator));
registry.register(Arc::new(ByteValidator));
registry.register(Arc::new(NonNegativeIntegerValidator));
registry.register(Arc::new(PositiveIntegerValidator));
registry.register(Arc::new(NonPositiveIntegerValidator));
registry.register(Arc::new(NegativeIntegerValidator));
registry.register(Arc::new(UnsignedLongValidator));
registry.register(Arc::new(UnsignedIntValidator));
registry.register(Arc::new(UnsignedShortValidator));
registry.register(Arc::new(UnsignedByteValidator));
registry.register(Arc::new(QNameValidator));
registry.register(Arc::new(NotationValidator));
registry.register(Arc::new(NmTokensValidator));
registry.register(Arc::new(IdRefsValidator));
registry.register(Arc::new(EntitiesValidator));
registry.register(Arc::new(YearMonthDurationValidator));
registry.register(Arc::new(DayTimeDurationValidator));
registry.register(Arc::new(DateTimeStampValidator));
registry
}
pub fn register(&mut self, validator: Arc<dyn TypeValidator>) {
let name = validator.type_name();
let code = validator.type_code();
self.validators.insert(name, validator.clone());
self.by_code.insert(code, validator);
}
pub fn get_by_name(&self, name: &str) -> Option<&dyn TypeValidator> {
self.validators.get(name).map(|v| v.as_ref())
}
pub fn get_by_code(&self, code: XmlTypeCode) -> Option<&dyn TypeValidator> {
self.by_code.get(&code).map(|v| v.as_ref())
}
pub fn validate(&self, type_code: XmlTypeCode, value: &str) -> ValidationResult<XmlValue> {
match self.get_by_code(type_code) {
Some(validator) => validator.validate(value),
None => Err(ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "unknown",
message: format!("No validator for type code {:?}", type_code),
}),
}
}
}
impl Default for ValidatorRegistry {
fn default() -> Self {
Self::new()
}
}
pub static VALIDATOR_REGISTRY: Lazy<ValidatorRegistry> = Lazy::new(ValidatorRegistry::new);
pub struct StringValidator;
impl TypeValidator for StringValidator {
fn type_name(&self) -> &'static str {
"string"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::String
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::String
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Preserve
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
Ok(XmlValue::string(value))
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let ws = facets
.whitespace
.as_ref()
.map(|w| w.value)
.unwrap_or(WhitespaceMode::Preserve);
let normalized = normalize_whitespace(value, ws);
facets.validate_string(&normalized)?;
Ok(XmlValue::string(normalized))
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"length" | "minLength" | "maxLength" | "pattern" | "enumeration" | "whitespace"
)
}
}
pub struct NormalizedStringValidator;
impl TypeValidator for NormalizedStringValidator {
fn type_name(&self) -> &'static str {
"normalizedString"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::NormalizedString
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::String
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Replace
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Replace);
Ok(XmlValue::new(
XmlTypeCode::NormalizedString,
XmlValueKind::Atomic(XmlAtomicValue::String(normalized)),
))
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let base_ws = facets
.whitespace
.as_ref()
.map(|w| w.value)
.unwrap_or(WhitespaceMode::Replace);
let ws = if matches!(base_ws, WhitespaceMode::Collapse) {
WhitespaceMode::Collapse
} else {
WhitespaceMode::Replace
};
let normalized = normalize_whitespace(value, ws);
facets.validate_string(&normalized)?;
Ok(XmlValue::new(
XmlTypeCode::NormalizedString,
XmlValueKind::Atomic(XmlAtomicValue::String(normalized)),
))
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"length" | "minLength" | "maxLength" | "pattern" | "enumeration" | "whitespace"
)
}
}
pub struct TokenValidator;
impl TypeValidator for TokenValidator {
fn type_name(&self) -> &'static str {
"token"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::Token
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::String
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
Ok(XmlValue::new(
XmlTypeCode::Token,
XmlValueKind::Atomic(XmlAtomicValue::String(normalized)),
))
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
facets.validate_string(&normalized)?;
Ok(XmlValue::new(
XmlTypeCode::Token,
XmlValueKind::Atomic(XmlAtomicValue::String(normalized)),
))
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"length" | "minLength" | "maxLength" | "pattern" | "enumeration" | "whitespace"
)
}
}
pub struct LanguageValidator;
impl TypeValidator for LanguageValidator {
fn type_name(&self) -> &'static str {
"language"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::Language
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::String
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
if !is_valid_language(&normalized) {
return Err(ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "language",
message: "Invalid language tag format".to_string(),
});
}
Ok(XmlValue::new(
XmlTypeCode::Language,
XmlValueKind::Atomic(XmlAtomicValue::String(normalized)),
))
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
facets.validate_string(&result.to_string_value())?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"length" | "minLength" | "maxLength" | "pattern" | "enumeration" | "whitespace"
)
}
}
pub struct NmTokenValidator;
impl TypeValidator for NmTokenValidator {
fn type_name(&self) -> &'static str {
"NMTOKEN"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::NmToken
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::String
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
if normalized.is_empty() || !normalized.chars().all(is_name_char) {
return Err(ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "NMTOKEN",
message: "Must contain only name characters".to_string(),
});
}
Ok(XmlValue::new(
XmlTypeCode::NmToken,
XmlValueKind::Atomic(XmlAtomicValue::String(normalized)),
))
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
facets.validate_string(&result.to_string_value())?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"length" | "minLength" | "maxLength" | "pattern" | "enumeration" | "whitespace"
)
}
}
pub struct NameValidator;
impl TypeValidator for NameValidator {
fn type_name(&self) -> &'static str {
"Name"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::Name
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::String
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
if !is_valid_name(&normalized) {
return Err(ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "Name",
message: "Invalid XML Name".to_string(),
});
}
Ok(XmlValue::new(
XmlTypeCode::Name,
XmlValueKind::Atomic(XmlAtomicValue::String(normalized)),
))
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
facets.validate_string(&result.to_string_value())?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"length" | "minLength" | "maxLength" | "pattern" | "enumeration" | "whitespace"
)
}
}
pub struct NCNameValidator;
impl TypeValidator for NCNameValidator {
fn type_name(&self) -> &'static str {
"NCName"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::NCName
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::String
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
if !is_valid_ncname(&normalized) {
return Err(ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "NCName",
message: "Invalid NCName (Name without colons)".to_string(),
});
}
Ok(XmlValue::new(
XmlTypeCode::NCName,
XmlValueKind::Atomic(XmlAtomicValue::String(normalized)),
))
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
facets.validate_string(&result.to_string_value())?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"length" | "minLength" | "maxLength" | "pattern" | "enumeration" | "whitespace"
)
}
}
pub struct IdValidator;
impl TypeValidator for IdValidator {
fn type_name(&self) -> &'static str {
"ID"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::Id
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::String
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
if !is_valid_ncname(&normalized) {
return Err(ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "ID",
message: "Invalid ID (must be NCName)".to_string(),
});
}
Ok(XmlValue::new(
XmlTypeCode::Id,
XmlValueKind::Atomic(XmlAtomicValue::String(normalized)),
))
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
facets.validate_string(&result.to_string_value())?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"length" | "minLength" | "maxLength" | "pattern" | "enumeration" | "whitespace"
)
}
}
pub struct IdRefValidator;
impl TypeValidator for IdRefValidator {
fn type_name(&self) -> &'static str {
"IDREF"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::IdRef
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::String
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
if !is_valid_ncname(&normalized) {
return Err(ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "IDREF",
message: "Invalid IDREF (must be NCName)".to_string(),
});
}
Ok(XmlValue::new(
XmlTypeCode::IdRef,
XmlValueKind::Atomic(XmlAtomicValue::String(normalized)),
))
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
facets.validate_string(&result.to_string_value())?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"length" | "minLength" | "maxLength" | "pattern" | "enumeration" | "whitespace"
)
}
}
pub struct EntityValidator;
impl TypeValidator for EntityValidator {
fn type_name(&self) -> &'static str {
"ENTITY"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::Entity
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::String
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
if !is_valid_ncname(&normalized) {
return Err(ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "ENTITY",
message: "Invalid ENTITY (must be NCName)".to_string(),
});
}
Ok(XmlValue::new(
XmlTypeCode::Entity,
XmlValueKind::Atomic(XmlAtomicValue::String(normalized)),
))
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
facets.validate_string(&result.to_string_value())?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"length" | "minLength" | "maxLength" | "pattern" | "enumeration" | "whitespace"
)
}
}
pub struct BooleanValidator;
impl TypeValidator for BooleanValidator {
fn type_name(&self) -> &'static str {
"boolean"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::Boolean
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::Boolean
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
match normalized.as_str() {
"true" | "1" => Ok(XmlValue::boolean(true)),
"false" | "0" => Ok(XmlValue::boolean(false)),
_ => Err(ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "boolean",
message: "Expected 'true', 'false', '1', or '0'".to_string(),
}),
}
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
facets.validate_patterns_only(value)?;
if let XmlValueKind::Atomic(XmlAtomicValue::Boolean(b)) = result.value {
facets.validate_enum_value_space(
|s| match normalize_whitespace(s, WhitespaceMode::Collapse).as_str() {
"true" | "1" => b,
"false" | "0" => !b,
_ => false,
},
value,
)?;
}
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(facet, "pattern" | "enumeration")
}
}
pub struct DecimalValidator;
impl TypeValidator for DecimalValidator {
fn type_name(&self) -> &'static str {
"decimal"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::Decimal
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::Decimal
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
normalized
.parse::<Decimal>()
.map(XmlValue::decimal)
.map_err(|e| ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "decimal",
message: e.to_string(),
})
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let Some(d) = result.as_decimal() {
facets.validate_decimal(&d)?;
}
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
facets.validate_string_patterns_enums(&normalized)?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"totalDigits"
| "fractionDigits"
| "minInclusive"
| "maxInclusive"
| "minExclusive"
| "maxExclusive"
| "pattern"
| "enumeration"
)
}
}
pub struct IntegerValidator;
impl TypeValidator for IntegerValidator {
fn type_name(&self) -> &'static str {
"integer"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::Integer
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::Decimal
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
normalized
.parse::<BigInt>()
.map(XmlValue::integer)
.map_err(|e| ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "integer",
message: e.to_string(),
})
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let Some(d) = result.as_decimal() {
facets.validate_decimal(&d)?;
}
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
facets.validate_string_patterns_enums(&normalized)?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"totalDigits"
| "fractionDigits"
| "minInclusive"
| "maxInclusive"
| "minExclusive"
| "maxExclusive"
| "pattern"
| "enumeration"
)
}
}
pub struct FloatValidator;
impl TypeValidator for FloatValidator {
fn type_name(&self) -> &'static str {
"float"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::Float
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::Float
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
parse_float(&normalized)
.map(XmlValue::float)
.map_err(|msg| ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "float",
message: msg,
})
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let XmlValueKind::Atomic(XmlAtomicValue::Float(f)) = result.value {
facets.validate_float(f)?;
facets.validate_enum_value_space(
enum_matches_value_space!(f, parse_float, float_eq),
value,
)?;
}
facets.validate_patterns_only(value)?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"minInclusive"
| "maxInclusive"
| "minExclusive"
| "maxExclusive"
| "pattern"
| "enumeration"
)
}
}
pub struct DoubleValidator;
impl TypeValidator for DoubleValidator {
fn type_name(&self) -> &'static str {
"double"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::Double
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::Double
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
parse_double(&normalized)
.map(XmlValue::double)
.map_err(|msg| ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "double",
message: msg,
})
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let XmlValueKind::Atomic(XmlAtomicValue::Double(d)) = result.value {
facets.validate_double(d)?;
facets.validate_enum_value_space(
enum_matches_value_space!(d, parse_double, double_eq),
value,
)?;
}
facets.validate_patterns_only(value)?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"minInclusive"
| "maxInclusive"
| "minExclusive"
| "maxExclusive"
| "pattern"
| "enumeration"
)
}
}
pub fn is_valid_xsd_float_lexical(s: &str) -> bool {
if s == "INF" || s == "-INF" || s == "+INF" || s == "NaN" {
return true;
}
let bytes = s.as_bytes();
if bytes.is_empty() {
return false;
}
let mut i = 0;
if bytes[i] == b'+' || bytes[i] == b'-' {
i += 1;
}
let mut has_int_digits = false;
while i < bytes.len() && bytes[i].is_ascii_digit() {
has_int_digits = true;
i += 1;
}
let mut has_frac_digits = false;
if i < bytes.len() && bytes[i] == b'.' {
i += 1;
while i < bytes.len() && bytes[i].is_ascii_digit() {
has_frac_digits = true;
i += 1;
}
}
if !has_int_digits && !has_frac_digits {
return false;
}
if i < bytes.len() && (bytes[i] == b'e' || bytes[i] == b'E') {
i += 1;
if i < bytes.len() && (bytes[i] == b'+' || bytes[i] == b'-') {
i += 1;
}
let mut has_exp_digits = false;
while i < bytes.len() && bytes[i].is_ascii_digit() {
has_exp_digits = true;
i += 1;
}
if !has_exp_digits {
return false;
}
}
i == bytes.len()
}
fn parse_float(s: &str) -> Result<f32, String> {
if !is_valid_xsd_float_lexical(s) {
return Err(format!("Invalid xs:float lexical form: '{}'", s));
}
match s {
"INF" | "+INF" => Ok(f32::INFINITY),
"-INF" => Ok(f32::NEG_INFINITY),
"NaN" => Ok(f32::NAN),
_ => s
.parse()
.map_err(|e: std::num::ParseFloatError| e.to_string()),
}
}
fn parse_double(s: &str) -> Result<f64, String> {
if !is_valid_xsd_float_lexical(s) {
return Err(format!("Invalid xs:double lexical form: '{}'", s));
}
match s {
"INF" | "+INF" => Ok(f64::INFINITY),
"-INF" => Ok(f64::NEG_INFINITY),
"NaN" => Ok(f64::NAN),
_ => s
.parse()
.map_err(|e: std::num::ParseFloatError| e.to_string()),
}
}
use crate::types::equality::{double_eq, float_eq};
fn datetime_value_eq(a: &DateTimeValue, b: &DateTimeValue) -> bool {
a.partial_cmp(b) == Some(std::cmp::Ordering::Equal)
}
fn date_value_eq(a: &DateValue, b: &DateValue) -> bool {
a.partial_cmp(b) == Some(std::cmp::Ordering::Equal)
}
fn time_value_eq(a: &TimeValue, b: &TimeValue) -> bool {
a.partial_cmp(b) == Some(std::cmp::Ordering::Equal)
}
fn duration_eq(a: &DurationValue, b: &DurationValue) -> bool {
let sign = |d: &DurationValue| if d.negative { -1i64 } else { 1 };
let total_months = |d: &DurationValue| d.years as i64 * 12 + d.months as i64;
if sign(a) * total_months(a) != sign(b) * total_months(b) {
return false;
}
let day_secs = |d: &DurationValue| {
let s = rust_decimal::Decimal::from(d.days as i64) * rust_decimal::Decimal::from(86400)
+ rust_decimal::Decimal::from(d.hours as i64) * rust_decimal::Decimal::from(3600)
+ rust_decimal::Decimal::from(d.minutes as i64) * rust_decimal::Decimal::from(60)
+ d.seconds;
if d.negative {
-s
} else {
s
}
};
day_secs(a) == day_secs(b)
}
fn year_month_duration_value_eq(a: &YearMonthDurationValue, b: &YearMonthDurationValue) -> bool {
a.partial_cmp(b) == Some(std::cmp::Ordering::Equal)
}
fn day_time_duration_value_eq(a: &DayTimeDurationValue, b: &DayTimeDurationValue) -> bool {
a.partial_cmp(b) == Some(std::cmp::Ordering::Equal)
}
fn validate_bounds<T: PartialOrd, F: Fn(&str) -> Option<T>>(
value: &T,
facets: &FacetSet,
_type_name: &str,
value_str: &str,
parse_fn: F,
) -> ValidationResult<()> {
if let Some(ref min) = facets.min_inclusive {
if let Some(bound) = parse_fn(&min.value) {
if value.partial_cmp(&bound) != Some(std::cmp::Ordering::Greater)
&& value.partial_cmp(&bound) != Some(std::cmp::Ordering::Equal)
{
return Err(ValidationError::FacetViolation(
FacetError::MinInclusiveViolation {
value: value_str.to_string(),
min: min.value.clone(),
},
));
}
}
}
if let Some(ref max) = facets.max_inclusive {
if let Some(bound) = parse_fn(&max.value) {
if value.partial_cmp(&bound) != Some(std::cmp::Ordering::Less)
&& value.partial_cmp(&bound) != Some(std::cmp::Ordering::Equal)
{
return Err(ValidationError::FacetViolation(
FacetError::MaxInclusiveViolation {
value: value_str.to_string(),
max: max.value.clone(),
},
));
}
}
}
if let Some(ref min) = facets.min_exclusive {
if let Some(bound) = parse_fn(&min.value) {
if value.partial_cmp(&bound) != Some(std::cmp::Ordering::Greater) {
return Err(ValidationError::FacetViolation(
FacetError::MinExclusiveViolation {
value: value_str.to_string(),
min: min.value.clone(),
},
));
}
}
}
if let Some(ref max) = facets.max_exclusive {
if let Some(bound) = parse_fn(&max.value) {
if value.partial_cmp(&bound) != Some(std::cmp::Ordering::Less) {
return Err(ValidationError::FacetViolation(
FacetError::MaxExclusiveViolation {
value: value_str.to_string(),
max: max.value.clone(),
},
));
}
}
}
Ok(())
}
pub struct DurationValidator;
impl TypeValidator for DurationValidator {
fn type_name(&self) -> &'static str {
"duration"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::Duration
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::Duration
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
parse_duration(&normalized).map(|d| {
XmlValue::new(
XmlTypeCode::Duration,
XmlValueKind::Atomic(XmlAtomicValue::Duration(d)),
)
})
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let XmlValueKind::Atomic(XmlAtomicValue::Duration(ref dur)) = result.value {
validate_bounds(dur, facets, "duration", value, |s| {
parse_duration(&normalize_whitespace(s, WhitespaceMode::Collapse)).ok()
})?;
facets.validate_enum_value_space(
enum_matches_value_space!(dur, parse_duration, |a, b| duration_eq(a, &b)),
value,
)?;
}
facets.validate_patterns_only(value)?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"minInclusive"
| "maxInclusive"
| "minExclusive"
| "maxExclusive"
| "pattern"
| "enumeration"
)
}
}
pub struct DateTimeValidator;
impl TypeValidator for DateTimeValidator {
fn type_name(&self) -> &'static str {
"dateTime"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::DateTime
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::DateTime
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
parse_datetime(&normalized).map(|dt| {
XmlValue::new(
XmlTypeCode::DateTime,
XmlValueKind::Atomic(XmlAtomicValue::DateTime(dt)),
)
})
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let XmlValueKind::Atomic(XmlAtomicValue::DateTime(ref dt)) = result.value {
facets.validate_explicit_timezone(dt.timezone.is_some())?;
validate_bounds(dt, facets, "dateTime", value, |s| {
parse_datetime(&normalize_whitespace(s, WhitespaceMode::Collapse)).ok()
})?;
facets.validate_enum_value_space(
enum_matches_value_space!(dt, parse_datetime, |a, b| datetime_value_eq(a, &b)),
value,
)?;
}
facets.validate_patterns_only(value)?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"minInclusive"
| "maxInclusive"
| "minExclusive"
| "maxExclusive"
| "pattern"
| "enumeration"
| "explicitTimezone"
)
}
}
pub struct DateValidator;
impl TypeValidator for DateValidator {
fn type_name(&self) -> &'static str {
"date"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::Date
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::Date
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
parse_date(&normalized).map(|d| {
XmlValue::new(
XmlTypeCode::Date,
XmlValueKind::Atomic(XmlAtomicValue::Date(d)),
)
})
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let XmlValueKind::Atomic(XmlAtomicValue::Date(ref d)) = result.value {
facets.validate_explicit_timezone(d.timezone.is_some())?;
validate_bounds(d, facets, "date", value, |s| {
parse_date(&normalize_whitespace(s, WhitespaceMode::Collapse)).ok()
})?;
facets.validate_enum_value_space(
enum_matches_value_space!(d, parse_date, |a, b| date_value_eq(a, &b)),
value,
)?;
}
facets.validate_patterns_only(value)?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"minInclusive"
| "maxInclusive"
| "minExclusive"
| "maxExclusive"
| "pattern"
| "enumeration"
| "explicitTimezone"
)
}
}
pub struct TimeValidator;
impl TypeValidator for TimeValidator {
fn type_name(&self) -> &'static str {
"time"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::Time
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::Time
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
parse_time(&normalized).map(|t| {
XmlValue::new(
XmlTypeCode::Time,
XmlValueKind::Atomic(XmlAtomicValue::Time(t)),
)
})
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let XmlValueKind::Atomic(XmlAtomicValue::Time(ref t)) = result.value {
facets.validate_explicit_timezone(t.timezone.is_some())?;
validate_bounds(t, facets, "time", value, |s| {
parse_time(&normalize_whitespace(s, WhitespaceMode::Collapse)).ok()
})?;
facets.validate_enum_value_space(
enum_matches_value_space!(t, parse_time, |a, b| time_value_eq(a, &b)),
value,
)?;
}
facets.validate_patterns_only(value)?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"minInclusive"
| "maxInclusive"
| "minExclusive"
| "maxExclusive"
| "pattern"
| "enumeration"
| "explicitTimezone"
)
}
}
pub struct GYearMonthValidator;
impl TypeValidator for GYearMonthValidator {
fn type_name(&self) -> &'static str {
"gYearMonth"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::GYearMonth
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::GYearMonth
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
parse_gyearmonth(&normalized).map(|v| {
XmlValue::new(
XmlTypeCode::GYearMonth,
XmlValueKind::Atomic(XmlAtomicValue::GYearMonth(v)),
)
})
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let XmlValueKind::Atomic(XmlAtomicValue::GYearMonth(ref v)) = result.value {
facets.validate_explicit_timezone(v.timezone.is_some())?;
validate_bounds(v, facets, "gYearMonth", value, |s| {
parse_gyearmonth(&normalize_whitespace(s, WhitespaceMode::Collapse)).ok()
})?;
}
facets.validate_string(&result.to_string_value())?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"minInclusive"
| "maxInclusive"
| "minExclusive"
| "maxExclusive"
| "pattern"
| "enumeration"
| "explicitTimezone"
)
}
}
pub struct GYearValidator;
impl TypeValidator for GYearValidator {
fn type_name(&self) -> &'static str {
"gYear"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::GYear
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::GYear
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
parse_gyear(&normalized).map(|v| {
XmlValue::new(
XmlTypeCode::GYear,
XmlValueKind::Atomic(XmlAtomicValue::GYear(v)),
)
})
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let XmlValueKind::Atomic(XmlAtomicValue::GYear(ref v)) = result.value {
facets.validate_explicit_timezone(v.timezone.is_some())?;
validate_bounds(v, facets, "gYear", value, |s| {
parse_gyear(&normalize_whitespace(s, WhitespaceMode::Collapse)).ok()
})?;
}
facets.validate_string(&result.to_string_value())?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"minInclusive"
| "maxInclusive"
| "minExclusive"
| "maxExclusive"
| "pattern"
| "enumeration"
| "explicitTimezone"
)
}
}
pub struct GMonthDayValidator;
impl TypeValidator for GMonthDayValidator {
fn type_name(&self) -> &'static str {
"gMonthDay"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::GMonthDay
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::GMonthDay
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
parse_gmonthday(&normalized).map(|v| {
XmlValue::new(
XmlTypeCode::GMonthDay,
XmlValueKind::Atomic(XmlAtomicValue::GMonthDay(v)),
)
})
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let XmlValueKind::Atomic(XmlAtomicValue::GMonthDay(ref v)) = result.value {
facets.validate_explicit_timezone(v.timezone.is_some())?;
validate_bounds(v, facets, "gMonthDay", value, |s| {
parse_gmonthday(&normalize_whitespace(s, WhitespaceMode::Collapse)).ok()
})?;
}
facets.validate_string(&result.to_string_value())?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"minInclusive"
| "maxInclusive"
| "minExclusive"
| "maxExclusive"
| "pattern"
| "enumeration"
| "explicitTimezone"
)
}
}
pub struct GDayValidator;
impl TypeValidator for GDayValidator {
fn type_name(&self) -> &'static str {
"gDay"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::GDay
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::GDay
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
parse_gday(&normalized).map(|v| {
XmlValue::new(
XmlTypeCode::GDay,
XmlValueKind::Atomic(XmlAtomicValue::GDay(v)),
)
})
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let XmlValueKind::Atomic(XmlAtomicValue::GDay(ref v)) = result.value {
facets.validate_explicit_timezone(v.timezone.is_some())?;
validate_bounds(v, facets, "gDay", value, |s| {
parse_gday(&normalize_whitespace(s, WhitespaceMode::Collapse)).ok()
})?;
}
facets.validate_string(&result.to_string_value())?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"minInclusive"
| "maxInclusive"
| "minExclusive"
| "maxExclusive"
| "pattern"
| "enumeration"
| "explicitTimezone"
)
}
}
pub struct GMonthValidator;
impl TypeValidator for GMonthValidator {
fn type_name(&self) -> &'static str {
"gMonth"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::GMonth
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::GMonth
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
parse_gmonth(&normalized).map(|v| {
XmlValue::new(
XmlTypeCode::GMonth,
XmlValueKind::Atomic(XmlAtomicValue::GMonth(v)),
)
})
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let XmlValueKind::Atomic(XmlAtomicValue::GMonth(ref v)) = result.value {
facets.validate_explicit_timezone(v.timezone.is_some())?;
validate_bounds(v, facets, "gMonth", value, |s| {
parse_gmonth(&normalize_whitespace(s, WhitespaceMode::Collapse)).ok()
})?;
}
facets.validate_string(&result.to_string_value())?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"minInclusive"
| "maxInclusive"
| "minExclusive"
| "maxExclusive"
| "pattern"
| "enumeration"
| "explicitTimezone"
)
}
}
pub struct HexBinaryValidator;
impl TypeValidator for HexBinaryValidator {
fn type_name(&self) -> &'static str {
"hexBinary"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::HexBinary
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::HexBinary
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
hex::decode(&normalized)
.map(|bytes| {
XmlValue::new(
XmlTypeCode::HexBinary,
XmlValueKind::Atomic(XmlAtomicValue::HexBinary(bytes)),
)
})
.map_err(|e| ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "hexBinary",
message: e.to_string(),
})
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let XmlValueKind::Atomic(XmlAtomicValue::HexBinary(bytes)) = &result.value {
facets.validate_binary_length(bytes.len() as u64)?;
}
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
facets.validate_string_patterns_enums(&normalized)?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"length" | "minLength" | "maxLength" | "pattern" | "enumeration"
)
}
}
pub struct Base64BinaryValidator;
impl TypeValidator for Base64BinaryValidator {
fn type_name(&self) -> &'static str {
"base64Binary"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::Base64Binary
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::Base64Binary
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
use base64::Engine;
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
base64::engine::general_purpose::STANDARD
.decode(&normalized)
.map(|bytes| {
XmlValue::new(
XmlTypeCode::Base64Binary,
XmlValueKind::Atomic(XmlAtomicValue::Base64Binary(bytes)),
)
})
.map_err(|e| ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "base64Binary",
message: e.to_string(),
})
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let XmlValueKind::Atomic(XmlAtomicValue::Base64Binary(bytes)) = &result.value {
facets.validate_binary_length(bytes.len() as u64)?;
}
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
facets.validate_string_patterns_enums(&normalized)?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"length" | "minLength" | "maxLength" | "pattern" | "enumeration"
)
}
}
pub struct AnyUriValidator;
impl TypeValidator for AnyUriValidator {
fn type_name(&self) -> &'static str {
"anyURI"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::AnyUri
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::AnyUri
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
Ok(XmlValue::new(
XmlTypeCode::AnyUri,
XmlValueKind::Atomic(XmlAtomicValue::AnyUri(normalized)),
))
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
facets.validate_string(&result.to_string_value())?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"length" | "minLength" | "maxLength" | "pattern" | "enumeration"
)
}
}
pub fn is_valid_uri_scheme(s: &str) -> bool {
let mut chars = s.chars();
let Some(first) = chars.next() else {
return false;
};
if !first.is_ascii_alphabetic() {
return false;
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '-' || c == '.')
}
pub fn is_strict_xsd10_anyuri(value: &str) -> bool {
let collapsed: String;
let value: &str = if value.chars().any(char::is_whitespace) {
collapsed = normalize_whitespace(value, WhitespaceMode::Collapse);
collapsed.as_str()
} else {
value
};
if value.is_empty() {
return true;
}
let mut scheme_end: Option<usize> = None;
for (i, c) in value.char_indices() {
match c {
':' => {
scheme_end = Some(i);
break;
}
'/' | '?' | '#' => return true,
_ => {}
}
}
let Some(end) = scheme_end else { return true };
is_valid_uri_scheme(&value[..end])
}
fn parse_duration(s: &str) -> ValidationResult<DurationValue> {
let bad = |message: String| ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "duration",
message,
};
let bytes = s.as_bytes();
let mut pos = 0usize;
let negative = bytes.first() == Some(&b'-');
if negative {
pos += 1;
}
if bytes.get(pos) != Some(&b'P') {
return Err(bad("Must start with 'P'".to_string()));
}
pos += 1;
let mut out = DurationValue {
negative,
years: 0,
months: 0,
days: 0,
hours: 0,
minutes: 0,
seconds: Decimal::ZERO,
};
let mut seen = [false; 6];
let mut in_time = false;
while pos < bytes.len() {
if bytes[pos] == b'T' {
if in_time {
return Err(bad("Duplicate 'T' separator".to_string()));
}
in_time = true;
pos += 1;
continue;
}
let int_start = pos;
while pos < bytes.len() && bytes[pos].is_ascii_digit() {
pos += 1;
}
if pos == int_start {
return Err(bad(format!(
"Unexpected character '{}' at position {}",
bytes[pos] as char, pos
)));
}
let int_end = pos;
if pos < bytes.len() && bytes[pos] == b'.' {
pos += 1;
let frac_start = pos;
while pos < bytes.len() && bytes[pos].is_ascii_digit() {
pos += 1;
}
if pos == frac_start {
return Err(bad(
"Fractional part must have at least one digit".to_string()
));
}
}
let has_fraction = pos != int_end;
if pos >= bytes.len() {
return Err(bad("Missing designator after number".to_string()));
}
let designator = bytes[pos];
pos += 1;
let num_str = &s[int_start..pos - 1];
let (field, name, allow_fraction): (Field, &str, bool) = match (designator, in_time) {
(b'Y', false) => (Field::Years, "Y", false),
(b'M', false) => (Field::Months, "M", false),
(b'D', false) => (Field::Days, "D", false),
(b'H', true) => (Field::Hours, "H", false),
(b'M', true) => (Field::Minutes, "M", false),
(b'S', true) => (Field::Seconds, "S", true),
(other, _) => {
return Err(bad(format!(
"Unexpected designator '{}' (in_time={})",
other as char, in_time
)));
}
};
if seen[field as usize] {
return Err(bad(format!("Duplicate '{}' designator", name)));
}
seen[field as usize] = true;
if has_fraction && !allow_fraction {
return Err(bad(format!("'{}' must be an integer", name)));
}
match field {
Field::Seconds => {
out.seconds = num_str
.parse()
.map_err(|_| bad(format!("Invalid seconds value '{}'", num_str)))?;
}
_ => {
let v: u32 = num_str
.parse()
.map_err(|_| bad(format!("Invalid {} value '{}'", name, num_str)))?;
match field {
Field::Years => out.years = v,
Field::Months => out.months = v,
Field::Days => out.days = v,
Field::Hours => out.hours = v,
Field::Minutes => out.minutes = v,
Field::Seconds => unreachable!(),
}
}
}
}
if !seen.iter().any(|&b| b) {
return Err(bad(
"Duration must contain at least one component".to_string()
));
}
let any_time = seen[Field::Hours as usize]
|| seen[Field::Minutes as usize]
|| seen[Field::Seconds as usize];
if in_time && !any_time {
return Err(bad(
"Time designator 'T' requires at least one time component".to_string(),
));
}
Ok(out)
}
#[repr(u8)]
#[derive(Copy, Clone)]
enum Field {
Years = 0,
Months = 1,
Days = 2,
Hours = 3,
Minutes = 4,
Seconds = 5,
}
fn parse_datetime(s: &str) -> ValidationResult<DateTimeValue> {
let (date_time, tz) = split_timezone(s);
let parts: Vec<&str> = date_time.split('T').collect();
if parts.len() != 2 {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "dateTime",
message: "Missing 'T' separator".to_string(),
});
}
let date = parse_date_part(parts[0], "dateTime")?;
let time = parse_time_part(parts[1], "dateTime")?;
let (year, month, day, hour) = if time.0 == 24 {
if time.1 != 0 || time.2 != Decimal::from(0) {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "dateTime",
message: "24:00:00 requires minute=0 and second=0".to_string(),
});
}
let (y, m, d) = add_one_day(date.0, date.1, date.2);
(y, m, d, 0u8)
} else {
(date.0, date.1, date.2, time.0)
};
Ok(DateTimeValue {
year,
month,
day,
hour,
minute: time.1,
second: time.2,
timezone: tz,
})
}
fn add_one_day(year: i32, month: u8, day: u8) -> (i32, u8, u8) {
let dim = days_in_month(year, month);
if (day as u32) < dim {
(year, month, day + 1)
} else if month < 12 {
(year, month + 1, 1)
} else {
(year + 1, 1, 1)
}
}
fn days_in_month(year: i32, month: u8) -> u32 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
let leap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
if leap {
29
} else {
28
}
}
_ => 0,
}
}
fn parse_date(s: &str) -> ValidationResult<DateValue> {
let (date_str, tz) = split_timezone(s);
let (year, month, day) = parse_date_part(date_str, "date")?;
Ok(DateValue {
year,
month,
day,
timezone: tz,
})
}
fn parse_time(s: &str) -> ValidationResult<TimeValue> {
let (time_str, tz) = split_timezone(s);
let (mut hour, minute, second) = parse_time_part(time_str, "time")?;
if hour == 24 {
if minute != 0 || second != Decimal::from(0) {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "time",
message: "24:00:00 requires minute=0 and second=0".to_string(),
});
}
hour = 0;
}
Ok(TimeValue {
hour,
minute,
second,
timezone: tz,
})
}
fn valid_year_lexical(year_str: &str) -> bool {
let digits = year_str.strip_prefix('-').unwrap_or(year_str);
if digits.is_empty() || !digits.chars().all(|c| c.is_ascii_digit()) {
return false;
}
if digits.len() < 4 {
return false;
}
if digits.len() > 4 && digits.starts_with('0') {
return false;
}
true
}
fn parse_date_part(s: &str, type_name: &'static str) -> ValidationResult<(i32, u8, u8)> {
let parts: Vec<&str> = s.split('-').collect();
let (year_str, year, month, day) = if s.starts_with('-') && parts.len() >= 4 {
let year_str = format!("-{}", parts[1]);
let year: i32 = year_str
.parse()
.map_err(|_| ValidationError::InvalidLexical {
value: s.to_string(),
type_name,
message: "Invalid year".to_string(),
})?;
let month: u8 = parts[2]
.parse()
.map_err(|_| ValidationError::InvalidLexical {
value: s.to_string(),
type_name,
message: "Invalid month".to_string(),
})?;
let day: u8 = parts[3]
.parse()
.map_err(|_| ValidationError::InvalidLexical {
value: s.to_string(),
type_name,
message: "Invalid day".to_string(),
})?;
(year_str, year, month, day)
} else if parts.len() == 3 {
let year_str = parts[0].to_string();
let year: i32 = year_str
.parse()
.map_err(|_| ValidationError::InvalidLexical {
value: s.to_string(),
type_name,
message: "Invalid year".to_string(),
})?;
let month: u8 = parts[1]
.parse()
.map_err(|_| ValidationError::InvalidLexical {
value: s.to_string(),
type_name,
message: "Invalid month".to_string(),
})?;
let day: u8 = parts[2]
.parse()
.map_err(|_| ValidationError::InvalidLexical {
value: s.to_string(),
type_name,
message: "Invalid day".to_string(),
})?;
(year_str, year, month, day)
} else {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name,
message: "Invalid date format".to_string(),
});
};
if !valid_year_lexical(&year_str) {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name,
message: "Invalid year: must be 4+ digits, no leading zeros except single '0YYY' form"
.to_string(),
});
}
if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name,
message: "Date out of range".to_string(),
});
}
if (day as u32) > days_in_month(year, month) {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name,
message: "Day does not exist in the given month".to_string(),
});
}
Ok((year, month, day))
}
fn parse_time_part(s: &str, type_name: &'static str) -> ValidationResult<(u8, u8, Decimal)> {
let parts: Vec<&str> = s.split(':').collect();
if parts.len() != 3 {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name,
message: "Invalid time format".to_string(),
});
}
let is_two_ascii_digits = |t: &str| t.len() == 2 && t.bytes().all(|b| b.is_ascii_digit());
if !is_two_ascii_digits(parts[0]) {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name,
message: "Hour must be exactly two ASCII digits".to_string(),
});
}
if !is_two_ascii_digits(parts[1]) {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name,
message: "Minute must be exactly two ASCII digits".to_string(),
});
}
let sec_str = parts[2];
let (sec_int, sec_frac) = match sec_str.find('.') {
Some(p) => (&sec_str[..p], Some(&sec_str[p + 1..])),
None => (sec_str, None),
};
if !is_two_ascii_digits(sec_int) {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name,
message: "Seconds integer part must be exactly two ASCII digits".to_string(),
});
}
if let Some(frac) = sec_frac {
if frac.is_empty() || !frac.bytes().all(|b| b.is_ascii_digit()) {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name,
message: "Seconds fractional part must be one or more ASCII digits".to_string(),
});
}
}
let hour: u8 = parts[0]
.parse()
.map_err(|_| ValidationError::InvalidLexical {
value: s.to_string(),
type_name,
message: "Invalid hour".to_string(),
})?;
let minute: u8 = parts[1]
.parse()
.map_err(|_| ValidationError::InvalidLexical {
value: s.to_string(),
type_name,
message: "Invalid minute".to_string(),
})?;
let second: Decimal = parts[2]
.parse()
.map_err(|_| ValidationError::InvalidLexical {
value: s.to_string(),
type_name,
message: "Invalid second".to_string(),
})?;
if hour > 24 || minute > 59 || second >= Decimal::from(60) {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name,
message: "Time out of range".to_string(),
});
}
Ok((hour, minute, second))
}
fn split_timezone(s: &str) -> (&str, Option<TimezoneOffset>) {
if let Some(stripped) = s.strip_suffix('Z') {
(stripped, Some(TimezoneOffset::UTC))
} else if let Some(pos) = s.rfind('+') {
if pos > 0 && pos < s.len() - 1 {
let tz_str = &s[pos + 1..];
if let Some(tz) = parse_timezone_offset(tz_str, false) {
return (&s[..pos], Some(tz));
}
}
(s, None)
} else if let Some(pos) = s.rfind('-') {
if pos > 0 && pos < s.len() - 1 {
let tz_str = &s[pos + 1..];
if let Some(tz) = parse_timezone_offset(tz_str, true) {
return (&s[..pos], Some(tz));
}
}
(s, None)
} else {
(s, None)
}
}
fn parse_timezone_offset(s: &str, negative: bool) -> Option<TimezoneOffset> {
let parts: Vec<&str> = s.split(':').collect();
if parts.len() != 2 {
return None;
}
let hours: i8 = parts[0].parse().ok()?;
let minutes: i8 = parts[1].parse().ok()?;
let offset = hours as i16 * 60 + minutes as i16;
Some(TimezoneOffset(if negative { -offset } else { offset }))
}
fn parse_gyearmonth(s: &str) -> ValidationResult<GYearMonthValue> {
let (date_str, tz) = split_timezone(s);
let parts: Vec<&str> = date_str.split('-').collect();
let (year_str, year, month) = if date_str.starts_with('-') && parts.len() >= 3 {
let year_str = format!("-{}", parts[1]);
let year: i32 = year_str
.parse()
.map_err(|_| ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "gYearMonth",
message: "Invalid year".to_string(),
})?;
let month: u8 = parts[2]
.parse()
.map_err(|_| ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "gYearMonth",
message: "Invalid month".to_string(),
})?;
(year_str, year, month)
} else if parts.len() == 2 {
let year_str = parts[0].to_string();
let year: i32 = year_str
.parse()
.map_err(|_| ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "gYearMonth",
message: "Invalid year".to_string(),
})?;
let month: u8 = parts[1]
.parse()
.map_err(|_| ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "gYearMonth",
message: "Invalid month".to_string(),
})?;
(year_str, year, month)
} else {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "gYearMonth",
message: "Invalid format".to_string(),
});
};
if !valid_year_lexical(&year_str) {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "gYearMonth",
message: "Invalid year lexical form".to_string(),
});
}
if !(1..=12).contains(&month) {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "gYearMonth",
message: "Month out of range".to_string(),
});
}
Ok(GYearMonthValue {
year,
month,
timezone: tz,
})
}
fn parse_gyear(s: &str) -> ValidationResult<GYearValue> {
let (year_str, tz) = split_timezone(s);
let year: i32 = year_str
.parse()
.map_err(|_| ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "gYear",
message: "Invalid year".to_string(),
})?;
if !valid_year_lexical(year_str) {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "gYear",
message: "Invalid year lexical form".to_string(),
});
}
Ok(GYearValue { year, timezone: tz })
}
fn parse_gmonthday(s: &str) -> ValidationResult<GMonthDayValue> {
let (date_str, tz) = split_timezone(s);
if !date_str.starts_with("--") {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "gMonthDay",
message: "Must start with '--'".to_string(),
});
}
let rest = &date_str[2..];
let parts: Vec<&str> = rest.split('-').collect();
if parts.len() != 2 {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "gMonthDay",
message: "Invalid format".to_string(),
});
}
let month: u8 = parts[0]
.parse()
.map_err(|_| ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "gMonthDay",
message: "Invalid month".to_string(),
})?;
let day: u8 = parts[1]
.parse()
.map_err(|_| ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "gMonthDay",
message: "Invalid day".to_string(),
})?;
if !(1..=12).contains(&month) {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "gMonthDay",
message: "Month out of range".to_string(),
});
}
let max_day = match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => 29,
_ => unreachable!(),
};
if !(1..=max_day).contains(&day) {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "gMonthDay",
message: "Day does not exist in the given month".to_string(),
});
}
Ok(GMonthDayValue {
month,
day,
timezone: tz,
})
}
fn parse_gday(s: &str) -> ValidationResult<GDayValue> {
let (day_str, tz) = split_timezone(s);
if !day_str.starts_with("---") {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "gDay",
message: "Must start with '---'".to_string(),
});
}
let day: u8 = day_str[3..]
.parse()
.map_err(|_| ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "gDay",
message: "Invalid day".to_string(),
})?;
if !(1..=31).contains(&day) {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "gDay",
message: "Day out of range".to_string(),
});
}
Ok(GDayValue { day, timezone: tz })
}
fn parse_gmonth(s: &str) -> ValidationResult<GMonthValue> {
let (month_str, tz) = split_timezone(s);
if !month_str.starts_with("--") {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "gMonth",
message: "Must start with '--'".to_string(),
});
}
let month: u8 = month_str[2..]
.parse()
.map_err(|_| ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "gMonth",
message: "Invalid month".to_string(),
})?;
if !(1..=12).contains(&month) {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "gMonth",
message: "Month out of range".to_string(),
});
}
Ok(GMonthValue {
month,
timezone: tz,
})
}
fn parse_year_month_duration(s: &str) -> ValidationResult<YearMonthDurationValue> {
let (negative, rest) = if let Some(stripped) = s.strip_prefix('-') {
(true, stripped)
} else {
(false, s)
};
if !rest.starts_with('P') {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "yearMonthDuration",
message: "Must start with 'P' (or '-P')".to_string(),
});
}
let content = &rest[1..];
if content.is_empty() {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "yearMonthDuration",
message: "Duration cannot be empty".to_string(),
});
}
if content.contains('D') || content.contains('T') {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "yearMonthDuration",
message: "yearMonthDuration cannot contain day or time components".to_string(),
});
}
let mut years = 0u32;
let mut months = 0u32;
let mut current = content;
if let Some(y_pos) = current.find('Y') {
years = current[..y_pos]
.parse()
.map_err(|_| ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "yearMonthDuration",
message: "Invalid years value".to_string(),
})?;
current = ¤t[y_pos + 1..];
}
if let Some(m_pos) = current.find('M') {
months = current[..m_pos]
.parse()
.map_err(|_| ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "yearMonthDuration",
message: "Invalid months value".to_string(),
})?;
current = ¤t[m_pos + 1..];
}
if !current.is_empty() {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "yearMonthDuration",
message: "Invalid duration format".to_string(),
});
}
Ok(YearMonthDurationValue {
negative,
years,
months,
})
}
fn parse_day_time_duration(s: &str) -> ValidationResult<DayTimeDurationValue> {
let (negative, rest) = if let Some(stripped) = s.strip_prefix('-') {
(true, stripped)
} else {
(false, s)
};
if !rest.starts_with('P') {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "dayTimeDuration",
message: "Must start with 'P' (or '-P')".to_string(),
});
}
let content = &rest[1..];
if content.is_empty() {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "dayTimeDuration",
message: "Duration cannot be empty".to_string(),
});
}
let date_part = if let Some(t_pos) = content.find('T') {
&content[..t_pos]
} else {
content
};
if date_part.contains('Y') || date_part.contains('M') {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "dayTimeDuration",
message: "dayTimeDuration cannot contain year or month components".to_string(),
});
}
let mut days = 0u32;
let mut hours = 0u32;
let mut minutes = 0u32;
let mut seconds = Decimal::ZERO;
let mut current = content;
if let Some(d_pos) = current.find('D') {
days = current[..d_pos]
.parse()
.map_err(|_| ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "dayTimeDuration",
message: "Invalid days value".to_string(),
})?;
current = ¤t[d_pos + 1..];
}
if let Some(stripped) = current.strip_prefix('T') {
current = stripped;
if let Some(h_pos) = current.find('H') {
hours = current[..h_pos]
.parse()
.map_err(|_| ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "dayTimeDuration",
message: "Invalid hours value".to_string(),
})?;
current = ¤t[h_pos + 1..];
}
if let Some(m_pos) = current.find('M') {
minutes = current[..m_pos]
.parse()
.map_err(|_| ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "dayTimeDuration",
message: "Invalid minutes value".to_string(),
})?;
current = ¤t[m_pos + 1..];
}
if let Some(s_pos) = current.find('S') {
seconds = current[..s_pos]
.parse()
.map_err(|_| ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "dayTimeDuration",
message: "Invalid seconds value".to_string(),
})?;
current = ¤t[s_pos + 1..];
}
}
if !current.is_empty() {
return Err(ValidationError::InvalidLexical {
value: s.to_string(),
type_name: "dayTimeDuration",
message: "Invalid duration format".to_string(),
});
}
Ok(DayTimeDurationValue {
negative,
days,
hours,
minutes,
seconds,
})
}
fn is_name_start_char(c: char) -> bool {
matches!(c,
'A'..='Z' | 'a'..='z' | '_' | ':' |
'\u{C0}'..='\u{D6}' | '\u{D8}'..='\u{F6}' | '\u{F8}'..='\u{2FF}' |
'\u{370}'..='\u{37D}' | '\u{37F}'..='\u{1FFF}' | '\u{200C}'..='\u{200D}' |
'\u{2070}'..='\u{218F}' | '\u{2C00}'..='\u{2FEF}' | '\u{3001}'..='\u{D7FF}' |
'\u{F900}'..='\u{FDCF}' | '\u{FDF0}'..='\u{FFFD}' | '\u{10000}'..='\u{EFFFF}'
)
}
fn is_name_char(c: char) -> bool {
is_name_start_char(c)
|| matches!(c,
'-' | '.' | '0'..='9' | '\u{B7}' |
'\u{0300}'..='\u{036F}' | '\u{203F}'..='\u{2040}'
)
}
fn is_ncname_start_char(c: char) -> bool {
is_name_start_char(c) && c != ':'
}
fn is_ncname_char(c: char) -> bool {
is_name_char(c) && c != ':'
}
fn is_valid_name(s: &str) -> bool {
let mut chars = s.chars();
match chars.next() {
Some(c) if is_name_start_char(c) => chars.all(is_name_char),
_ => false,
}
}
fn is_valid_ncname(s: &str) -> bool {
let mut chars = s.chars();
match chars.next() {
Some(c) if is_ncname_start_char(c) => chars.all(is_ncname_char),
_ => false,
}
}
pub fn is_valid_language(s: &str) -> bool {
if s.is_empty() {
return false;
}
let parts: Vec<&str> = s.split('-').collect();
if parts.is_empty() {
return false;
}
let first = parts[0];
if first.is_empty() || first.len() > 8 || !first.chars().all(|c| c.is_ascii_alphabetic()) {
return false;
}
for part in parts.iter().skip(1) {
if part.is_empty() || part.len() > 8 || !part.chars().all(|c| c.is_ascii_alphanumeric()) {
return false;
}
}
true
}
pub struct LongValidator;
impl TypeValidator for LongValidator {
fn type_name(&self) -> &'static str {
"long"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::Long
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::Decimal
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
normalized
.parse::<i64>()
.map(|v| {
XmlValue::new(
XmlTypeCode::Long,
XmlValueKind::Atomic(XmlAtomicValue::Integer(BigInt::from(v))),
)
})
.map_err(|e| ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "long",
message: e.to_string(),
})
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let Some(d) = result.as_decimal() {
facets.validate_decimal(&d)?;
}
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
facets.validate_string_patterns_enums(&normalized)?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"totalDigits"
| "fractionDigits"
| "minInclusive"
| "maxInclusive"
| "minExclusive"
| "maxExclusive"
| "pattern"
| "enumeration"
)
}
}
pub struct IntValidator;
impl TypeValidator for IntValidator {
fn type_name(&self) -> &'static str {
"int"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::Int
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::Decimal
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
normalized
.parse::<i32>()
.map(|v| {
XmlValue::new(
XmlTypeCode::Int,
XmlValueKind::Atomic(XmlAtomicValue::Integer(BigInt::from(v))),
)
})
.map_err(|e| ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "int",
message: e.to_string(),
})
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let Some(d) = result.as_decimal() {
facets.validate_decimal(&d)?;
}
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
facets.validate_string_patterns_enums(&normalized)?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"totalDigits"
| "fractionDigits"
| "minInclusive"
| "maxInclusive"
| "minExclusive"
| "maxExclusive"
| "pattern"
| "enumeration"
)
}
}
pub struct ShortValidator;
impl TypeValidator for ShortValidator {
fn type_name(&self) -> &'static str {
"short"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::Short
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::Decimal
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
normalized
.parse::<i16>()
.map(|v| {
XmlValue::new(
XmlTypeCode::Short,
XmlValueKind::Atomic(XmlAtomicValue::Integer(BigInt::from(v))),
)
})
.map_err(|e| ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "short",
message: e.to_string(),
})
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let Some(d) = result.as_decimal() {
facets.validate_decimal(&d)?;
}
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
facets.validate_string_patterns_enums(&normalized)?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"totalDigits"
| "fractionDigits"
| "minInclusive"
| "maxInclusive"
| "minExclusive"
| "maxExclusive"
| "pattern"
| "enumeration"
)
}
}
pub struct ByteValidator;
impl TypeValidator for ByteValidator {
fn type_name(&self) -> &'static str {
"byte"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::Byte
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::Decimal
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
normalized
.parse::<i8>()
.map(|v| {
XmlValue::new(
XmlTypeCode::Byte,
XmlValueKind::Atomic(XmlAtomicValue::Integer(BigInt::from(v))),
)
})
.map_err(|e| ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "byte",
message: e.to_string(),
})
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let Some(d) = result.as_decimal() {
facets.validate_decimal(&d)?;
}
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
facets.validate_string_patterns_enums(&normalized)?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"totalDigits"
| "fractionDigits"
| "minInclusive"
| "maxInclusive"
| "minExclusive"
| "maxExclusive"
| "pattern"
| "enumeration"
)
}
}
pub struct NonNegativeIntegerValidator;
impl TypeValidator for NonNegativeIntegerValidator {
fn type_name(&self) -> &'static str {
"nonNegativeInteger"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::NonNegativeInteger
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::Decimal
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
let bigint = normalized
.parse::<BigInt>()
.map_err(|e| ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "nonNegativeInteger",
message: e.to_string(),
})?;
if bigint < BigInt::from(0) {
return Err(ValidationError::RangeError {
value: value.to_string(),
type_name: "nonNegativeInteger",
});
}
Ok(XmlValue::new(
XmlTypeCode::NonNegativeInteger,
XmlValueKind::Atomic(XmlAtomicValue::Integer(bigint)),
))
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let Some(d) = result.as_decimal() {
facets.validate_decimal(&d)?;
}
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
facets.validate_string_patterns_enums(&normalized)?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"totalDigits"
| "fractionDigits"
| "minInclusive"
| "maxInclusive"
| "minExclusive"
| "maxExclusive"
| "pattern"
| "enumeration"
)
}
}
pub struct PositiveIntegerValidator;
impl TypeValidator for PositiveIntegerValidator {
fn type_name(&self) -> &'static str {
"positiveInteger"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::PositiveInteger
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::Decimal
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
let bigint = normalized
.parse::<BigInt>()
.map_err(|e| ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "positiveInteger",
message: e.to_string(),
})?;
if bigint <= BigInt::from(0) {
return Err(ValidationError::RangeError {
value: value.to_string(),
type_name: "positiveInteger",
});
}
Ok(XmlValue::new(
XmlTypeCode::PositiveInteger,
XmlValueKind::Atomic(XmlAtomicValue::Integer(bigint)),
))
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let Some(d) = result.as_decimal() {
facets.validate_decimal(&d)?;
}
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
facets.validate_string_patterns_enums(&normalized)?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"totalDigits"
| "fractionDigits"
| "minInclusive"
| "maxInclusive"
| "minExclusive"
| "maxExclusive"
| "pattern"
| "enumeration"
)
}
}
pub struct NonPositiveIntegerValidator;
impl TypeValidator for NonPositiveIntegerValidator {
fn type_name(&self) -> &'static str {
"nonPositiveInteger"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::NonPositiveInteger
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::Decimal
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
let bigint = normalized
.parse::<BigInt>()
.map_err(|e| ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "nonPositiveInteger",
message: e.to_string(),
})?;
if bigint > BigInt::from(0) {
return Err(ValidationError::RangeError {
value: value.to_string(),
type_name: "nonPositiveInteger",
});
}
Ok(XmlValue::new(
XmlTypeCode::NonPositiveInteger,
XmlValueKind::Atomic(XmlAtomicValue::Integer(bigint)),
))
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let Some(d) = result.as_decimal() {
facets.validate_decimal(&d)?;
}
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
facets.validate_string_patterns_enums(&normalized)?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"totalDigits"
| "fractionDigits"
| "minInclusive"
| "maxInclusive"
| "minExclusive"
| "maxExclusive"
| "pattern"
| "enumeration"
)
}
}
pub struct NegativeIntegerValidator;
impl TypeValidator for NegativeIntegerValidator {
fn type_name(&self) -> &'static str {
"negativeInteger"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::NegativeInteger
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::Decimal
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
let bigint = normalized
.parse::<BigInt>()
.map_err(|e| ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "negativeInteger",
message: e.to_string(),
})?;
if bigint >= BigInt::from(0) {
return Err(ValidationError::RangeError {
value: value.to_string(),
type_name: "negativeInteger",
});
}
Ok(XmlValue::new(
XmlTypeCode::NegativeInteger,
XmlValueKind::Atomic(XmlAtomicValue::Integer(bigint)),
))
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let Some(d) = result.as_decimal() {
facets.validate_decimal(&d)?;
}
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
facets.validate_string_patterns_enums(&normalized)?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"totalDigits"
| "fractionDigits"
| "minInclusive"
| "maxInclusive"
| "minExclusive"
| "maxExclusive"
| "pattern"
| "enumeration"
)
}
}
pub struct UnsignedLongValidator;
impl TypeValidator for UnsignedLongValidator {
fn type_name(&self) -> &'static str {
"unsignedLong"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::UnsignedLong
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::Decimal
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
normalized
.parse::<u64>()
.map(|v| {
XmlValue::new(
XmlTypeCode::UnsignedLong,
XmlValueKind::Atomic(XmlAtomicValue::Integer(BigInt::from(v))),
)
})
.map_err(|e| ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "unsignedLong",
message: e.to_string(),
})
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let Some(d) = result.as_decimal() {
facets.validate_decimal(&d)?;
}
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
facets.validate_string_patterns_enums(&normalized)?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"totalDigits"
| "fractionDigits"
| "minInclusive"
| "maxInclusive"
| "minExclusive"
| "maxExclusive"
| "pattern"
| "enumeration"
)
}
}
pub struct UnsignedIntValidator;
impl TypeValidator for UnsignedIntValidator {
fn type_name(&self) -> &'static str {
"unsignedInt"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::UnsignedInt
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::Decimal
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
normalized
.parse::<u32>()
.map(|v| {
XmlValue::new(
XmlTypeCode::UnsignedInt,
XmlValueKind::Atomic(XmlAtomicValue::Integer(BigInt::from(v))),
)
})
.map_err(|e| ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "unsignedInt",
message: e.to_string(),
})
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let Some(d) = result.as_decimal() {
facets.validate_decimal(&d)?;
}
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
facets.validate_string_patterns_enums(&normalized)?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"totalDigits"
| "fractionDigits"
| "minInclusive"
| "maxInclusive"
| "minExclusive"
| "maxExclusive"
| "pattern"
| "enumeration"
)
}
}
pub struct UnsignedShortValidator;
impl TypeValidator for UnsignedShortValidator {
fn type_name(&self) -> &'static str {
"unsignedShort"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::UnsignedShort
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::Decimal
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
normalized
.parse::<u16>()
.map(|v| {
XmlValue::new(
XmlTypeCode::UnsignedShort,
XmlValueKind::Atomic(XmlAtomicValue::Integer(BigInt::from(v))),
)
})
.map_err(|e| ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "unsignedShort",
message: e.to_string(),
})
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let Some(d) = result.as_decimal() {
facets.validate_decimal(&d)?;
}
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
facets.validate_string_patterns_enums(&normalized)?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"totalDigits"
| "fractionDigits"
| "minInclusive"
| "maxInclusive"
| "minExclusive"
| "maxExclusive"
| "pattern"
| "enumeration"
)
}
}
pub struct UnsignedByteValidator;
impl TypeValidator for UnsignedByteValidator {
fn type_name(&self) -> &'static str {
"unsignedByte"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::UnsignedByte
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::Decimal
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
normalized
.parse::<u8>()
.map(|v| {
XmlValue::new(
XmlTypeCode::UnsignedByte,
XmlValueKind::Atomic(XmlAtomicValue::Integer(BigInt::from(v))),
)
})
.map_err(|e| ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "unsignedByte",
message: e.to_string(),
})
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let Some(d) = result.as_decimal() {
facets.validate_decimal(&d)?;
}
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
facets.validate_string_patterns_enums(&normalized)?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"totalDigits"
| "fractionDigits"
| "minInclusive"
| "maxInclusive"
| "minExclusive"
| "maxExclusive"
| "pattern"
| "enumeration"
)
}
}
pub struct QNameValidator;
impl TypeValidator for QNameValidator {
fn type_name(&self) -> &'static str {
"QName"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::QName
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::QName
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
let (prefix, local) = if let Some(colon_pos) = normalized.find(':') {
let prefix = &normalized[..colon_pos];
let local = &normalized[colon_pos + 1..];
(Some(prefix), local)
} else {
(None, normalized.as_str())
};
if let Some(p) = prefix {
if !is_valid_ncname(p) {
return Err(ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "QName",
message: format!("Invalid prefix '{}' (must be NCName)", p),
});
}
}
if !is_valid_ncname(local) {
return Err(ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "QName",
message: format!("Invalid local name '{}' (must be NCName)", local),
});
}
Ok(XmlValue::new(
XmlTypeCode::QName,
XmlValueKind::Atomic(XmlAtomicValue::String(normalized)),
))
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
facets.validate_string_patterns_enums(&result.to_string_value())?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"length" | "minLength" | "maxLength" | "pattern" | "enumeration"
)
}
}
pub struct NotationValidator;
impl TypeValidator for NotationValidator {
fn type_name(&self) -> &'static str {
"NOTATION"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::Notation
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::Notation
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
let (prefix, local) = if let Some(colon_pos) = normalized.find(':') {
let prefix = &normalized[..colon_pos];
let local = &normalized[colon_pos + 1..];
(Some(prefix), local)
} else {
(None, normalized.as_str())
};
if let Some(p) = prefix {
if !is_valid_ncname(p) {
return Err(ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "NOTATION",
message: format!("Invalid prefix '{}' (must be NCName)", p),
});
}
}
if !is_valid_ncname(local) {
return Err(ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "NOTATION",
message: format!("Invalid local name '{}' (must be NCName)", local),
});
}
Ok(XmlValue::new(
XmlTypeCode::Notation,
XmlValueKind::Atomic(XmlAtomicValue::String(normalized)),
))
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
facets.validate_string_patterns_enums(&result.to_string_value())?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"length" | "minLength" | "maxLength" | "pattern" | "enumeration"
)
}
}
pub struct NmTokensValidator;
impl TypeValidator for NmTokensValidator {
fn type_name(&self) -> &'static str {
"NMTOKENS"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::NmTokens
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::String
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
if normalized.is_empty() {
return Err(ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "NMTOKENS",
message: "NMTOKENS must contain at least one token".to_string(),
});
}
let mut items = Vec::new();
for token in normalized.split_whitespace() {
if !token.chars().all(is_name_char) {
return Err(ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "NMTOKENS",
message: format!("Invalid NMTOKEN: '{}'", token),
});
}
items.push(XmlAtomicValue::String(token.to_string()));
}
Ok(XmlValue::new(
XmlTypeCode::NmTokens,
XmlValueKind::List {
item_type: XmlTypeCode::NmToken,
items,
},
))
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let XmlValueKind::List { items, .. } = &result.value {
facets.validate_list_length(items.len() as u64)?;
}
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"length" | "minLength" | "maxLength" | "pattern" | "enumeration"
)
}
}
pub struct IdRefsValidator;
impl TypeValidator for IdRefsValidator {
fn type_name(&self) -> &'static str {
"IDREFS"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::IdRefs
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::String
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
if normalized.is_empty() {
return Err(ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "IDREFS",
message: "IDREFS must contain at least one IDREF".to_string(),
});
}
let mut items = Vec::new();
for token in normalized.split_whitespace() {
if !is_valid_ncname(token) {
return Err(ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "IDREFS",
message: format!("Invalid IDREF: '{}' (must be NCName)", token),
});
}
items.push(XmlAtomicValue::String(token.to_string()));
}
Ok(XmlValue::new(
XmlTypeCode::IdRefs,
XmlValueKind::List {
item_type: XmlTypeCode::IdRef,
items,
},
))
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let XmlValueKind::List { items, .. } = &result.value {
facets.validate_list_length(items.len() as u64)?;
}
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"length" | "minLength" | "maxLength" | "pattern" | "enumeration"
)
}
}
pub struct EntitiesValidator;
impl TypeValidator for EntitiesValidator {
fn type_name(&self) -> &'static str {
"ENTITIES"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::Entities
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::String
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
if normalized.is_empty() {
return Err(ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "ENTITIES",
message: "ENTITIES must contain at least one ENTITY".to_string(),
});
}
let mut items = Vec::new();
for token in normalized.split_whitespace() {
if !is_valid_ncname(token) {
return Err(ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "ENTITIES",
message: format!("Invalid ENTITY: '{}' (must be NCName)", token),
});
}
items.push(XmlAtomicValue::String(token.to_string()));
}
Ok(XmlValue::new(
XmlTypeCode::Entities,
XmlValueKind::List {
item_type: XmlTypeCode::Entity,
items,
},
))
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let XmlValueKind::List { items, .. } = &result.value {
facets.validate_list_length(items.len() as u64)?;
}
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"length" | "minLength" | "maxLength" | "pattern" | "enumeration"
)
}
}
pub struct YearMonthDurationValidator;
impl TypeValidator for YearMonthDurationValidator {
fn type_name(&self) -> &'static str {
"yearMonthDuration"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::YearMonthDuration
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::Duration
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
parse_year_month_duration(&normalized).map(|d| {
XmlValue::new(
XmlTypeCode::YearMonthDuration,
XmlValueKind::Atomic(XmlAtomicValue::YearMonthDuration(d)),
)
})
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let XmlValueKind::Atomic(XmlAtomicValue::YearMonthDuration(ref dur)) = result.value {
validate_bounds(dur, facets, "yearMonthDuration", value, |s| {
parse_year_month_duration(&normalize_whitespace(s, WhitespaceMode::Collapse)).ok()
})?;
facets.validate_enum_value_space(
enum_matches_value_space!(dur, parse_year_month_duration, |a, b| {
year_month_duration_value_eq(a, &b)
}),
value,
)?;
}
facets.validate_patterns_only(value)?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"minInclusive"
| "maxInclusive"
| "minExclusive"
| "maxExclusive"
| "pattern"
| "enumeration"
)
}
}
pub struct DayTimeDurationValidator;
impl TypeValidator for DayTimeDurationValidator {
fn type_name(&self) -> &'static str {
"dayTimeDuration"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::DayTimeDuration
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::Duration
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
parse_day_time_duration(&normalized).map(|d| {
XmlValue::new(
XmlTypeCode::DayTimeDuration,
XmlValueKind::Atomic(XmlAtomicValue::DayTimeDuration(d)),
)
})
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let XmlValueKind::Atomic(XmlAtomicValue::DayTimeDuration(ref dur)) = result.value {
validate_bounds(dur, facets, "dayTimeDuration", value, |s| {
parse_day_time_duration(&normalize_whitespace(s, WhitespaceMode::Collapse)).ok()
})?;
facets.validate_enum_value_space(
enum_matches_value_space!(dur, parse_day_time_duration, |a, b| {
day_time_duration_value_eq(a, &b)
}),
value,
)?;
}
facets.validate_patterns_only(value)?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"minInclusive"
| "maxInclusive"
| "minExclusive"
| "maxExclusive"
| "pattern"
| "enumeration"
)
}
}
pub struct DateTimeStampValidator;
impl TypeValidator for DateTimeStampValidator {
fn type_name(&self) -> &'static str {
"dateTimeStamp"
}
fn type_code(&self) -> XmlTypeCode {
XmlTypeCode::DateTimeStamp
}
fn primitive_type(&self) -> PrimitiveTypeCode {
PrimitiveTypeCode::DateTime
}
fn whitespace(&self) -> WhitespaceMode {
WhitespaceMode::Collapse
}
fn validate(&self, value: &str) -> ValidationResult<XmlValue> {
let normalized = normalize_whitespace(value, WhitespaceMode::Collapse);
let dt = parse_datetime(&normalized)?;
if dt.timezone.is_none() {
return Err(ValidationError::InvalidLexical {
value: value.to_string(),
type_name: "dateTimeStamp",
message: "dateTimeStamp requires a timezone".to_string(),
});
}
Ok(XmlValue::new(
XmlTypeCode::DateTimeStamp,
XmlValueKind::Atomic(XmlAtomicValue::DateTime(dt)),
))
}
fn validate_with_facets(&self, value: &str, facets: &FacetSet) -> ValidationResult<XmlValue> {
let result = self.validate(value)?;
if let XmlValueKind::Atomic(XmlAtomicValue::DateTime(ref dt)) = result.value {
facets.validate_explicit_timezone(dt.timezone.is_some())?;
validate_bounds(dt, facets, "dateTimeStamp", value, |s| {
parse_datetime(&normalize_whitespace(s, WhitespaceMode::Collapse)).ok()
})?;
facets.validate_enum_value_space(
enum_matches_value_space!(dt, parse_datetime, |a, b| datetime_value_eq(a, &b)),
value,
)?;
}
facets.validate_patterns_only(value)?;
Ok(result)
}
fn facet_applicable(&self, facet: &str) -> bool {
matches!(
facet,
"minInclusive"
| "maxInclusive"
| "minExclusive"
| "maxExclusive"
| "pattern"
| "enumeration"
| "explicitTimezone"
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_string_validator() {
let v = StringValidator;
let result = v.validate("hello").unwrap();
assert_eq!(result.type_code, XmlTypeCode::String);
assert_eq!(result.to_string_value(), "hello");
}
#[test]
fn test_boolean_validator() {
let v = BooleanValidator;
assert_eq!(v.validate("true").unwrap().as_boolean(), Some(true));
assert_eq!(v.validate("false").unwrap().as_boolean(), Some(false));
assert_eq!(v.validate("1").unwrap().as_boolean(), Some(true));
assert_eq!(v.validate("0").unwrap().as_boolean(), Some(false));
assert!(v.validate("yes").is_err());
}
#[test]
fn test_decimal_validator() {
let v = DecimalValidator;
let result = v.validate("123.45").unwrap();
assert_eq!(result.type_code, XmlTypeCode::Decimal);
assert!(result.as_decimal().is_some());
}
#[test]
fn test_integer_validator() {
let v = IntegerValidator;
let result = v.validate("12345").unwrap();
assert_eq!(result.type_code, XmlTypeCode::Integer);
assert_eq!(result.as_integer(), Some(&BigInt::from(12345)));
}
#[test]
fn test_float_validator() {
let v = FloatValidator;
assert!(v.validate("2.5").is_ok());
assert!(v.validate("INF").is_ok());
assert!(v.validate("-INF").is_ok());
assert!(v.validate("NaN").is_ok());
}
#[test]
fn test_double_validator() {
let v = DoubleValidator;
let result = v.validate("2.718281828").unwrap();
assert_eq!(result.type_code, XmlTypeCode::Double);
}
#[test]
fn test_datetime_validator() {
let v = DateTimeValidator;
let result = v.validate("2024-03-15T10:30:00Z").unwrap();
assert_eq!(result.type_code, XmlTypeCode::DateTime);
}
#[test]
fn test_date_validator() {
let v = DateValidator;
let result = v.validate("2024-03-15").unwrap();
assert_eq!(result.type_code, XmlTypeCode::Date);
}
#[test]
fn test_time_validator() {
let v = TimeValidator;
let result = v.validate("10:30:00").unwrap();
assert_eq!(result.type_code, XmlTypeCode::Time);
}
#[test]
fn test_duration_validator() {
let v = DurationValidator;
let result = v.validate("P1Y2M3DT4H5M6S").unwrap();
assert_eq!(result.type_code, XmlTypeCode::Duration);
}
#[test]
fn test_hex_binary_validator() {
let v = HexBinaryValidator;
let result = v.validate("DEADBEEF").unwrap();
assert_eq!(result.type_code, XmlTypeCode::HexBinary);
}
#[test]
fn test_base64_binary_validator() {
let v = Base64BinaryValidator;
let result = v.validate("SGVsbG8=").unwrap();
assert_eq!(result.type_code, XmlTypeCode::Base64Binary);
}
#[test]
fn test_anyuri_validator() {
let v = AnyUriValidator;
let result = v.validate("http://example.com").unwrap();
assert_eq!(result.type_code, XmlTypeCode::AnyUri);
}
#[test]
fn test_validator_registry() {
let registry = ValidatorRegistry::new();
assert!(registry.get_by_name("string").is_some());
assert!(registry.get_by_name("integer").is_some());
assert!(registry.get_by_code(XmlTypeCode::Boolean).is_some());
assert!(registry.get_by_name("nonexistent").is_none());
}
#[test]
fn test_whitespace_normalization() {
let v = TokenValidator;
let result = v.validate(" hello world ").unwrap();
assert_eq!(result.to_string_value(), "hello world");
}
}