use std::sync::Arc;
use super::schema::QName;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConstraintKind {
Key,
Unique,
KeyRef,
}
#[derive(Debug, Clone)]
pub struct IdentityConstraint {
pub name: QName,
pub kind: ConstraintKind,
pub selector: SelectorPath,
pub fields: Vec<FieldPath>,
pub refer: Option<QName>,
}
#[derive(Debug, Clone)]
pub struct SelectorPath {
pub paths: Vec<PathExpr>,
}
#[derive(Debug, Clone)]
pub struct FieldPath {
pub paths: Vec<PathExpr>,
pub all_attribute: bool,
}
#[derive(Debug, Clone)]
pub struct PathExpr {
pub descendant: bool,
pub steps: Vec<PathStep>,
}
#[derive(Debug, Clone)]
pub enum PathStep {
Child(NameTest),
Attribute(NameTest),
}
#[derive(Debug, Clone)]
pub enum NameTest {
Any,
Name(QName),
AnyInNs(Arc<str>),
}
pub fn parse_selector(
xpath: &str,
resolve_prefix: &dyn Fn(&str) -> Option<String>,
) -> Result<SelectorPath, String> {
let mut paths = Vec::new();
for part in xpath.split('|') {
paths.push(parse_path(part.trim(), false, resolve_prefix)?);
}
if paths.is_empty() {
return Err("empty selector".to_string());
}
Ok(SelectorPath { paths })
}
pub fn parse_field(
xpath: &str,
resolve_prefix: &dyn Fn(&str) -> Option<String>,
) -> Result<FieldPath, String> {
let mut paths = Vec::new();
for part in xpath.split('|') {
paths.push(parse_path(part.trim(), true, resolve_prefix)?);
}
if paths.is_empty() {
return Err("empty field".to_string());
}
let all_attribute = paths.iter().all(|p|
matches!(p.steps.last(), Some(PathStep::Attribute(_)))
);
Ok(FieldPath { paths, all_attribute })
}
fn parse_path(
raw: &str,
allow_attribute_tail: bool,
resolve_prefix: &dyn Fn(&str) -> Option<String>,
) -> Result<PathExpr, String> {
let trimmed = raw.trim();
let normalized = strip_xpath_whitespace(trimmed);
let raw = normalized.as_str();
let (descendant, body) = if let Some(rest) = raw.strip_prefix(".//") {
(true, rest)
} else if let Some(rest) = raw.strip_prefix("./") {
(false, rest)
} else if raw == "." {
return Ok(PathExpr { descendant: false, steps: Vec::new() });
} else {
(false, raw)
};
let mut steps = Vec::new();
let parts: Vec<&str> = body.split('/').collect();
for (i, part) in parts.iter().enumerate() {
let normalized = part.trim()
.replace(" :: ", "::")
.replace(":: ", "::")
.replace(" ::", "::")
.replace("@ ", "@");
let p = normalized.trim();
if p.is_empty() {
return Err(format!("empty step in path {raw:?}"));
}
if p == "." { continue; }
let is_last = i == parts.len() - 1;
let step = if let Some(rest) = p.strip_prefix("attribute::") {
if !allow_attribute_tail || !is_last {
return Err(format!(
"attribute step not allowed here: {raw:?}"
));
}
PathStep::Attribute(parse_name_test(rest, resolve_prefix)?)
} else if let Some(rest) = p.strip_prefix('@') {
if !allow_attribute_tail || !is_last {
return Err(format!(
"attribute step not allowed here: {raw:?}"
));
}
PathStep::Attribute(parse_name_test(rest, resolve_prefix)?)
} else {
let bare = p.strip_prefix("child::").unwrap_or(p);
PathStep::Child(parse_name_test(bare, resolve_prefix)?)
};
steps.push(step);
}
if steps.is_empty() && !descendant {
return Err(format!("path has no steps: {raw:?}"));
}
Ok(PathExpr { descendant, steps })
}
fn parse_name_test(
s: &str,
resolve_prefix: &dyn Fn(&str) -> Option<String>,
) -> Result<NameTest, String> {
if s == "*" {
return Ok(NameTest::Any);
}
if s.is_empty() {
return Err("empty name in identity-constraint XPath".to_string());
}
if let Some((prefix, local)) = s.split_once(':') {
ensure_ncname(prefix)?;
let ns = resolve_prefix(prefix).ok_or_else(||
format!("undeclared namespace prefix in identity-constraint XPath: {prefix:?}"))?;
if local == "*" {
return Ok(NameTest::AnyInNs(Arc::from(ns)));
}
ensure_ncname(local)?;
Ok(NameTest::Name(QName::new(Some(&ns), local)))
} else {
ensure_ncname(s)?;
Ok(NameTest::Name(QName::new(None, s)))
}
}
fn ensure_ncname(s: &str) -> Result<(), String> {
let mut chars = s.chars();
let Some(first) = chars.next() else {
return Err("empty NCName in identity-constraint XPath".to_string());
};
if !is_name_start(first) || first == ':' {
return Err(format!("invalid NCName start char {first:?} in identity-constraint XPath"));
}
for c in chars {
if !is_name_char(c) || c == ':' {
return Err(format!("invalid NCName char {c:?} in identity-constraint XPath"));
}
}
Ok(())
}
fn is_name_start(c: char) -> bool {
matches!(c,
'A'..='Z' | '_' | 'a'..='z'
| '\u{C0}'..='\u{D6}' | '\u{D8}'..='\u{F6}' | '\u{F8}'..='\u{2FF}'
| '\u{370}'..='\u{37D}' | '\u{37F}'..='\u{1FFF}'
| '\u{200C}'..='\u{200D}' | '\u{2070}'..='\u{218F}'
| '\u{2C00}'..='\u{2FEF}' | '\u{3001}'..='\u{D7FF}'
| '\u{F900}'..='\u{FDCF}' | '\u{FDF0}'..='\u{FFFD}'
| '\u{10000}'..='\u{EFFFF}'
)
}
fn is_name_char(c: char) -> bool {
is_name_start(c)
|| matches!(c,
'-' | '.' | '0'..='9' | '\u{B7}'
| '\u{0300}'..='\u{036F}' | '\u{203F}'..='\u{2040}'
)
}
fn strip_xpath_whitespace(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut prev_was_struct = false;
for tok in s.split_whitespace() {
if !out.is_empty() && !prev_was_struct && !tok.starts_with('/') {
out.push(' ');
}
out.push_str(tok);
prev_was_struct = tok.ends_with('/');
}
let mut compacted = String::with_capacity(out.len());
let bytes = out.as_bytes();
let mut i = 0;
while i < bytes.len() {
let c = bytes[i] as char;
if c == ' ' {
let prev = compacted.chars().last();
let next = bytes.get(i + 1).map(|b| *b as char);
let prev2: Option<char> = {
let s = compacted.as_str();
let mut chars = s.chars().rev();
chars.next(); chars.next()
};
let next2 = bytes.get(i + 2).map(|b| *b as char);
let prev_is_axis = matches!((prev2, prev), (Some(':'), Some(':')));
let next_is_axis = matches!((next, next2), (Some(':'), Some(':')));
let touches_struct = matches!(prev, Some('/') | Some('@'))
|| matches!(next, Some('/') | Some('@'))
|| prev_is_axis
|| next_is_axis;
if touches_struct { i += 1; continue; }
}
compacted.push(c);
i += 1;
}
compacted
}
#[cfg(test)]
mod tests {
use super::*;
fn no_prefixes(_: &str) -> Option<String> { None }
#[test]
fn parses_simple_child_path() {
let p = parse_selector("child", &no_prefixes).unwrap();
assert_eq!(p.paths.len(), 1);
assert_eq!(p.paths[0].steps.len(), 1);
assert!(matches!(&p.paths[0].steps[0], PathStep::Child(NameTest::Name(_))));
assert!(!p.paths[0].descendant);
}
#[test]
fn parses_descendant_prefix() {
let p = parse_selector(".//foo/bar", &no_prefixes).unwrap();
assert!(p.paths[0].descendant);
assert_eq!(p.paths[0].steps.len(), 2);
}
#[test]
fn parses_dot_self() {
let p = parse_selector(".", &no_prefixes).unwrap();
assert_eq!(p.paths[0].steps.len(), 0);
}
#[test]
fn parses_wildcard() {
let p = parse_selector("*", &no_prefixes).unwrap();
assert!(matches!(&p.paths[0].steps[0], PathStep::Child(NameTest::Any)));
}
#[test]
fn parses_field_with_attribute() {
let f = parse_field("@id", &no_prefixes).unwrap();
assert_eq!(f.paths[0].steps.len(), 1);
assert!(matches!(&f.paths[0].steps[0], PathStep::Attribute(_)));
assert!(f.all_attribute);
}
#[test]
fn parses_field_with_child_then_attribute() {
let f = parse_field("foo/@id", &no_prefixes).unwrap();
assert_eq!(f.paths[0].steps.len(), 2);
assert!(matches!(&f.paths[0].steps[0], PathStep::Child(_)));
assert!(matches!(&f.paths[0].steps[1], PathStep::Attribute(_)));
assert!(f.all_attribute);
}
#[test]
fn rejects_attribute_in_selector() {
assert!(parse_selector("@id", &no_prefixes).is_err());
assert!(parse_selector("foo/@id", &no_prefixes).is_err());
}
#[test]
fn rejects_attribute_mid_path() {
assert!(parse_field("@id/foo", &no_prefixes).is_err());
}
#[test]
fn parses_union() {
let p = parse_selector("foo | bar | baz", &no_prefixes).unwrap();
assert_eq!(p.paths.len(), 3);
}
#[test]
fn resolves_prefixed_names() {
let resolver = |p: &str| if p == "ns" { Some("urn:x".into()) } else { None };
let p = parse_selector("ns:foo", &resolver).unwrap();
match &p.paths[0].steps[0] {
PathStep::Child(NameTest::Name(qn)) => {
assert_eq!(qn.namespace.as_deref(), Some("urn:x"));
assert_eq!(qn.local.as_ref(), "foo");
}
_ => panic!(),
}
}
#[test]
fn rejects_undeclared_prefix() {
assert!(parse_selector("unknown:foo", &no_prefixes).is_err());
}
#[test]
fn parses_namespace_wildcard() {
let resolver = |p: &str| if p == "ns" { Some("urn:x".into()) } else { None };
let p = parse_selector("ns:*", &resolver).unwrap();
assert!(matches!(&p.paths[0].steps[0], PathStep::Child(NameTest::AnyInNs(_))));
}
}