fn parse_optional_attr<T, F>(
attrs: &AttributeMap,
name_table: &NameTable,
name: &str,
parse: F,
) -> SchemaResult<Option<T>>
where
F: FnOnce(&str) -> Result<T, String>,
{
match attrs.get_value_by_name(name_table, name) {
Some(value) => {
let parsed = parse(value).map_err(|err| {
SchemaError::structural(
"ct-props-correct",
format!("Invalid value for attribute '{}': {}", name, err),
None,
)
})?;
Ok(Some(parsed))
}
None => Ok(None),
}
}
fn validate_attr_value<T, F>(
attrs: &AttributeMap,
name_table: &NameTable,
name: &str,
parse: F,
) -> SchemaResult<()>
where
F: FnOnce(&str) -> Result<T, String>,
{
parse_optional_attr(attrs, name_table, name, parse).map(|_| ())
}
fn parse_optional_bool_attr(
attrs: &AttributeMap,
name_table: &NameTable,
name: &str,
) -> SchemaResult<Option<bool>> {
parse_optional_attr(attrs, name_table, name, parse_boolean)
}
fn parse_bool_attr_default(
attrs: &AttributeMap,
name_table: &NameTable,
name: &str,
default: bool,
) -> SchemaResult<bool> {
Ok(parse_optional_bool_attr(attrs, name_table, name)?.unwrap_or(default))
}
fn parse_occurs_attr_raw(
attrs: &AttributeMap,
name_table: &NameTable,
name: &str,
) -> SchemaResult<Option<Option<u32>>> {
match attrs.get_value_by_name(name_table, name) {
Some(value) => {
let parsed = parse_occurs(value).map_err(|err| {
SchemaError::structural(
"ct-props-correct",
format!("Invalid value for attribute '{}': {}", name, err),
None,
)
})?;
Ok(Some(parsed))
}
None => Ok(None),
}
}
fn parse_min_occurs_attr(
attrs: &AttributeMap,
name_table: &NameTable,
name: &str,
) -> SchemaResult<u32> {
match parse_occurs_attr_raw(attrs, name_table, name)? {
None => Ok(1),
Some(Some(value)) => Ok(value),
Some(None) => Err(SchemaError::structural(
"ct-props-correct",
format!("Invalid value for attribute '{}': 'unbounded'", name),
None,
)),
}
}
fn parse_max_occurs_attr(
attrs: &AttributeMap,
name_table: &NameTable,
name: &str,
) -> SchemaResult<Option<u32>> {
match parse_occurs_attr_raw(attrs, name_table, name)? {
None => Ok(Some(1)),
Some(Some(value)) => Ok(Some(value)),
Some(None) => Ok(None),
}
}
fn parse_process_contents_value(value: &str) -> Result<ProcessContents, String> {
match value {
"strict" => Ok(ProcessContents::Strict),
"lax" => Ok(ProcessContents::Lax),
"skip" => Ok(ProcessContents::Skip),
_ => Err(format!("Invalid processContents value: '{}'", value)),
}
}
#[cfg(feature = "xsd11")]
fn parse_open_content_mode(value: &str) -> Result<OpenContentMode, String> {
match value {
"none" => Ok(OpenContentMode::None),
"interleave" => Ok(OpenContentMode::Interleave),
"suffix" => Ok(OpenContentMode::Suffix),
_ => Err(format!("Invalid open content mode: '{}'", value)),
}
}
fn parse_process_contents_attr(
attrs: &AttributeMap,
name_table: &NameTable,
name: &str,
) -> SchemaResult<ProcessContents> {
match parse_optional_attr(attrs, name_table, name, parse_process_contents_value)? {
Some(value) => Ok(value),
None => Ok(ProcessContents::Strict),
}
}
#[cfg(feature = "xsd11")]
fn parse_open_content_mode_attr(
attrs: &AttributeMap,
name_table: &NameTable,
name: &str,
) -> SchemaResult<OpenContentMode> {
match parse_optional_attr(attrs, name_table, name, parse_open_content_mode)? {
Some(value) => Ok(value),
None => Ok(OpenContentMode::Interleave),
}
}
fn parse_derivation_set_opt(value: Option<&str>) -> SchemaResult<Option<DerivationSet>> {
let Some(value) = value else {
return Ok(None);
};
if value == "#all" {
return Ok(Some(DerivationSet::ALL));
}
let mut set = DerivationSet::empty();
for token in value.split_whitespace() {
match token {
"extension" => set |= DerivationSet::EXTENSION,
"restriction" => set |= DerivationSet::RESTRICTION,
"list" => set |= DerivationSet::LIST,
"union" => set |= DerivationSet::UNION,
"substitution" => set |= DerivationSet::SUBSTITUTION,
_ => {
return Err(SchemaError::structural(
"sch-props-correct",
format!("Invalid derivation method: '{}'", token),
None,
));
}
}
}
Ok(Some(set))
}
fn parse_derivation_set(value: Option<&str>) -> SchemaResult<DerivationSet> {
Ok(parse_derivation_set_opt(value)?.unwrap_or_default())
}
fn parse_qname_ref(
value: &str,
name_table: &NameTable,
ns_snapshot: &NamespaceContextSnapshot,
) -> SchemaResult<QNameRef> {
let value = value.trim();
let (local, prefix) = if let Some(pos) = value.find(':') {
let prefix = &value[..pos];
let local = &value[pos + 1..];
(local, Some(prefix))
} else {
(value, None)
};
if local.is_empty() || local.contains(':') {
return Err(SchemaError::structural(
"src-resolve",
format!("Invalid QName: '{}'", value),
None,
));
}
if let Some(p) = prefix {
if p.is_empty() {
return Err(SchemaError::structural(
"src-resolve",
format!("Invalid QName: '{}'", value),
None,
));
}
}
let local_name = name_table.add(local);
let prefix_id = prefix.and_then(|p| name_table.get(p));
let namespace = if let Some(pid) = prefix_id {
ns_snapshot.resolve_prefix(pid)
} else {
ns_snapshot.default_namespace()
};
Ok(QNameRef {
prefix: prefix_id,
local_name,
namespace,
})
}
fn parse_qname_list(
value: &str,
name_table: &NameTable,
ns_snapshot: &NamespaceContextSnapshot,
) -> SchemaResult<Vec<QNameRef>> {
value
.split_whitespace()
.map(|s| parse_qname_ref(s, name_table, ns_snapshot))
.collect()
}
fn parse_namespace_constraint(
value: Option<&str>,
name_table: &NameTable,
) -> SchemaResult<WildcardNamespace> {
let Some(value) = value else {
return Ok(WildcardNamespace::Any);
};
match value {
"##any" => Ok(WildcardNamespace::Any),
"##other" => Ok(WildcardNamespace::Other),
"##targetNamespace" => Ok(WildcardNamespace::TargetNamespace),
"##local" => Ok(WildcardNamespace::Local),
_ => {
let mut namespaces = Vec::new();
for s in value.split_whitespace() {
match s {
"##targetNamespace" => namespaces.push(NamespaceToken::TargetNamespace),
"##local" => namespaces.push(NamespaceToken::Local),
"##any" | "##other" => {
return Err(SchemaError::structural(
"src-wildcard",
format!("'{}' must appear alone, not in a list namespace constraint", s),
None,
))
}
s if s.starts_with("##") => {
return Err(SchemaError::structural(
"src-wildcard",
format!("Unrecognized namespace token '{}'; expected ##any, ##other, ##targetNamespace, ##local, or a URI", s),
None,
))
}
_ => namespaces.push(NamespaceToken::Uri(name_table.add(s))),
}
}
Ok(WildcardNamespace::List(namespaces))
}
}
}
#[cfg(feature = "xsd11")]
pub(crate) fn parse_not_namespace(
value: Option<&str>,
name_table: &NameTable,
) -> Vec<NamespaceToken> {
let Some(value) = value else {
return Vec::new();
};
value
.split_whitespace()
.map(|s| match s {
"##targetNamespace" => NamespaceToken::TargetNamespace,
"##local" => NamespaceToken::Local,
_ => NamespaceToken::Uri(name_table.add(s)),
})
.collect()
}
#[cfg(feature = "xsd11")]
pub(crate) fn parse_not_qname(
value: Option<&str>,
name_table: &NameTable,
ns_snapshot: &NamespaceContextSnapshot,
is_element_wildcard: bool,
) -> SchemaResult<Vec<NotQNameItem>> {
let Some(value) = value else {
return Ok(Vec::new());
};
let mut items = Vec::new();
for s in value.split_whitespace() {
match s {
"##defined" => items.push(NotQNameItem::Defined),
"##definedSibling" => {
if !is_element_wildcard {
return Err(SchemaError::structural(
"w-props-correct",
"##definedSibling is not allowed on xs:anyAttribute".to_string(),
None,
));
}
items.push(NotQNameItem::DefinedSibling);
}
_ => {
let (prefix, local) = match s.split_once(':') {
Some((p, l)) => (Some(p), l),
None => (None, s),
};
if local.is_empty() || local.contains(':') {
return Err(SchemaError::structural(
"w-props-correct",
format!("Invalid QName '{}' in notQName", s),
None,
));
}
if let Some(p) = prefix {
if p.is_empty() {
return Err(SchemaError::structural(
"w-props-correct",
format!("Invalid QName '{}' in notQName", s),
None,
));
}
}
let namespace = if let Some(p) = prefix {
let prefix_id = match name_table.get(p) {
Some(id) => id,
None => {
return Err(SchemaError::structural(
"w-props-correct",
format!(
"Undeclared prefix '{}' in notQName entry '{}'",
p, s
),
None,
));
}
};
match ns_snapshot.resolve_prefix(prefix_id) {
Some(ns) => Some(ns),
None => {
return Err(SchemaError::structural(
"w-props-correct",
format!(
"Undeclared prefix '{}' in notQName entry '{}'",
p, s
),
None,
));
}
}
} else {
ns_snapshot.default_namespace()
};
let local_name = name_table.add(local);
items.push(NotQNameItem::QName { namespace, local_name });
}
}
}
Ok(items)
}
fn parse_nonneg_integer<T: std::str::FromStr>(value: &str, facet_name: &str) -> SchemaResult<T> {
let trimmed = value.trim();
let text = trimmed.strip_prefix('+').unwrap_or(trimmed);
text.parse().map_err(|_| {
SchemaError::structural(
"st-props-correct",
format!("Invalid {} value '{}': must be a nonNegativeInteger", facet_name, value),
None,
)
})
}
fn apply_facet(facets: &mut FacetSet, facet: FacetResult) -> SchemaResult<()> {
use crate::types::facets::{FacetFixed, WhitespaceMode};
let fixed = if facet.fixed {
FacetFixed::Fixed
} else {
FacetFixed::Default
};
let dup = |name: &str| {
SchemaError::structural(
"st-props-correct",
format!("Facet '{}' must not appear more than once in a restriction", name),
None,
)
};
match facet.kind {
FacetKind::Enumeration => {
facets.add_enumeration(facet.value, facet.source);
}
FacetKind::Pattern => {
facets.add_pattern_unchecked(facet.value, facet.source);
}
FacetKind::MinLength => {
if facets.min_length.is_some() { return Err(dup("minLength")); }
facets.set_min_length(parse_nonneg_integer(&facet.value, "minLength")?, fixed, facet.source);
}
FacetKind::MaxLength => {
if facets.max_length.is_some() { return Err(dup("maxLength")); }
facets.set_max_length(parse_nonneg_integer(&facet.value, "maxLength")?, fixed, facet.source);
}
FacetKind::Length => {
if facets.length.is_some() { return Err(dup("length")); }
facets.set_length(parse_nonneg_integer(&facet.value, "length")?, fixed, facet.source);
}
FacetKind::MinInclusive => {
if facets.min_inclusive.is_some() { return Err(dup("minInclusive")); }
facets.set_min_inclusive(facet.value, fixed, facet.source);
}
FacetKind::MaxInclusive => {
if facets.max_inclusive.is_some() { return Err(dup("maxInclusive")); }
facets.set_max_inclusive(facet.value, fixed, facet.source);
}
FacetKind::MinExclusive => {
if facets.min_exclusive.is_some() { return Err(dup("minExclusive")); }
facets.set_min_exclusive(facet.value, fixed, facet.source);
}
FacetKind::MaxExclusive => {
if facets.max_exclusive.is_some() { return Err(dup("maxExclusive")); }
facets.set_max_exclusive(facet.value, fixed, facet.source);
}
FacetKind::TotalDigits => {
if facets.total_digits.is_some() { return Err(dup("totalDigits")); }
let v: u32 = parse_nonneg_integer(&facet.value, "totalDigits")?;
if v == 0 {
return Err(SchemaError::structural(
"st-props-correct",
"Invalid totalDigits value '0': must be a positiveInteger (> 0)",
None,
));
}
facets.set_total_digits(v, fixed, facet.source);
}
FacetKind::FractionDigits => {
if facets.fraction_digits.is_some() { return Err(dup("fractionDigits")); }
facets.set_fraction_digits(parse_nonneg_integer(&facet.value, "fractionDigits")?, fixed, facet.source);
}
FacetKind::WhiteSpace => {
if facets.whitespace.is_some() { return Err(dup("whiteSpace")); }
let mode = match facet.value.as_str() {
"preserve" => WhitespaceMode::Preserve,
"replace" => WhitespaceMode::Replace,
"collapse" => WhitespaceMode::Collapse,
_ => WhitespaceMode::Collapse,
};
facets.set_whitespace(mode, fixed, facet.source);
}
FacetKind::Assertion => {
facets.add_assertion(
facet.value,
facet.xpath_default_namespace,
facet.ns_snapshot.unwrap_or_default(),
facet.source,
);
}
FacetKind::ExplicitTimezone => {
if facets.explicit_timezone.is_some() { return Err(dup("explicitTimezone")); }
let mode = match facet.value.as_str() {
"required" => ExplicitTimezone::Required,
"prohibited" => ExplicitTimezone::Prohibited,
"optional" => ExplicitTimezone::Optional,
other => {
return Err(SchemaError::structural(
"st-props-correct",
format!(
"Invalid explicitTimezone value '{}': expected 'required', 'prohibited', or 'optional'",
other
),
None,
));
}
};
facets.set_explicit_timezone(mode, fixed, facet.source);
}
}
Ok(())
}