use std::collections::BTreeMap;
use crate::core::{Document, ErrorKind, NodeId, NodeKind, QName, XmlError, XmlResult};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum IdAttributePolicy {
#[default]
Standard,
Custom(Vec<QName>),
}
pub fn find_element_by_id(
document: &Document,
id: &str,
policy: &IdAttributePolicy,
) -> XmlResult<NodeId> {
let index = collect_ids(document, policy)?;
match index.get(id).copied() {
Some(node) => Ok(node),
None => Err(XmlError::new(
ErrorKind::Signature,
format!("no element found with XML ID `{id}`"),
)),
}
}
pub fn ensure_unique_ids(document: &Document, policy: &IdAttributePolicy) -> XmlResult<()> {
collect_ids(document, policy).map(|_| ())
}
fn collect_ids(
document: &Document,
policy: &IdAttributePolicy,
) -> XmlResult<BTreeMap<String, NodeId>> {
let mut ids = BTreeMap::new();
if let Some(root) = document.root() {
collect_ids_from_node(document, root, policy, &mut ids)?;
}
Ok(ids)
}
fn collect_ids_from_node(
document: &Document,
node: NodeId,
policy: &IdAttributePolicy,
ids: &mut BTreeMap<String, NodeId>,
) -> XmlResult<()> {
let NodeKind::Element(element) = document.node(node)?.kind() else {
return Ok(());
};
for attribute in element.attributes() {
if !policy.matches(attribute.name()) {
continue;
}
if let Some(previous) = ids.insert(attribute.value().to_owned(), node) {
return Err(XmlError::new(
ErrorKind::Signature,
format!(
"duplicate XML ID `{}` found at nodes {} and {}",
attribute.value(),
previous.index(),
node.index()
),
));
}
}
for child in element.children() {
collect_ids_from_node(document, *child, policy, ids)?;
}
Ok(())
}
impl IdAttributePolicy {
fn matches(&self, name: &QName) -> bool {
match self {
Self::Standard => is_standard_id_name(name),
Self::Custom(names) => names.iter().any(|candidate| candidate == name),
}
}
}
pub(crate) fn is_id_attribute_name(policy: &IdAttributePolicy, name: &QName) -> bool {
policy.matches(name)
}
fn is_standard_id_name(name: &QName) -> bool {
let is_unqualified = name.prefix().is_none() && name.namespace_uri().is_none();
if is_unqualified && matches!(name.local(), "Id" | "ID") {
return true;
}
name.prefix().map(|prefix| prefix.as_str()) == Some("xml")
&& name.namespace_uri().map(|uri| uri.as_str()) == Some(crate::core::XML_NAMESPACE_URI)
&& name.local() == "id"
}
#[cfg(test)]
mod tests {
use crate::core::{Attribute, XML_NAMESPACE_URI};
use super::*;
#[test]
fn signature_ids_find_standard_id_attributes() -> XmlResult<()> {
let mut document = Document::new();
let root = document.add_root_element(QName::new("Root")?)?;
let child = document.add_element(root, QName::new("Child")?)?;
let xml_id = document.add_element(root, QName::new("XmlId")?)?;
document.add_attribute(root, Attribute::new(QName::new("Id")?, "root-id"))?;
document.add_attribute(child, Attribute::new(QName::new("ID")?, "child-id"))?;
document.add_attribute(
xml_id,
Attribute::new(QName::qualified("xml", "id", XML_NAMESPACE_URI)?, "xml-id"),
)?;
assert_eq!(
find_element_by_id(&document, "root-id", &IdAttributePolicy::Standard)?,
root
);
assert_eq!(
find_element_by_id(&document, "child-id", &IdAttributePolicy::Standard)?,
child
);
assert_eq!(
find_element_by_id(&document, "xml-id", &IdAttributePolicy::Standard)?,
xml_id
);
Ok(())
}
#[test]
fn signature_ids_support_custom_policy() -> XmlResult<()> {
let mut document = Document::new();
let root = document.add_root_element(QName::new("Root")?)?;
document.add_attribute(root, Attribute::new(QName::new("code")?, "A1"))?;
assert_eq!(
find_element_by_id(
&document,
"A1",
&IdAttributePolicy::Custom(vec![QName::new("code")?])
)?,
root
);
assert!(find_element_by_id(&document, "A1", &IdAttributePolicy::Standard).is_err());
Ok(())
}
#[test]
fn signature_ids_reject_duplicates() -> XmlResult<()> {
let mut document = Document::new();
let root = document.add_root_element(QName::new("Root")?)?;
let first = document.add_element(root, QName::new("First")?)?;
let second = document.add_element(root, QName::new("Second")?)?;
document.add_attribute(first, Attribute::new(QName::new("Id")?, "same"))?;
document.add_attribute(second, Attribute::new(QName::new("ID")?, "same"))?;
let error = ensure_unique_ids(&document, &IdAttributePolicy::Standard)
.expect_err("duplicate IDs must fail");
assert_eq!(error.kind(), &ErrorKind::Signature);
assert!(error.message().contains("duplicate XML ID"));
Ok(())
}
}