use crate::error::Result;
use crate::schema::error::SchemaError;
use crate::schema::xsd::primitive::PrimitiveKind;
use crate::schema::xsd::types::XsdFacet;
use crate::schema::xsd::value_compare::compare_values;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum FacetBase {
Primitive(PrimitiveKind),
StringFamily,
Unknown,
}
pub(crate) fn classify_builtin_base(local: &str) -> FacetBase {
if let Some(kind) = PrimitiveKind::from_type_name(local) {
return FacetBase::Primitive(kind);
}
match local {
"string" | "normalizedString" | "token" => FacetBase::StringFamily,
_ => FacetBase::Unknown,
}
}
fn length_applicable(kind: PrimitiveKind) -> bool {
use PrimitiveKind::*;
matches!(
kind,
HexBinary
| Base64Binary
| AnyUri
| QName
| Name
| Ncname
| Nmtoken
| Language
| Id
| Idref
| Entity
)
}
fn ordered(kind: PrimitiveKind) -> bool {
use PrimitiveKind::*;
!matches!(
kind,
Boolean
| HexBinary
| Base64Binary
| AnyUri
| QName
| Name
| Ncname
| Nmtoken
| Language
| Id
| Idref
| Entity
)
}
fn digits_applicable(kind: PrimitiveKind) -> bool {
use PrimitiveKind::*;
matches!(
kind,
Decimal
| Integer
| Long
| Int
| Short
| Byte
| NonNegativeInteger
| PositiveInteger
| NonPositiveInteger
| NegativeInteger
| UnsignedLong
| UnsignedInt
| UnsignedShort
| UnsignedByte
)
}
fn err(message: String) -> crate::error::Error {
SchemaError::InvalidSchema { message }.into()
}
pub(crate) fn check_facets(facets: &[XsdFacet], base: FacetBase) -> Result<()> {
let mut length = false;
let mut min_length = false;
let mut max_length = false;
let mut white_space = false;
let mut total_digits = false;
let mut fraction_digits = false;
let mut min_inclusive: Option<&str> = None;
let mut min_exclusive: Option<&str> = None;
let mut max_inclusive: Option<&str> = None;
let mut max_exclusive: Option<&str> = None;
let dup = |seen: &mut bool, name: &str| -> Result<()> {
if *seen {
return Err(err(format!(
"facet '{name}' may only appear once in a restriction"
)));
}
*seen = true;
Ok(())
};
for facet in facets {
match facet {
XsdFacet::Length(_) => dup(&mut length, "length")?,
XsdFacet::MinLength(_) => dup(&mut min_length, "minLength")?,
XsdFacet::MaxLength(_) => dup(&mut max_length, "maxLength")?,
XsdFacet::WhiteSpace(_) => dup(&mut white_space, "whiteSpace")?,
XsdFacet::TotalDigits(n) => {
dup(&mut total_digits, "totalDigits")?;
if *n == 0 {
return Err(err(
"totalDigits must be a positive integer, found 0".to_string()
));
}
}
XsdFacet::FractionDigits(_) => dup(&mut fraction_digits, "fractionDigits")?,
XsdFacet::MinInclusive(v) => {
if min_inclusive.is_some() {
return Err(err("facet 'minInclusive' may only appear once".to_string()));
}
min_inclusive = Some(v);
}
XsdFacet::MinExclusive(v) => {
if min_exclusive.is_some() {
return Err(err("facet 'minExclusive' may only appear once".to_string()));
}
min_exclusive = Some(v);
}
XsdFacet::MaxInclusive(v) => {
if max_inclusive.is_some() {
return Err(err("facet 'maxInclusive' may only appear once".to_string()));
}
max_inclusive = Some(v);
}
XsdFacet::MaxExclusive(v) => {
if max_exclusive.is_some() {
return Err(err("facet 'maxExclusive' may only appear once".to_string()));
}
max_exclusive = Some(v);
}
XsdFacet::Enumeration(_) | XsdFacet::Pattern(_) | XsdFacet::ExplicitTimezone(_) => {}
}
}
if length && (min_length || max_length) {
return Err(err(
"'length' cannot be combined with 'minLength' or 'maxLength'".to_string(),
));
}
if min_inclusive.is_some() && min_exclusive.is_some() {
return Err(err(
"'minInclusive' and 'minExclusive' cannot both be specified".to_string(),
));
}
if max_inclusive.is_some() && max_exclusive.is_some() {
return Err(err(
"'maxInclusive' and 'maxExclusive' cannot both be specified".to_string(),
));
}
let has_length_facet = length || min_length || max_length;
let has_digit_facet = total_digits || fraction_digits;
let range_facets = [
("minInclusive", min_inclusive),
("minExclusive", min_exclusive),
("maxInclusive", max_inclusive),
("maxExclusive", max_exclusive),
];
match base {
FacetBase::Primitive(kind) => {
if has_length_facet && !length_applicable(kind) {
return Err(err(format!(
"length facets are not applicable to a {kind:?} base"
)));
}
if has_digit_facet && !digits_applicable(kind) {
return Err(err(format!(
"digit facets are not applicable to a {kind:?} base"
)));
}
for (name, value) in range_facets {
if let Some(value) = value {
if !ordered(kind) {
return Err(err(format!(
"'{name}' is not applicable to an unordered {kind:?} base"
)));
}
if kind.validate(value.trim()).is_err() {
return Err(SchemaError::InvalidFacetValue {
facet: name.to_string(),
value: value.to_string(),
reason: format!("not a valid {kind:?} literal"),
}
.into());
}
}
}
for facet in facets {
if let XsdFacet::Enumeration(value) = facet
&& kind.validate(value.trim()).is_err()
{
return Err(SchemaError::InvalidFacetValue {
facet: "enumeration".to_string(),
value: value.to_string(),
reason: format!("not a valid {kind:?} literal"),
}
.into());
}
}
let kind = Some(kind);
for (lo_name, lo, hi_name, hi) in [
("minInclusive", min_inclusive, "maxInclusive", max_inclusive),
("minExclusive", min_exclusive, "maxExclusive", max_exclusive),
("minInclusive", min_inclusive, "maxExclusive", max_exclusive),
("minExclusive", min_exclusive, "maxInclusive", max_inclusive),
] {
if let (Some(lo), Some(hi)) = (lo, hi)
&& compare_values(kind, lo.trim(), hi.trim())
== Some(std::cmp::Ordering::Greater)
{
return Err(err(format!(
"'{lo_name}' ({lo}) must not be greater than '{hi_name}' ({hi})"
)));
}
}
}
FacetBase::StringFamily => {
for (name, value) in range_facets {
if value.is_some() {
return Err(err(format!("'{name}' is not applicable to a string base")));
}
}
if has_digit_facet {
return Err(err(
"digit facets are not applicable to a string base".to_string()
));
}
}
FacetBase::Unknown => {}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn int_base() -> FacetBase {
FacetBase::Primitive(PrimitiveKind::Int)
}
#[test]
fn bad_range_literal_rejected() {
let facets = [XsdFacet::MinInclusive("not-a-number".into())];
assert!(check_facets(&facets, int_base()).is_err());
}
#[test]
fn good_range_literal_accepted() {
let facets = [
XsdFacet::MinInclusive("1".into()),
XsdFacet::MaxInclusive("10".into()),
];
assert!(check_facets(&facets, int_base()).is_ok());
}
#[test]
fn min_greater_than_max_rejected() {
let facets = [
XsdFacet::MinInclusive("7".into()),
XsdFacet::MaxInclusive("1".into()),
];
assert!(check_facets(&facets, int_base()).is_err());
}
#[test]
fn inclusive_and_exclusive_bound_rejected() {
let facets = [
XsdFacet::MaxInclusive("5".into()),
XsdFacet::MaxExclusive("5".into()),
];
assert!(check_facets(&facets, int_base()).is_err());
}
#[test]
fn length_with_max_length_rejected() {
let facets = [XsdFacet::Length(5), XsdFacet::MaxLength(10)];
assert!(check_facets(&facets, FacetBase::StringFamily).is_err());
}
#[test]
fn zero_total_digits_rejected() {
let facets = [XsdFacet::TotalDigits(0)];
assert!(check_facets(&facets, FacetBase::Unknown).is_err());
}
#[test]
fn range_on_string_rejected() {
let facets = [XsdFacet::MinInclusive("1".into())];
assert!(check_facets(&facets, FacetBase::StringFamily).is_err());
}
#[test]
fn length_on_int_rejected() {
let facets = [XsdFacet::Length(5)];
assert!(check_facets(&facets, int_base()).is_err());
}
#[test]
fn digits_on_string_rejected() {
let facets = [XsdFacet::FractionDigits(1)];
assert!(check_facets(&facets, FacetBase::StringFamily).is_err());
}
#[test]
fn bad_enumeration_literal_rejected() {
let facets = [XsdFacet::Enumeration("twelve".into())];
assert!(check_facets(&facets, int_base()).is_err());
}
#[test]
fn duplicate_facet_rejected() {
let facets = [
XsdFacet::MaxInclusive("5".into()),
XsdFacet::MaxInclusive("6".into()),
];
assert!(check_facets(&facets, int_base()).is_err());
}
#[test]
fn unknown_base_skips_value_checks() {
let facets = [
XsdFacet::MinInclusive("garbage".into()),
XsdFacet::Length(5),
];
assert!(check_facets(&facets, FacetBase::Unknown).is_ok());
}
}