use crate::error::{FacetError, FacetResult};
use crate::namespace::context::NamespaceContextSnapshot;
use crate::parser::location::SourceRef;
use crate::regex_convert::lenient_ms_preprocess;
#[cfg(feature = "xsd11")]
use crate::regex_convert::rewrite_xsd10_category_escapes;
use crate::regex_convert::validate_xml_pattern_syntax;
#[cfg(not(feature = "xsd11"))]
use crate::regex_convert::{convert_xml_pattern, ConvertOptions};
use crate::schema::model::{RegexCompat, XsdVersion};
#[cfg(not(feature = "xsd11"))]
use regex::Regex;
use std::collections::HashSet;
#[cfg(feature = "xsd11")]
use std::sync::Arc;
use super::XmlTypeCode;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FacetFixed {
#[default]
Default,
Fixed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WhitespaceMode {
Preserve,
Replace,
#[default]
Collapse,
}
#[derive(Debug, Clone)]
pub struct LengthFacet {
pub value: u64,
pub fixed: FacetFixed,
pub source: Option<SourceRef>,
}
#[derive(Debug, Clone)]
pub struct MinLengthFacet {
pub value: u64,
pub fixed: FacetFixed,
pub source: Option<SourceRef>,
}
#[derive(Debug, Clone)]
pub struct MaxLengthFacet {
pub value: u64,
pub fixed: FacetFixed,
pub source: Option<SourceRef>,
}
#[derive(Debug, Clone)]
pub struct PatternFacet {
pub value: String,
#[cfg(not(feature = "xsd11"))]
compiled: Option<Regex>,
#[cfg(feature = "xsd11")]
compiled: Option<Arc<regexml::Regex>>,
pub source: Option<SourceRef>,
}
impl PatternFacet {
pub fn new(
value: String,
source: Option<SourceRef>,
xsd_version: XsdVersion,
regex_compat: RegexCompat,
) -> FacetResult<Self> {
let mut facet = Self::new_unchecked(value, source);
facet.compile(xsd_version, regex_compat)?;
Ok(facet)
}
pub fn new_unchecked(value: String, source: Option<SourceRef>) -> Self {
Self {
value,
compiled: None,
source,
}
}
#[cfg(not(feature = "xsd11"))]
pub fn compile(
&mut self,
xsd_version: XsdVersion,
regex_compat: RegexCompat,
) -> FacetResult<()> {
if self.compiled.is_none() {
let effective: std::borrow::Cow<'_, str> = match regex_compat {
RegexCompat::Strict => std::borrow::Cow::Borrowed(self.value.as_str()),
RegexCompat::LenientMs => lenient_ms_preprocess(&self.value),
};
if xsd_version == XsdVersion::V1_0 && regex_compat == RegexCompat::Strict {
validate_xml_pattern_syntax(&effective).map_err(|message| {
FacetError::InvalidPattern {
pattern: self.value.clone(),
message,
}
})?;
}
let opts = match xsd_version {
XsdVersion::V1_0 => ConvertOptions::xsd_v1_0(),
XsdVersion::V1_1 => ConvertOptions::xsd(),
};
let rust_pattern = convert_xml_pattern(&effective, opts);
let compiled = Regex::new(&rust_pattern).map_err(|e| FacetError::InvalidPattern {
pattern: self.value.clone(),
message: e.to_string(),
})?;
self.compiled = Some(compiled);
}
Ok(())
}
#[cfg(feature = "xsd11")]
pub fn compile(
&mut self,
xsd_version: XsdVersion,
regex_compat: RegexCompat,
) -> FacetResult<()> {
if self.compiled.is_none() {
let strict = regex_compat == RegexCompat::Strict;
let effective: String = if strict {
self.value.clone()
} else {
lenient_ms_preprocess(&self.value).into_owned()
};
if xsd_version == XsdVersion::V1_0 && strict {
validate_xml_pattern_syntax(&effective).map_err(|message| {
FacetError::InvalidPattern {
pattern: self.value.clone(),
message,
}
})?;
}
let xsd_validated: std::borrow::Cow<'_, str> = match xsd_version {
XsdVersion::V1_0 => {
if strict {
regexml::Regex::xsd(&effective, "").map_err(|e| {
FacetError::InvalidPattern {
pattern: self.value.clone(),
message: format!("{:?}", e),
}
})?;
}
std::borrow::Cow::Borrowed(effective.as_str())
}
XsdVersion::V1_1 => validate_xsd11_pattern_with_block_fallback(&effective)?,
};
let pinned: std::borrow::Cow<'_, str> = match xsd_version {
XsdVersion::V1_0 => {
std::borrow::Cow::Owned(rewrite_xsd10_category_escapes(&effective))
}
XsdVersion::V1_1 => xsd_validated,
};
let anchored = format!("^(?:{})$", pinned);
let compiled =
regexml::Regex::xpath(&anchored, "").map_err(|e| FacetError::InvalidPattern {
pattern: self.value.clone(),
message: format!("{:?}", e),
})?;
self.compiled = Some(Arc::new(compiled));
}
Ok(())
}
#[cfg(not(feature = "xsd11"))]
pub fn matches(&self, value: &str) -> bool {
match &self.compiled {
Some(regex) => regex.is_match(value),
None => {
if let Ok(rust_pattern) = std::panic::catch_unwind(|| {
convert_xml_pattern(&self.value, ConvertOptions::xsd())
}) {
if let Ok(regex) = Regex::new(&rust_pattern) {
return regex.is_match(value);
}
}
false
}
}
}
#[cfg(feature = "xsd11")]
pub fn matches(&self, value: &str) -> bool {
match &self.compiled {
Some(regex) => regex.is_match(value),
None => {
if let Ok(rewritten) = validate_xsd11_pattern_with_block_fallback(&self.value) {
let anchored = format!("^(?:{})$", rewritten);
if let Ok(regex) = regexml::Regex::xpath(&anchored, "") {
return regex.is_match(value);
}
}
false
}
}
}
}
#[cfg(feature = "xsd11")]
fn validate_xsd11_pattern_with_block_fallback(
value: &str,
) -> FacetResult<std::borrow::Cow<'_, str>> {
let mut current: std::borrow::Cow<'_, str> = std::borrow::Cow::Borrowed(value);
for _ in 0..16 {
let err = match regexml::Regex::xsd(¤t, "") {
Ok(_) => return Ok(current),
Err(e) => format!("{:?}", e),
};
const PREFIX: &str = "Unknown Unicode block: ";
let Some(start) = err.find(PREFIX) else {
return Err(FacetError::InvalidPattern {
pattern: value.to_string(),
message: err,
});
};
let after = &err[start + PREFIX.len()..];
let end = after
.find(|c: char| !c.is_alphanumeric() && c != '-' && c != '_' && c != ' ')
.unwrap_or(after.len());
let block = after[..end].trim();
if block.is_empty() {
return Err(FacetError::InvalidPattern {
pattern: value.to_string(),
message: err,
});
}
match rewrite_pattern_isblock_token(¤t, block) {
Some(rewritten) => current = std::borrow::Cow::Owned(rewritten),
None => {
return Err(FacetError::InvalidPattern {
pattern: value.to_string(),
message: err,
});
}
}
}
if let Err(e) = regexml::Regex::xsd(¤t, "") {
return Err(FacetError::InvalidPattern {
pattern: value.to_string(),
message: format!("{:?}", e),
});
}
Ok(current)
}
#[cfg(feature = "xsd11")]
fn rewrite_pattern_isblock_token(pattern: &str, block_name: &str) -> Option<String> {
let inner_p = format!("p{{Is{}}}", block_name);
let inner_cap = format!("P{{Is{}}}", block_name);
let token_len = 1 + inner_p.len();
if !pattern.contains(&format!("\\{}", inner_p))
&& !pattern.contains(&format!("\\{}", inner_cap))
{
return None;
}
let bytes = pattern.as_bytes();
let mut result = String::with_capacity(pattern.len());
let mut i = 0;
let mut in_class = false;
let mut found = false;
while i < bytes.len() {
if bytes[i] == b'\\' && i + token_len <= bytes.len() {
let candidate = &pattern[i + 1..i + token_len];
if candidate == inner_p || candidate == inner_cap {
if in_class {
result.push_str("\\s\\S");
} else {
result.push_str("[\\s\\S]");
}
i += token_len;
found = true;
continue;
}
}
let c = bytes[i];
if c == b'\\' && i + 1 < bytes.len() {
let next_len = pattern[i + 1..]
.chars()
.next()
.map(|ch| ch.len_utf8())
.unwrap_or(1);
result.push_str(&pattern[i..i + 1 + next_len]);
i += 1 + next_len;
continue;
}
if c == b'[' {
in_class = true;
result.push('[');
i += 1;
continue;
}
if c == b']' {
in_class = false;
result.push(']');
i += 1;
continue;
}
let next_len = pattern[i..]
.chars()
.next()
.map(|ch| ch.len_utf8())
.unwrap_or(1);
result.push_str(&pattern[i..i + next_len]);
i += next_len;
}
if found {
Some(result)
} else {
None
}
}
#[derive(Debug, Clone)]
pub struct EnumerationFacet {
pub values: HashSet<String>,
pub source: Option<SourceRef>,
}
#[derive(Debug, Clone)]
pub struct WhitespaceFacet {
pub value: WhitespaceMode,
pub fixed: FacetFixed,
pub source: Option<SourceRef>,
}
#[derive(Debug, Clone)]
pub struct MinInclusiveFacet {
pub value: String,
pub fixed: FacetFixed,
pub source: Option<SourceRef>,
}
#[derive(Debug, Clone)]
pub struct MaxInclusiveFacet {
pub value: String,
pub fixed: FacetFixed,
pub source: Option<SourceRef>,
}
#[derive(Debug, Clone)]
pub struct MinExclusiveFacet {
pub value: String,
pub fixed: FacetFixed,
pub source: Option<SourceRef>,
}
#[derive(Debug, Clone)]
pub struct MaxExclusiveFacet {
pub value: String,
pub fixed: FacetFixed,
pub source: Option<SourceRef>,
}
#[derive(Debug, Clone)]
pub struct TotalDigitsFacet {
pub value: u32,
pub fixed: FacetFixed,
pub source: Option<SourceRef>,
}
#[derive(Debug, Clone)]
pub struct FractionDigitsFacet {
pub value: u32,
pub fixed: FacetFixed,
pub source: Option<SourceRef>,
}
#[derive(Debug, Clone)]
pub struct AssertionFacet {
pub test: String,
pub xpath_default_namespace: Option<String>,
pub ns_snapshot: NamespaceContextSnapshot,
pub source: Option<SourceRef>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExplicitTimezone {
Required,
Prohibited,
Optional,
}
#[derive(Debug, Clone)]
pub struct ExplicitTimezoneFacet {
pub value: ExplicitTimezone,
pub fixed: FacetFixed,
pub source: Option<SourceRef>,
}
#[derive(Debug, Clone, Default)]
pub struct FacetSet {
pub length: Option<LengthFacet>,
pub min_length: Option<MinLengthFacet>,
pub max_length: Option<MaxLengthFacet>,
pub patterns: Vec<Vec<PatternFacet>>,
pub enumeration: Option<EnumerationFacet>,
pub whitespace: Option<WhitespaceFacet>,
pub min_inclusive: Option<MinInclusiveFacet>,
pub max_inclusive: Option<MaxInclusiveFacet>,
pub min_exclusive: Option<MinExclusiveFacet>,
pub max_exclusive: Option<MaxExclusiveFacet>,
pub total_digits: Option<TotalDigitsFacet>,
pub fraction_digits: Option<FractionDigitsFacet>,
pub assertions: Vec<AssertionFacet>,
pub explicit_timezone: Option<ExplicitTimezoneFacet>,
}
impl FacetSet {
pub fn new() -> Self {
Self::default()
}
pub fn is_empty(&self) -> bool {
self.length.is_none()
&& self.min_length.is_none()
&& self.max_length.is_none()
&& self.patterns.iter().all(|step| step.is_empty())
&& self.enumeration.is_none()
&& self.whitespace.is_none()
&& self.min_inclusive.is_none()
&& self.max_inclusive.is_none()
&& self.min_exclusive.is_none()
&& self.max_exclusive.is_none()
&& self.total_digits.is_none()
&& self.fraction_digits.is_none()
&& self.assertions.is_empty()
&& self.explicit_timezone.is_none()
}
pub fn set_length(&mut self, value: u64, fixed: FacetFixed, source: Option<SourceRef>) {
self.length = Some(LengthFacet {
value,
fixed,
source,
});
}
pub fn set_min_length(&mut self, value: u64, fixed: FacetFixed, source: Option<SourceRef>) {
self.min_length = Some(MinLengthFacet {
value,
fixed,
source,
});
}
pub fn set_max_length(&mut self, value: u64, fixed: FacetFixed, source: Option<SourceRef>) {
self.max_length = Some(MaxLengthFacet {
value,
fixed,
source,
});
}
pub fn add_pattern(
&mut self,
value: String,
source: Option<SourceRef>,
xsd_version: XsdVersion,
regex_compat: RegexCompat,
) -> FacetResult<()> {
let pattern = PatternFacet::new(value, source, xsd_version, regex_compat)?;
self.push_pattern_to_current_step(pattern);
Ok(())
}
pub fn add_pattern_unchecked(&mut self, value: String, source: Option<SourceRef>) {
self.push_pattern_to_current_step(PatternFacet::new_unchecked(value, source));
}
fn push_pattern_to_current_step(&mut self, pattern: PatternFacet) {
if self.patterns.is_empty() {
self.patterns.push(Vec::new());
}
self.patterns.last_mut().unwrap().push(pattern);
}
pub fn compile_patterns(
&mut self,
xsd_version: XsdVersion,
regex_compat: RegexCompat,
) -> FacetResult<()> {
for step in &mut self.patterns {
for pattern in step {
pattern.compile(xsd_version, regex_compat)?;
}
}
Ok(())
}
fn check_patterns(&self, value: &str) -> FacetResult<()> {
for step in &self.patterns {
if step.is_empty() {
continue;
}
if !step.iter().any(|p| p.matches(value)) {
return Err(FacetError::pattern(value, &step[0].value));
}
}
Ok(())
}
pub fn add_enumeration(&mut self, value: String, source: Option<SourceRef>) {
let enumeration = self.enumeration.get_or_insert_with(|| EnumerationFacet {
values: HashSet::new(),
source: source.clone(),
});
enumeration.values.insert(value);
}
pub fn set_whitespace(
&mut self,
value: WhitespaceMode,
fixed: FacetFixed,
source: Option<SourceRef>,
) {
self.whitespace = Some(WhitespaceFacet {
value,
fixed,
source,
});
}
pub fn set_min_inclusive(
&mut self,
value: String,
fixed: FacetFixed,
source: Option<SourceRef>,
) {
self.min_inclusive = Some(MinInclusiveFacet {
value,
fixed,
source,
});
}
pub fn set_max_inclusive(
&mut self,
value: String,
fixed: FacetFixed,
source: Option<SourceRef>,
) {
self.max_inclusive = Some(MaxInclusiveFacet {
value,
fixed,
source,
});
}
pub fn set_min_exclusive(
&mut self,
value: String,
fixed: FacetFixed,
source: Option<SourceRef>,
) {
self.min_exclusive = Some(MinExclusiveFacet {
value,
fixed,
source,
});
}
pub fn set_max_exclusive(
&mut self,
value: String,
fixed: FacetFixed,
source: Option<SourceRef>,
) {
self.max_exclusive = Some(MaxExclusiveFacet {
value,
fixed,
source,
});
}
pub fn set_total_digits(&mut self, value: u32, fixed: FacetFixed, source: Option<SourceRef>) {
self.total_digits = Some(TotalDigitsFacet {
value,
fixed,
source,
});
}
pub fn set_fraction_digits(
&mut self,
value: u32,
fixed: FacetFixed,
source: Option<SourceRef>,
) {
self.fraction_digits = Some(FractionDigitsFacet {
value,
fixed,
source,
});
}
pub fn add_assertion(
&mut self,
test: String,
xpath_default_namespace: Option<String>,
ns_snapshot: NamespaceContextSnapshot,
source: Option<SourceRef>,
) {
self.assertions.push(AssertionFacet {
test,
xpath_default_namespace,
ns_snapshot,
source,
});
}
pub fn set_explicit_timezone(
&mut self,
value: ExplicitTimezone,
fixed: FacetFixed,
source: Option<SourceRef>,
) {
self.explicit_timezone = Some(ExplicitTimezoneFacet {
value,
fixed,
source,
});
}
pub fn inherit_from(&mut self, base: &FacetSet) {
if self.length.is_none() {
self.length = base.length.clone();
}
if self.min_length.is_none() {
self.min_length = base.min_length.clone();
}
if self.max_length.is_none() {
self.max_length = base.max_length.clone();
}
for base_step in &base.patterns {
if !base_step.is_empty() {
self.patterns.push(base_step.clone());
}
}
if self.whitespace.is_none() {
self.whitespace = base.whitespace.clone();
}
if self.min_inclusive.is_none() {
self.min_inclusive = base.min_inclusive.clone();
}
if self.max_inclusive.is_none() {
self.max_inclusive = base.max_inclusive.clone();
}
if self.min_exclusive.is_none() {
self.min_exclusive = base.min_exclusive.clone();
}
if self.max_exclusive.is_none() {
self.max_exclusive = base.max_exclusive.clone();
}
if self.total_digits.is_none() {
self.total_digits = base.total_digits.clone();
}
if self.fraction_digits.is_none() {
self.fraction_digits = base.fraction_digits.clone();
}
for assertion in &base.assertions {
self.assertions.push(assertion.clone());
}
if self.explicit_timezone.is_none() {
self.explicit_timezone = base.explicit_timezone.clone();
}
}
pub fn merge_with_base(&self, base: &FacetSet) -> FacetResult<FacetSet> {
if self.length.is_some() && self.min_length.is_some() {
return Err(FacetError::conflicting(
"length and minLength cannot both appear in the same restriction step",
));
}
if self.length.is_some() && self.max_length.is_some() {
return Err(FacetError::conflicting(
"length and maxLength cannot both appear in the same restriction step",
));
}
let mut result = self.clone();
if let Some(ref base_length) = base.length {
match &result.length {
Some(derived) => {
if base_length.fixed == FacetFixed::Fixed && derived.value != base_length.value
{
return Err(FacetError::fixed_violation(
"length",
base_length.value.to_string(),
derived.value.to_string(),
));
}
}
None => {
result.length = Some(base_length.clone());
}
}
}
if let Some(ref base_min) = base.min_length {
match &result.min_length {
Some(derived) => {
if base_min.fixed == FacetFixed::Fixed && derived.value != base_min.value {
return Err(FacetError::fixed_violation(
"minLength",
base_min.value.to_string(),
derived.value.to_string(),
));
}
if derived.value < base_min.value {
return Err(FacetError::derivation(format!(
"minLength {} is less restrictive than base minLength {}",
derived.value, base_min.value
)));
}
}
None => {
result.min_length = Some(base_min.clone());
}
}
}
if let Some(ref base_max) = base.max_length {
match &result.max_length {
Some(derived) => {
if base_max.fixed == FacetFixed::Fixed && derived.value != base_max.value {
return Err(FacetError::fixed_violation(
"maxLength",
base_max.value.to_string(),
derived.value.to_string(),
));
}
if derived.value > base_max.value {
return Err(FacetError::derivation(format!(
"maxLength {} is less restrictive than base maxLength {}",
derived.value, base_max.value
)));
}
}
None => {
result.max_length = Some(base_max.clone());
}
}
}
for base_step in &base.patterns {
if !base_step.is_empty() {
result.patterns.push(base_step.clone());
}
}
if let Some(ref base_enum) = base.enumeration {
match &result.enumeration {
Some(derived_enum) => {
for value in &derived_enum.values {
if !base_enum.values.contains(value) {
return Err(FacetError::derivation(format!(
"enumeration value '{}' is not in base enumeration",
value
)));
}
}
}
None => {
result.enumeration = Some(base_enum.clone());
}
}
}
if let Some(ref base_ws) = base.whitespace {
match &result.whitespace {
Some(derived) => {
if base_ws.fixed == FacetFixed::Fixed && derived.value != base_ws.value {
return Err(FacetError::fixed_violation(
"whiteSpace",
format!("{:?}", base_ws.value),
format!("{:?}", derived.value),
));
}
if !is_whitespace_more_restrictive(derived.value, base_ws.value) {
return Err(FacetError::derivation(format!(
"whiteSpace {:?} is less restrictive than base {:?}",
derived.value, base_ws.value
)));
}
}
None => {
result.whitespace = Some(base_ws.clone());
}
}
}
if let Some(ref base_facet) = base.min_inclusive {
if let Some(ref derived) = result.min_inclusive {
if base_facet.fixed == FacetFixed::Fixed && derived.value != base_facet.value {
return Err(FacetError::fixed_violation(
"minInclusive",
&base_facet.value,
&derived.value,
));
}
} else if result.min_exclusive.is_none() {
result.min_inclusive = Some(base_facet.clone());
}
}
if let Some(ref base_facet) = base.max_inclusive {
if let Some(ref derived) = result.max_inclusive {
if base_facet.fixed == FacetFixed::Fixed && derived.value != base_facet.value {
return Err(FacetError::fixed_violation(
"maxInclusive",
&base_facet.value,
&derived.value,
));
}
} else if result.max_exclusive.is_none() {
result.max_inclusive = Some(base_facet.clone());
}
}
if let Some(ref base_facet) = base.min_exclusive {
if let Some(ref derived) = result.min_exclusive {
if base_facet.fixed == FacetFixed::Fixed && derived.value != base_facet.value {
return Err(FacetError::fixed_violation(
"minExclusive",
&base_facet.value,
&derived.value,
));
}
} else if result.min_inclusive.is_none() {
result.min_exclusive = Some(base_facet.clone());
}
}
if let Some(ref base_facet) = base.max_exclusive {
if let Some(ref derived) = result.max_exclusive {
if base_facet.fixed == FacetFixed::Fixed && derived.value != base_facet.value {
return Err(FacetError::fixed_violation(
"maxExclusive",
&base_facet.value,
&derived.value,
));
}
} else if result.max_inclusive.is_none() {
result.max_exclusive = Some(base_facet.clone());
}
}
if let Some(ref base_td) = base.total_digits {
match &result.total_digits {
Some(derived) => {
if base_td.fixed == FacetFixed::Fixed && derived.value != base_td.value {
return Err(FacetError::fixed_violation(
"totalDigits",
base_td.value.to_string(),
derived.value.to_string(),
));
}
if derived.value > base_td.value {
return Err(FacetError::derivation(format!(
"totalDigits {} is less restrictive than base totalDigits {}",
derived.value, base_td.value
)));
}
}
None => {
result.total_digits = Some(base_td.clone());
}
}
}
if let Some(ref base_fd) = base.fraction_digits {
match &result.fraction_digits {
Some(derived) => {
if base_fd.fixed == FacetFixed::Fixed && derived.value != base_fd.value {
return Err(FacetError::fixed_violation(
"fractionDigits",
base_fd.value.to_string(),
derived.value.to_string(),
));
}
if derived.value > base_fd.value {
return Err(FacetError::derivation(format!(
"fractionDigits {} is less restrictive than base fractionDigits {}",
derived.value, base_fd.value
)));
}
}
None => {
result.fraction_digits = Some(base_fd.clone());
}
}
}
for assertion in &base.assertions {
result.assertions.push(assertion.clone());
}
if let Some(ref base_etz) = base.explicit_timezone {
if let Some(ref derived) = result.explicit_timezone {
let restriction_ok = match base_etz.value {
ExplicitTimezone::Optional => true,
ExplicitTimezone::Required => derived.value == ExplicitTimezone::Required,
ExplicitTimezone::Prohibited => derived.value == ExplicitTimezone::Prohibited,
};
if !restriction_ok {
return Err(FacetError::derivation(format!(
"explicitTimezone {:?} is not a valid restriction of base {:?}",
derived.value, base_etz.value
)));
}
if base_etz.fixed == FacetFixed::Fixed && derived.value != base_etz.value {
return Err(FacetError::fixed_violation(
"explicitTimezone",
format!("{:?}", base_etz.value),
format!("{:?}", derived.value),
));
}
} else {
result.explicit_timezone = Some(base_etz.clone());
}
}
result.validate_consistency()?;
Ok(result)
}
fn validate_consistency(&self) -> FacetResult<()> {
if let (Some(min), Some(max)) = (&self.min_length, &self.max_length) {
if min.value > max.value {
return Err(FacetError::conflicting(format!(
"minLength {} is greater than maxLength {}",
min.value, max.value
)));
}
}
if let Some(len) = &self.length {
if let Some(min) = &self.min_length {
if len.value < min.value {
return Err(FacetError::conflicting(format!(
"length {} is less than minLength {}",
len.value, min.value
)));
}
}
if let Some(max) = &self.max_length {
if len.value > max.value {
return Err(FacetError::conflicting(format!(
"length {} is greater than maxLength {}",
len.value, max.value
)));
}
}
}
if self.min_inclusive.is_some() && self.min_exclusive.is_some() {
return Err(FacetError::conflicting(
"cannot have both minInclusive and minExclusive",
));
}
if self.max_inclusive.is_some() && self.max_exclusive.is_some() {
return Err(FacetError::conflicting(
"cannot have both maxInclusive and maxExclusive",
));
}
if let (Some(fd), Some(td)) = (&self.fraction_digits, &self.total_digits) {
if fd.value > td.value {
return Err(FacetError::conflicting(format!(
"fractionDigits {} is greater than totalDigits {}",
fd.value, td.value
)));
}
}
if let (Some(min_incl), Some(max_incl)) = (&self.min_inclusive, &self.max_inclusive) {
if let Some(cmp) = compare_decimal_strings(&min_incl.value, &max_incl.value) {
if cmp == std::cmp::Ordering::Greater {
return Err(FacetError::conflicting(format!(
"minInclusive '{}' is greater than maxInclusive '{}'",
min_incl.value, max_incl.value
)));
}
}
}
if let (Some(min_excl), Some(max_excl)) = (&self.min_exclusive, &self.max_exclusive) {
if let Some(cmp) = compare_decimal_strings(&min_excl.value, &max_excl.value) {
if cmp != std::cmp::Ordering::Less {
return Err(FacetError::conflicting(format!(
"minExclusive '{}' must be less than maxExclusive '{}'",
min_excl.value, max_excl.value
)));
}
}
}
if let (Some(min_incl), Some(max_excl)) = (&self.min_inclusive, &self.max_exclusive) {
if let Some(cmp) = compare_decimal_strings(&min_incl.value, &max_excl.value) {
if cmp != std::cmp::Ordering::Less {
return Err(FacetError::conflicting(format!(
"minInclusive '{}' must be less than maxExclusive '{}'",
min_incl.value, max_excl.value
)));
}
}
}
if let (Some(min_excl), Some(max_incl)) = (&self.min_exclusive, &self.max_inclusive) {
if let Some(cmp) = compare_decimal_strings(&min_excl.value, &max_incl.value) {
if cmp != std::cmp::Ordering::Less {
return Err(FacetError::conflicting(format!(
"minExclusive '{}' must be less than maxInclusive '{}'",
min_excl.value, max_incl.value
)));
}
}
}
Ok(())
}
pub fn validate_string(&self, value: &str) -> FacetResult<()> {
let normalized = match &self.whitespace {
Some(ws) => normalize_whitespace(value, ws.value),
None => value.to_string(),
};
let check_value = &normalized;
if let Some(ref length) = self.length {
let len = check_value.chars().count() as u64;
if len != length.value {
return Err(FacetError::length(format!(
"value length {} does not equal required length {}",
len, length.value
)));
}
}
if let Some(ref min_length) = self.min_length {
let len = check_value.chars().count() as u64;
if len < min_length.value {
return Err(FacetError::MinLengthViolation {
actual: len,
min: min_length.value,
});
}
}
if let Some(ref max_length) = self.max_length {
let len = check_value.chars().count() as u64;
if len > max_length.value {
return Err(FacetError::MaxLengthViolation {
actual: len,
max: max_length.value,
});
}
}
self.check_patterns(check_value)?;
if let Some(ref enumeration) = self.enumeration {
if !enumeration.values.contains(check_value) {
return Err(FacetError::enumeration(check_value));
}
}
Ok(())
}
pub fn validate_string_patterns_enums(&self, value: &str) -> FacetResult<()> {
let normalized = match &self.whitespace {
Some(ws) => normalize_whitespace(value, ws.value),
None => value.to_string(),
};
let check_value = &normalized;
self.check_patterns(check_value)?;
if let Some(ref enumeration) = self.enumeration {
if !enumeration.values.contains(check_value) {
return Err(FacetError::enumeration(check_value));
}
}
Ok(())
}
pub fn validate_patterns_only(&self, value: &str) -> FacetResult<()> {
let normalized = match &self.whitespace {
Some(ws) => normalize_whitespace(value, ws.value),
None => value.to_string(),
};
self.check_patterns(&normalized)
}
pub fn validate_enum_value_space(
&self,
is_match: impl Fn(&str) -> bool,
display: &str,
) -> FacetResult<()> {
if let Some(ref enumeration) = self.enumeration {
if !enumeration.values.iter().any(|s| is_match(s)) {
return Err(FacetError::enumeration(display));
}
}
Ok(())
}
pub fn validate_decimal(&self, value: &rust_decimal::Decimal) -> FacetResult<()> {
if let Some(ref td) = self.total_digits {
let total = count_total_digits(value);
if total > td.value {
return Err(FacetError::TotalDigitsViolation {
actual: total,
max: td.value,
});
}
}
if let Some(ref fd) = self.fraction_digits {
let frac = count_fraction_digits(value);
if frac > fd.value {
return Err(FacetError::FractionDigitsViolation {
actual: frac,
max: fd.value,
});
}
}
if let Some(ref min) = self.min_inclusive {
if let Ok(bound) = rust_decimal::Decimal::from_str_exact(&min.value) {
if *value < bound {
return Err(FacetError::MinInclusiveViolation {
value: value.to_string(),
min: min.value.clone(),
});
}
}
}
if let Some(ref max) = self.max_inclusive {
if let Ok(bound) = rust_decimal::Decimal::from_str_exact(&max.value) {
if *value > bound {
return Err(FacetError::MaxInclusiveViolation {
value: value.to_string(),
max: max.value.clone(),
});
}
}
}
if let Some(ref min) = self.min_exclusive {
if let Ok(bound) = rust_decimal::Decimal::from_str_exact(&min.value) {
if *value <= bound {
return Err(FacetError::MinExclusiveViolation {
value: value.to_string(),
min: min.value.clone(),
});
}
}
}
if let Some(ref max) = self.max_exclusive {
if let Ok(bound) = rust_decimal::Decimal::from_str_exact(&max.value) {
if *value >= bound {
return Err(FacetError::MaxExclusiveViolation {
value: value.to_string(),
max: max.value.clone(),
});
}
}
}
Ok(())
}
pub fn validate_float(&self, value: f32) -> FacetResult<()> {
if value.is_nan() {
return Ok(());
}
if let Some(ref min) = self.min_inclusive {
if let Ok(bound) = min.value.parse::<f32>() {
if !bound.is_nan() && value < bound {
return Err(FacetError::MinInclusiveViolation {
value: format_float_for_error(value),
min: min.value.clone(),
});
}
}
}
if let Some(ref max) = self.max_inclusive {
if let Ok(bound) = max.value.parse::<f32>() {
if !bound.is_nan() && value > bound {
return Err(FacetError::MaxInclusiveViolation {
value: format_float_for_error(value),
max: max.value.clone(),
});
}
}
}
if let Some(ref min) = self.min_exclusive {
if let Ok(bound) = min.value.parse::<f32>() {
if !bound.is_nan() && value <= bound {
return Err(FacetError::MinExclusiveViolation {
value: format_float_for_error(value),
min: min.value.clone(),
});
}
}
}
if let Some(ref max) = self.max_exclusive {
if let Ok(bound) = max.value.parse::<f32>() {
if !bound.is_nan() && value >= bound {
return Err(FacetError::MaxExclusiveViolation {
value: format_float_for_error(value),
max: max.value.clone(),
});
}
}
}
Ok(())
}
pub fn validate_double(&self, value: f64) -> FacetResult<()> {
if value.is_nan() {
return Ok(());
}
if let Some(ref min) = self.min_inclusive {
if let Ok(bound) = min.value.parse::<f64>() {
if !bound.is_nan() && value < bound {
return Err(FacetError::MinInclusiveViolation {
value: format_double_for_error(value),
min: min.value.clone(),
});
}
}
}
if let Some(ref max) = self.max_inclusive {
if let Ok(bound) = max.value.parse::<f64>() {
if !bound.is_nan() && value > bound {
return Err(FacetError::MaxInclusiveViolation {
value: format_double_for_error(value),
max: max.value.clone(),
});
}
}
}
if let Some(ref min) = self.min_exclusive {
if let Ok(bound) = min.value.parse::<f64>() {
if !bound.is_nan() && value <= bound {
return Err(FacetError::MinExclusiveViolation {
value: format_double_for_error(value),
min: min.value.clone(),
});
}
}
}
if let Some(ref max) = self.max_exclusive {
if let Ok(bound) = max.value.parse::<f64>() {
if !bound.is_nan() && value >= bound {
return Err(FacetError::MaxExclusiveViolation {
value: format_double_for_error(value),
max: max.value.clone(),
});
}
}
}
Ok(())
}
pub fn validate_explicit_timezone(&self, has_timezone: bool) -> FacetResult<()> {
if let Some(ref etz) = self.explicit_timezone {
match etz.value {
ExplicitTimezone::Required if !has_timezone => {
return Err(FacetError::ExplicitTimezoneViolation {
message: "timezone is required but not present".to_string(),
});
}
ExplicitTimezone::Prohibited if has_timezone => {
return Err(FacetError::ExplicitTimezoneViolation {
message: "timezone is prohibited but present".to_string(),
});
}
ExplicitTimezone::Optional
| ExplicitTimezone::Required
| ExplicitTimezone::Prohibited => {
}
}
}
Ok(())
}
pub fn validate_binary_length(&self, byte_count: u64) -> FacetResult<()> {
if let Some(ref length) = self.length {
if byte_count != length.value {
return Err(FacetError::length(format!(
"binary length {} does not equal required length {}",
byte_count, length.value
)));
}
}
if let Some(ref min_length) = self.min_length {
if byte_count < min_length.value {
return Err(FacetError::MinLengthViolation {
actual: byte_count,
min: min_length.value,
});
}
}
if let Some(ref max_length) = self.max_length {
if byte_count > max_length.value {
return Err(FacetError::MaxLengthViolation {
actual: byte_count,
max: max_length.value,
});
}
}
Ok(())
}
pub fn validate_list_length(&self, item_count: u64) -> FacetResult<()> {
if let Some(ref length) = self.length {
if item_count != length.value {
return Err(FacetError::length(format!(
"list length {} does not equal required length {}",
item_count, length.value
)));
}
}
if let Some(ref min_length) = self.min_length {
if item_count < min_length.value {
return Err(FacetError::MinLengthViolation {
actual: item_count,
min: min_length.value,
});
}
}
if let Some(ref max_length) = self.max_length {
if item_count > max_length.value {
return Err(FacetError::MaxLengthViolation {
actual: item_count,
max: max_length.value,
});
}
}
Ok(())
}
}
fn is_whitespace_more_restrictive(derived: WhitespaceMode, base: WhitespaceMode) -> bool {
use WhitespaceMode::*;
match (base, derived) {
(Preserve, Preserve) | (Replace, Replace) | (Collapse, Collapse) => true,
(Preserve, Replace) | (Preserve, Collapse) | (Replace, Collapse) => true,
_ => false,
}
}
fn compare_decimal_strings(a: &str, b: &str) -> Option<std::cmp::Ordering> {
let a_val: f64 = a.trim().parse().ok()?;
let b_val: f64 = b.trim().parse().ok()?;
a_val.partial_cmp(&b_val)
}
pub fn normalize_whitespace(s: &str, mode: WhitespaceMode) -> String {
match mode {
WhitespaceMode::Preserve => s.to_string(),
WhitespaceMode::Replace => {
s.chars()
.map(|c| match c {
'\t' | '\r' | '\n' => ' ',
_ => c,
})
.collect()
}
WhitespaceMode::Collapse => {
let replaced: String = s
.chars()
.map(|c| match c {
'\t' | '\r' | '\n' => ' ',
_ => c,
})
.collect();
let mut result = String::with_capacity(replaced.len());
let mut prev_space = true;
for c in replaced.chars() {
if c == ' ' {
if !prev_space {
result.push(' ');
prev_space = true;
}
} else {
result.push(c);
prev_space = false;
}
}
if result.ends_with(' ') {
result.pop();
}
result
}
}
}
fn count_total_digits(value: &rust_decimal::Decimal) -> u32 {
let normalized = value.abs().normalize();
let mut m = normalized.mantissa().unsigned_abs();
if m == 0 {
return 1;
}
let mut count = 0u32;
while m > 0 {
count += 1;
m /= 10;
}
count
}
fn count_fraction_digits(value: &rust_decimal::Decimal) -> u32 {
let s = value.normalize().to_string();
match s.find('.') {
Some(pos) => (s.len() - pos - 1) as u32,
None => 0,
}
}
fn format_float_for_error(v: f32) -> String {
if v.is_nan() {
"NaN".to_string()
} else if v.is_infinite() {
if v.is_sign_positive() {
"INF".to_string()
} else {
"-INF".to_string()
}
} else {
v.to_string()
}
}
fn format_double_for_error(v: f64) -> String {
if v.is_nan() {
"NaN".to_string()
} else if v.is_infinite() {
if v.is_sign_positive() {
"INF".to_string()
} else {
"-INF".to_string()
}
} else {
v.to_string()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FacetApplicability {
NotApplicable,
Applicable,
Required,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FacetKind {
Length,
MinLength,
MaxLength,
Pattern,
Enumeration,
Whitespace,
MinInclusive,
MaxInclusive,
MinExclusive,
MaxExclusive,
TotalDigits,
FractionDigits,
ExplicitTimezone,
Assertion,
}
impl FacetKind {
pub fn from_name(name: &str) -> Option<Self> {
match name {
"length" => Some(Self::Length),
"minLength" => Some(Self::MinLength),
"maxLength" => Some(Self::MaxLength),
"pattern" => Some(Self::Pattern),
"enumeration" => Some(Self::Enumeration),
"whiteSpace" => Some(Self::Whitespace),
"minInclusive" => Some(Self::MinInclusive),
"maxInclusive" => Some(Self::MaxInclusive),
"minExclusive" => Some(Self::MinExclusive),
"maxExclusive" => Some(Self::MaxExclusive),
"totalDigits" => Some(Self::TotalDigits),
"fractionDigits" => Some(Self::FractionDigits),
"explicitTimezone" => Some(Self::ExplicitTimezone),
"assertion" => Some(Self::Assertion),
_ => None,
}
}
pub fn name(&self) -> &'static str {
match self {
Self::Length => "length",
Self::MinLength => "minLength",
Self::MaxLength => "maxLength",
Self::Pattern => "pattern",
Self::Enumeration => "enumeration",
Self::Whitespace => "whiteSpace",
Self::MinInclusive => "minInclusive",
Self::MaxInclusive => "maxInclusive",
Self::MinExclusive => "minExclusive",
Self::MaxExclusive => "maxExclusive",
Self::TotalDigits => "totalDigits",
Self::FractionDigits => "fractionDigits",
Self::ExplicitTimezone => "explicitTimezone",
Self::Assertion => "assertion",
}
}
}
pub fn facet_applicable_for_type(facet: FacetKind, type_code: XmlTypeCode) -> FacetApplicability {
use FacetApplicability::*;
use FacetKind::*;
use XmlTypeCode::*;
match facet {
Length | MinLength | MaxLength => match type_code {
String | NormalizedString | Token | Language | NmToken | Name | NCName | Id | IdRef
| Entity | HexBinary | Base64Binary | AnyUri | QName | Notation | NmTokens | IdRefs
| Entities => Applicable,
_ => NotApplicable,
},
Pattern | Enumeration => {
if type_code.is_atomic() || type_code == AnySimpleType || type_code == AnyAtomicType {
Applicable
} else {
NotApplicable
}
}
Whitespace => match type_code {
String => Required,
NormalizedString | Token | Language | NmToken | Name | NCName | Id | IdRef | Entity => {
Applicable
}
_ if type_code.is_atomic() => Applicable,
_ => NotApplicable,
},
MinInclusive | MaxInclusive | MinExclusive | MaxExclusive => match type_code {
Decimal | Integer | NonPositiveInteger | NegativeInteger | NonNegativeInteger
| PositiveInteger | Long | Int | Short | Byte | UnsignedLong | UnsignedInt
| UnsignedShort | UnsignedByte => Applicable,
Float | Double => Applicable,
Duration | DateTime | Time | Date | GYearMonth | GYear | GMonthDay | GDay | GMonth
| YearMonthDuration | DayTimeDuration | DateTimeStamp => Applicable,
_ => NotApplicable,
},
TotalDigits => match type_code {
Decimal | Integer | NonPositiveInteger | NegativeInteger | NonNegativeInteger
| PositiveInteger | Long | Int | Short | Byte | UnsignedLong | UnsignedInt
| UnsignedShort | UnsignedByte => Applicable,
_ => NotApplicable,
},
FractionDigits => match type_code {
Decimal => Applicable,
Integer | NonPositiveInteger | NegativeInteger | NonNegativeInteger
| PositiveInteger | Long | Int | Short | Byte | UnsignedLong | UnsignedInt
| UnsignedShort | UnsignedByte => Applicable,
_ => NotApplicable,
},
ExplicitTimezone => match type_code {
DateTime | Time | Date | GYearMonth | GYear | GMonthDay | GDay | GMonth
| DateTimeStamp => Applicable,
_ => NotApplicable,
},
Assertion => Applicable,
}
}
pub fn facet_applicable(type_name: &str, facet_name: &str) -> FacetApplicability {
let facet = match FacetKind::from_name(facet_name) {
Some(f) => f,
None => return FacetApplicability::NotApplicable,
};
let type_code = match XmlTypeCode::from_local_name(type_name) {
Some(tc) => tc,
None => return FacetApplicability::NotApplicable,
};
facet_applicable_for_type(facet, type_code)
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal::Decimal;
use std::str::FromStr;
#[test]
fn test_facet_set_empty() {
let facets = FacetSet::new();
assert!(facets.is_empty());
}
#[test]
fn test_facet_set_length() {
let mut facets = FacetSet::new();
facets.set_length(10, FacetFixed::Default, None);
assert!(!facets.is_empty());
assert_eq!(facets.length.as_ref().unwrap().value, 10);
}
#[test]
fn test_facet_set_patterns() {
let mut facets = FacetSet::new();
facets
.add_pattern("[a-z]+".to_string(), None, XsdVersion::V1_1, RegexCompat::Strict)
.unwrap();
facets
.add_pattern("[0-9]+".to_string(), None, XsdVersion::V1_1, RegexCompat::Strict)
.unwrap();
assert_eq!(facets.patterns.len(), 1);
assert_eq!(facets.patterns[0].len(), 2);
}
#[test]
fn test_facet_set_enumeration() {
let mut facets = FacetSet::new();
facets.add_enumeration("red".to_string(), None);
facets.add_enumeration("green".to_string(), None);
facets.add_enumeration("blue".to_string(), None);
let enum_facet = facets.enumeration.as_ref().unwrap();
assert_eq!(enum_facet.values.len(), 3);
assert!(enum_facet.values.contains("red"));
}
#[test]
fn test_facet_inheritance() {
let mut base = FacetSet::new();
base.set_min_length(5, FacetFixed::Fixed, None);
base.set_max_length(100, FacetFixed::Default, None);
base.add_pattern("[a-z]+".to_string(), None, XsdVersion::V1_1, RegexCompat::Strict)
.unwrap();
let mut derived = FacetSet::new();
derived.set_max_length(50, FacetFixed::Default, None);
derived.inherit_from(&base);
assert_eq!(derived.min_length.as_ref().unwrap().value, 5);
assert_eq!(derived.max_length.as_ref().unwrap().value, 50);
assert_eq!(derived.patterns.len(), 1);
assert_eq!(derived.patterns[0].len(), 1);
}
#[test]
fn test_facet_applicability() {
use FacetApplicability::*;
assert_eq!(facet_applicable("string", "length"), Applicable);
assert_eq!(facet_applicable("decimal", "length"), NotApplicable);
assert_eq!(facet_applicable("decimal", "minInclusive"), Applicable);
assert_eq!(facet_applicable("string", "minInclusive"), NotApplicable);
assert_eq!(facet_applicable("string", "pattern"), Applicable);
assert_eq!(facet_applicable("decimal", "pattern"), Applicable);
assert_eq!(facet_applicable("string", "whiteSpace"), Required);
}
#[test]
fn test_facet_applicability_with_type_code() {
use FacetApplicability::*;
use FacetKind::*;
use XmlTypeCode::*;
assert_eq!(facet_applicable_for_type(Length, String), Applicable);
assert_eq!(facet_applicable_for_type(Length, HexBinary), Applicable);
assert_eq!(facet_applicable_for_type(Length, Decimal), NotApplicable);
assert_eq!(facet_applicable_for_type(TotalDigits, Decimal), Applicable);
assert_eq!(facet_applicable_for_type(TotalDigits, Integer), Applicable);
assert_eq!(facet_applicable_for_type(TotalDigits, Float), NotApplicable);
assert_eq!(
facet_applicable_for_type(ExplicitTimezone, DateTime),
Applicable
);
assert_eq!(
facet_applicable_for_type(ExplicitTimezone, String),
NotApplicable
);
}
#[test]
fn test_pattern_matching() {
let pattern =
PatternFacet::new("[a-z]+".to_string(), None, XsdVersion::V1_1, RegexCompat::Strict)
.unwrap();
assert!(pattern.matches("hello"));
assert!(!pattern.matches("HELLO"));
assert!(!pattern.matches("hello123"));
}
#[test]
fn test_pattern_xsd_anchoring() {
let pattern =
PatternFacet::new("abc".to_string(), None, XsdVersion::V1_1, RegexCompat::Strict)
.unwrap();
assert!(pattern.matches("abc"));
assert!(!pattern.matches("xabc"));
assert!(!pattern.matches("abcx"));
}
#[test]
fn test_pattern_xsd_name_chars() {
let pattern =
PatternFacet::new(r"\i\c*".to_string(), None, XsdVersion::V1_1, RegexCompat::Strict)
.unwrap();
assert!(pattern.matches("foo"));
assert!(pattern.matches("_bar"));
assert!(pattern.matches("x123"));
assert!(!pattern.matches("123"));
}
#[test]
fn test_invalid_pattern() {
let result =
PatternFacet::new("[invalid".to_string(), None, XsdVersion::V1_1, RegexCompat::Strict);
assert!(result.is_err());
}
#[test]
fn test_whitespace_preserve() {
let result = normalize_whitespace(" hello\t\nworld ", WhitespaceMode::Preserve);
assert_eq!(result, " hello\t\nworld ");
}
#[test]
fn test_whitespace_replace() {
let result = normalize_whitespace(" hello\t\nworld ", WhitespaceMode::Replace);
assert_eq!(result, " hello world ");
}
#[test]
fn test_whitespace_collapse() {
let result = normalize_whitespace(" hello\t\nworld ", WhitespaceMode::Collapse);
assert_eq!(result, "hello world");
}
#[test]
fn test_whitespace_collapse_multiple_spaces() {
let result = normalize_whitespace("a b", WhitespaceMode::Collapse);
assert_eq!(result, "a b");
}
#[test]
fn test_validate_string_length() {
let mut facets = FacetSet::new();
facets.set_length(5, FacetFixed::Default, None);
assert!(facets.validate_string("hello").is_ok());
assert!(facets.validate_string("hi").is_err());
assert!(facets.validate_string("toolong").is_err());
}
#[test]
fn test_validate_string_min_max_length() {
let mut facets = FacetSet::new();
facets.set_min_length(3, FacetFixed::Default, None);
facets.set_max_length(10, FacetFixed::Default, None);
assert!(facets.validate_string("hello").is_ok());
assert!(facets.validate_string("hi").is_err());
assert!(facets.validate_string("this is way too long").is_err());
}
#[test]
fn test_validate_string_pattern() {
let mut facets = FacetSet::new();
facets
.add_pattern("[a-z]+".to_string(), None, XsdVersion::V1_1, RegexCompat::Strict)
.unwrap();
assert!(facets.validate_string("hello").is_ok());
assert!(facets.validate_string("HELLO").is_err());
}
#[test]
fn test_validate_string_enumeration() {
let mut facets = FacetSet::new();
facets.add_enumeration("red".to_string(), None);
facets.add_enumeration("green".to_string(), None);
facets.add_enumeration("blue".to_string(), None);
assert!(facets.validate_string("red").is_ok());
assert!(facets.validate_string("yellow").is_err());
}
#[test]
fn test_validate_decimal_total_digits() {
let mut facets = FacetSet::new();
facets.set_total_digits(5, FacetFixed::Default, None);
let val = Decimal::from_str("12345").unwrap();
assert!(facets.validate_decimal(&val).is_ok());
let val = Decimal::from_str("123456").unwrap();
assert!(facets.validate_decimal(&val).is_err());
}
#[test]
fn test_validate_decimal_fraction_digits() {
let mut facets = FacetSet::new();
facets.set_fraction_digits(2, FacetFixed::Default, None);
let val = Decimal::from_str("123.45").unwrap();
assert!(facets.validate_decimal(&val).is_ok());
let val = Decimal::from_str("123.456").unwrap();
assert!(facets.validate_decimal(&val).is_err());
}
#[test]
fn test_validate_decimal_bounds() {
let mut facets = FacetSet::new();
facets.set_min_inclusive("0".to_string(), FacetFixed::Default, None);
facets.set_max_inclusive("100".to_string(), FacetFixed::Default, None);
let val = Decimal::from_str("50").unwrap();
assert!(facets.validate_decimal(&val).is_ok());
let val = Decimal::from_str("-1").unwrap();
assert!(facets.validate_decimal(&val).is_err());
let val = Decimal::from_str("101").unwrap();
assert!(facets.validate_decimal(&val).is_err());
}
#[test]
fn test_validate_decimal_exclusive_bounds() {
let mut facets = FacetSet::new();
facets.set_min_exclusive("0".to_string(), FacetFixed::Default, None);
facets.set_max_exclusive("100".to_string(), FacetFixed::Default, None);
let val = Decimal::from_str("0").unwrap();
assert!(facets.validate_decimal(&val).is_err());
let val = Decimal::from_str("100").unwrap();
assert!(facets.validate_decimal(&val).is_err());
let val = Decimal::from_str("50").unwrap();
assert!(facets.validate_decimal(&val).is_ok());
}
#[test]
fn test_validate_binary_length() {
let mut facets = FacetSet::new();
facets.set_length(4, FacetFixed::Default, None);
assert!(facets.validate_binary_length(4).is_ok());
assert!(facets.validate_binary_length(3).is_err());
assert!(facets.validate_binary_length(5).is_err());
}
#[test]
fn test_validate_list_length() {
let mut facets = FacetSet::new();
facets.set_min_length(1, FacetFixed::Default, None);
facets.set_max_length(5, FacetFixed::Default, None);
assert!(facets.validate_list_length(3).is_ok());
assert!(facets.validate_list_length(0).is_err());
assert!(facets.validate_list_length(10).is_err());
}
#[test]
fn test_merge_with_base_inherits_facets() {
let mut base = FacetSet::new();
base.set_min_length(5, FacetFixed::Default, None);
base.set_max_length(100, FacetFixed::Default, None);
let derived = FacetSet::new();
let merged = derived.merge_with_base(&base).unwrap();
assert_eq!(merged.min_length.as_ref().unwrap().value, 5);
assert_eq!(merged.max_length.as_ref().unwrap().value, 100);
}
#[test]
fn test_merge_with_base_allows_more_restrictive() {
let mut base = FacetSet::new();
base.set_min_length(5, FacetFixed::Default, None);
base.set_max_length(100, FacetFixed::Default, None);
let mut derived = FacetSet::new();
derived.set_min_length(10, FacetFixed::Default, None); derived.set_max_length(50, FacetFixed::Default, None);
let merged = derived.merge_with_base(&base).unwrap();
assert_eq!(merged.min_length.as_ref().unwrap().value, 10);
assert_eq!(merged.max_length.as_ref().unwrap().value, 50);
}
#[test]
fn test_merge_with_base_rejects_less_restrictive_min_length() {
let mut base = FacetSet::new();
base.set_min_length(10, FacetFixed::Default, None);
let mut derived = FacetSet::new();
derived.set_min_length(5, FacetFixed::Default, None);
let result = derived.merge_with_base(&base);
assert!(result.is_err());
}
#[test]
fn test_merge_with_base_rejects_less_restrictive_max_length() {
let mut base = FacetSet::new();
base.set_max_length(50, FacetFixed::Default, None);
let mut derived = FacetSet::new();
derived.set_max_length(100, FacetFixed::Default, None);
let result = derived.merge_with_base(&base);
assert!(result.is_err());
}
#[test]
fn test_merge_with_base_fixed_facet_same_value_ok() {
let mut base = FacetSet::new();
base.set_length(10, FacetFixed::Fixed, None);
let mut derived = FacetSet::new();
derived.set_length(10, FacetFixed::Default, None);
let result = derived.merge_with_base(&base);
assert!(result.is_ok());
}
#[test]
fn test_merge_with_base_fixed_facet_different_value_error() {
let mut base = FacetSet::new();
base.set_length(10, FacetFixed::Fixed, None);
let mut derived = FacetSet::new();
derived.set_length(20, FacetFixed::Default, None);
let result = derived.merge_with_base(&base);
assert!(result.is_err());
if let Err(FacetError::FixedFacetViolation { facet_name, .. }) = result {
assert_eq!(facet_name, "length");
} else {
panic!("Expected FixedFacetViolation error");
}
}
#[test]
fn test_merge_with_base_patterns_cumulative() {
let mut base = FacetSet::new();
base.add_pattern("[a-z]+".to_string(), None, XsdVersion::V1_1, RegexCompat::Strict)
.unwrap();
let mut derived = FacetSet::new();
derived
.add_pattern("[0-9]+".to_string(), None, XsdVersion::V1_1, RegexCompat::Strict)
.unwrap();
let merged = derived.merge_with_base(&base).unwrap();
assert_eq!(merged.patterns.len(), 2);
}
#[test]
fn test_merge_with_base_enumeration_subset() {
let mut base = FacetSet::new();
base.add_enumeration("red".to_string(), None);
base.add_enumeration("green".to_string(), None);
base.add_enumeration("blue".to_string(), None);
let mut derived = FacetSet::new();
derived.add_enumeration("red".to_string(), None);
derived.add_enumeration("blue".to_string(), None);
let merged = derived.merge_with_base(&base);
assert!(merged.is_ok());
}
#[test]
fn test_merge_with_base_enumeration_not_subset_error() {
let mut base = FacetSet::new();
base.add_enumeration("red".to_string(), None);
base.add_enumeration("green".to_string(), None);
let mut derived = FacetSet::new();
derived.add_enumeration("yellow".to_string(), None);
let result = derived.merge_with_base(&base);
assert!(result.is_err());
}
#[test]
fn test_merge_with_base_whitespace_more_restrictive() {
let mut base = FacetSet::new();
base.set_whitespace(WhitespaceMode::Preserve, FacetFixed::Default, None);
let mut derived = FacetSet::new();
derived.set_whitespace(WhitespaceMode::Collapse, FacetFixed::Default, None);
let result = derived.merge_with_base(&base);
assert!(result.is_ok());
}
#[test]
fn test_merge_with_base_whitespace_less_restrictive_error() {
let mut base = FacetSet::new();
base.set_whitespace(WhitespaceMode::Collapse, FacetFixed::Default, None);
let mut derived = FacetSet::new();
derived.set_whitespace(WhitespaceMode::Preserve, FacetFixed::Default, None);
let result = derived.merge_with_base(&base);
assert!(result.is_err());
}
#[test]
fn test_merge_with_base_digit_facets() {
let mut base = FacetSet::new();
base.set_total_digits(10, FacetFixed::Default, None);
base.set_fraction_digits(5, FacetFixed::Default, None);
let mut derived = FacetSet::new();
derived.set_total_digits(5, FacetFixed::Default, None); derived.set_fraction_digits(2, FacetFixed::Default, None);
let result = derived.merge_with_base(&base);
assert!(result.is_ok());
}
#[test]
fn test_merge_with_base_digit_facets_less_restrictive_error() {
let mut base = FacetSet::new();
base.set_total_digits(5, FacetFixed::Default, None);
let mut derived = FacetSet::new();
derived.set_total_digits(10, FacetFixed::Default, None);
let result = derived.merge_with_base(&base);
assert!(result.is_err());
}
#[test]
fn test_consistency_min_greater_than_max_length() {
let mut base = FacetSet::new();
base.set_min_length(10, FacetFixed::Default, None);
base.set_max_length(5, FacetFixed::Default, None);
let result = base.merge_with_base(&FacetSet::new());
assert!(result.is_err());
}
#[test]
fn test_consistency_both_inclusive_and_exclusive() {
let mut base = FacetSet::new();
base.set_min_inclusive("0".to_string(), FacetFixed::Default, None);
base.set_min_exclusive("0".to_string(), FacetFixed::Default, None);
let result = base.merge_with_base(&FacetSet::new());
assert!(result.is_err());
}
#[test]
fn test_consistency_fraction_greater_than_total() {
let mut base = FacetSet::new();
base.set_total_digits(3, FacetFixed::Default, None);
base.set_fraction_digits(5, FacetFixed::Default, None);
let result = base.merge_with_base(&FacetSet::new());
assert!(result.is_err());
}
#[test]
fn test_facet_kind_from_name() {
assert_eq!(FacetKind::from_name("length"), Some(FacetKind::Length));
assert_eq!(
FacetKind::from_name("minLength"),
Some(FacetKind::MinLength)
);
assert_eq!(FacetKind::from_name("pattern"), Some(FacetKind::Pattern));
assert_eq!(FacetKind::from_name("unknown"), None);
}
#[test]
fn test_facet_kind_name_roundtrip() {
let kinds = [
FacetKind::Length,
FacetKind::MinLength,
FacetKind::MaxLength,
FacetKind::Pattern,
FacetKind::Enumeration,
FacetKind::Whitespace,
FacetKind::MinInclusive,
FacetKind::MaxInclusive,
FacetKind::MinExclusive,
FacetKind::MaxExclusive,
FacetKind::TotalDigits,
FacetKind::FractionDigits,
FacetKind::ExplicitTimezone,
FacetKind::Assertion,
];
for kind in kinds {
let name = kind.name();
assert_eq!(FacetKind::from_name(name), Some(kind));
}
}
#[cfg(not(feature = "xsd11"))]
#[test]
fn test_xsd_pattern_anchoring() {
let rust = convert_xml_pattern("abc", ConvertOptions::xsd());
assert!(rust.starts_with('^'));
assert!(rust.ends_with('$'));
}
#[cfg(not(feature = "xsd11"))]
#[test]
fn test_xsd_pattern_initial_name_char() {
let rust = convert_xml_pattern(r"\i", ConvertOptions::xsd());
assert!(rust.contains("[A-Za-z_:]"));
}
#[cfg(not(feature = "xsd11"))]
#[test]
fn test_xsd_pattern_name_char() {
let rust = convert_xml_pattern(r"\c", ConvertOptions::xsd());
assert!(rust.contains(r"[A-Za-z0-9._:\-]"));
}
#[cfg(not(feature = "xsd11"))]
#[test]
fn test_xsd_pattern_standard_escapes() {
let rust = convert_xml_pattern(r"\d+\s*", ConvertOptions::xsd());
assert!(rust.contains(r"\d"));
assert!(rust.contains(r"\s"));
}
}