use std::collections::{HashMap, HashSet};
use crate::document::XmlDocument;
use crate::node::{NodeType, XmlNode};
use crate::schema::types::{CompiledConstraint, CompiledConstraintType, CompiledSchema};
use crate::xpath::{Query, XPathResult};
pub(crate) struct ConstraintTask {
pub node: XmlNode,
pub constraint: CompiledConstraint,
}
pub(crate) fn validate_identity_constraints(
schema: &CompiledSchema,
doc: &XmlDocument,
tasks: &[ConstraintTask],
node_kinds: &HashMap<crate::node::NodeId, crate::schema::xsd::primitive::PrimitiveKind>,
attr_kinds: &HashMap<
(crate::node::NodeId, String),
crate::schema::xsd::primitive::PrimitiveKind,
>,
) -> Vec<String> {
let mut errors = Vec::new();
let mut tables: HashMap<String, HashSet<Vec<String>>> = HashMap::new();
for task in tasks {
if task.constraint.constraint_type == CompiledConstraintType::KeyRef {
continue;
}
let is_key = task.constraint.constraint_type == CompiledConstraintType::Key;
let mut seen: HashSet<Vec<String>> = HashSet::new();
for selected in select_nodes(schema, doc, &task.node, &task.constraint.selector_xpath) {
let outcomes: Vec<FieldOutcome> = task
.constraint
.field_xpaths
.iter()
.map(|f| field_value(schema, doc, &selected, f, node_kinds, attr_kinds))
.collect();
if outcomes.iter().any(|v| matches!(v, FieldOutcome::Multiple)) {
errors.push(format!(
"{} '{}': a field matches more than one node",
if is_key { "key" } else { "unique" },
task.constraint.name
));
continue;
}
if outcomes.iter().any(|v| matches!(v, FieldOutcome::Absent)) {
if is_key {
errors.push(format!(
"key '{}': a field has no value for a selected node",
task.constraint.name
));
}
continue; }
let tuple: Vec<String> = outcomes
.into_iter()
.map(|v| match v {
FieldOutcome::Value(s) => s,
_ => unreachable!(),
})
.collect();
if !seen.insert(tuple.clone()) {
errors.push(format!(
"{} '{}': duplicate tuple [{}]",
if is_key { "key" } else { "unique" },
task.constraint.name,
tuple.join(", ")
));
}
}
let local = task
.constraint
.name
.rsplit(':')
.next()
.unwrap_or(&task.constraint.name);
tables.entry(local.to_string()).or_default().extend(seen);
}
for task in tasks {
if task.constraint.constraint_type != CompiledConstraintType::KeyRef {
continue;
}
let Some(ref refer) = task.constraint.refer else {
continue;
};
let refer_local = refer.rsplit(':').next().unwrap_or(refer);
let Some(table) = tables.get(refer_local) else {
continue;
};
for selected in select_nodes(schema, doc, &task.node, &task.constraint.selector_xpath) {
let outcomes: Vec<FieldOutcome> = task
.constraint
.field_xpaths
.iter()
.map(|f| field_value(schema, doc, &selected, f, node_kinds, attr_kinds))
.collect();
if !outcomes.iter().all(|v| matches!(v, FieldOutcome::Value(_))) {
continue; }
let tuple: Vec<String> = outcomes
.into_iter()
.map(|v| match v {
FieldOutcome::Value(s) => s,
_ => unreachable!(),
})
.collect();
if !table.contains(&tuple) {
errors.push(format!(
"keyref '{}': tuple [{}] does not match any '{}' key",
task.constraint.name,
tuple.join(", "),
refer_local
));
}
}
}
errors
}
fn compile_with_schema_ns(schema: &CompiledSchema, xpath: &str) -> Option<Query> {
let mut query = Query::compile(xpath).ok()?;
for (prefix, uri) in &schema.prefix_namespaces {
if !prefix.is_empty() {
query = query.namespace(prefix.clone(), uri.clone());
}
}
Some(query)
}
fn select_nodes(
schema: &CompiledSchema,
doc: &XmlDocument,
context: &XmlNode,
xpath: &str,
) -> Vec<XmlNode> {
let Some(query) = compile_with_schema_ns(schema, xpath) else {
return Vec::new();
};
match query.eval_from(doc, context) {
Ok(XPathResult::Nodes(nodes)) => nodes,
_ => Vec::new(),
}
}
enum FieldOutcome {
Value(String),
Absent,
Multiple,
}
fn direct_attr_local(xpath: &str) -> Option<&str> {
let x = xpath.trim();
let x = x.strip_prefix("./").unwrap_or(x);
let a = x.strip_prefix('@')?;
if a.contains('/') {
return None;
}
Some(a.rsplit(':').next().unwrap_or(a))
}
fn field_value(
schema: &CompiledSchema,
doc: &XmlDocument,
context: &XmlNode,
xpath: &str,
node_kinds: &HashMap<crate::node::NodeId, crate::schema::xsd::primitive::PrimitiveKind>,
attr_kinds: &HashMap<
(crate::node::NodeId, String),
crate::schema::xsd::primitive::PrimitiveKind,
>,
) -> FieldOutcome {
let Some(query) = compile_with_schema_ns(schema, xpath) else {
return FieldOutcome::Absent;
};
match query.eval_from(doc, context) {
Ok(XPathResult::Nodes(nodes)) => match nodes.as_slice() {
[] => FieldOutcome::Absent,
[node] => {
let kind = match direct_attr_local(xpath) {
Some(local) => attr_kinds.get(&(context.id(), local.to_string())).copied(),
None => node_kinds.get(&node.id()).copied(),
};
FieldOutcome::Value(crate::schema::xsd::value_compare::canonical_value(
kind,
string_value(node).trim(),
))
}
_ => FieldOutcome::Multiple,
},
Ok(XPathResult::String(s)) => FieldOutcome::Value(s.trim().to_string()),
Ok(XPathResult::Number(n)) => FieldOutcome::Value(n.to_string()),
Ok(XPathResult::Boolean(b)) => FieldOutcome::Value(b.to_string()),
Err(_) => FieldOutcome::Absent,
}
}
fn string_value(node: &XmlNode) -> String {
match node.get_type() {
NodeType::Element => {
let mut out = String::new();
collect_text(node, &mut out);
out
}
_ => node.get_content().unwrap_or_default(),
}
}
fn collect_text(node: &XmlNode, out: &mut String) {
for child in node.get_child_nodes() {
match child.get_type() {
NodeType::Text | NodeType::CData => {
if let Some(content) = child.get_content() {
out.push_str(&content);
}
}
NodeType::Element => collect_text(&child, out),
_ => {}
}
}
}