use oxml::{Document, NodeId};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Unsupported {
pub construct: String,
pub effect: String,
}
const HANDLED_ELEMENTS: &[&str] = &[
"schema",
"list",
"union",
"element",
"complexType",
"simpleType",
"sequence",
"choice",
"restriction",
"attribute",
"simpleContent",
"extension",
"annotation",
"documentation",
"appinfo",
"all",
"group",
"attributeGroup",
"complexContent",
"unique",
"key",
"keyref",
"selector",
"field",
"import",
"include",
"any",
"anyAttribute",
"enumeration",
"pattern",
"minLength",
"maxLength",
"length",
"minInclusive",
"maxInclusive",
"minExclusive",
"maxExclusive",
"totalDigits",
"fractionDigits",
"whiteSpace",
];
#[must_use]
pub fn unsupported(doc: &Document) -> Vec<Unsupported> {
let mut out = Vec::new();
for id in doc.descendants() {
if !doc.is_element(id) {
continue;
}
let Some(name) = doc.element_name(id).map(|n| n.local.clone()) else {
continue;
};
check_element(doc, id, &name, &mut out);
}
out.sort_by(|a, b| a.construct.cmp(&b.construct));
out.dedup();
out
}
fn check_element(
doc: &Document,
id: NodeId,
name: &str,
out: &mut Vec<Unsupported>,
) {
if !HANDLED_ELEMENTS.contains(&name) {
out.push(Unsupported {
construct: format!("xs:{name}"),
effect: "the element is ignored entirely".to_owned(),
});
return;
}
if matches!(name, "sequence" | "choice" | "all") {
let repeats = doc.attribute(id, "maxOccurs").is_some_and(|v| v != "1")
|| doc.attribute(id, "minOccurs").is_some_and(|v| v != "1");
let particles = doc
.children(id)
.iter()
.filter(|&&c| {
doc.element_name(c).is_some_and(|n| n.local != "annotation")
})
.count();
if repeats && particles > 1 {
out.push(Unsupported {
construct: format!("repeated xs:{name}"),
effect: "a model group repeated as a whole is not \
modelled, so the order across repetitions is \
not enforced"
.to_owned(),
});
}
}
if name == "pattern" {
if let Some(value) = doc.attribute(id, "value") {
if let Err(why) = crate::pattern::Pattern::compile(value) {
out.push(Unsupported {
construct: format!("xs:pattern {value:?}"),
effect: format!("the pattern does not compile: {why}"),
});
}
}
}
if let Some(type_name) = doc.attribute(id, "type") {
check_type_reference(doc, type_name, out);
}
if name == "restriction" || name == "extension" {
if let Some(base) = doc.attribute(id, "base") {
check_type_reference(doc, base, out);
}
}
for (attribute, effect) in [
("abstract", "the declaration is used as if it were concrete"),
("substitutionGroup", "substitution is not applied"),
("default", "the default value is not supplied"),
("form", "qualification is not applied"),
("block", "the blocking constraint is not applied"),
("final", "the derivation constraint is not applied"),
(
"mixed",
"mixed content is not distinguished from element-only",
),
] {
if doc.attribute(id, attribute).is_some() {
out.push(Unsupported {
construct: format!("@{attribute} on xs:{name}"),
effect: (*effect).to_owned(),
});
}
}
}
fn check_type_reference(
doc: &Document,
type_name: &str,
out: &mut Vec<Unsupported>,
) {
let local = type_name.rsplit(':').next().unwrap_or(type_name);
if oxml_builtin(local) || names_a_local_type(doc, local) {
return;
}
out.push(Unsupported {
construct: format!("type reference {type_name:?}"),
effect: "resolves to nothing, so the element accepts any content"
.to_owned(),
});
}
fn oxml_builtin(local: &str) -> bool {
crate::datatype::Datatype::from_name(local).is_some()
}
fn names_a_local_type(doc: &Document, local: &str) -> bool {
doc.descendants().any(|id| {
doc.element_name(id).is_some_and(|n| {
(n.local == "simpleType" || n.local == "complexType")
&& doc.attribute(id, "name") == Some(local)
})
})
}