1use std::sync::Arc;
33
34use super::schema::QName;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum ConstraintKind {
40 Key,
42 Unique,
44 KeyRef,
46}
47
48#[derive(Debug, Clone)]
49pub struct IdentityConstraint {
50 pub name: QName,
51 pub kind: ConstraintKind,
52 pub selector: SelectorPath,
53 pub fields: Vec<FieldPath>,
54 pub refer: Option<QName>,
56}
57
58#[derive(Debug, Clone)]
71pub struct SelectorPath {
72 pub paths: Vec<PathExpr>,
73}
74
75#[derive(Debug, Clone)]
76pub struct FieldPath {
77 pub paths: Vec<PathExpr>,
78 pub all_attribute: bool,
81}
82
83#[derive(Debug, Clone)]
84pub struct PathExpr {
85 pub descendant: bool,
88 pub steps: Vec<PathStep>,
89}
90
91#[derive(Debug, Clone)]
92pub enum PathStep {
93 Child(NameTest),
95 Attribute(NameTest),
98}
99
100#[derive(Debug, Clone)]
101pub enum NameTest {
102 Any,
104 Name(QName),
107 AnyInNs(Arc<str>),
109}
110
111pub fn parse_selector(
115 xpath: &str,
116 resolve_prefix: &dyn Fn(&str) -> Option<String>,
117) -> Result<SelectorPath, String> {
118 let mut paths = Vec::new();
119 for part in xpath.split('|') {
120 paths.push(parse_path(part.trim(), false, resolve_prefix)?);
121 }
122 if paths.is_empty() {
123 return Err("empty selector".to_string());
124 }
125 Ok(SelectorPath { paths })
126}
127
128pub fn parse_field(
131 xpath: &str,
132 resolve_prefix: &dyn Fn(&str) -> Option<String>,
133) -> Result<FieldPath, String> {
134 let mut paths = Vec::new();
135 for part in xpath.split('|') {
136 paths.push(parse_path(part.trim(), true, resolve_prefix)?);
137 }
138 if paths.is_empty() {
139 return Err("empty field".to_string());
140 }
141 let all_attribute = paths.iter().all(|p|
142 matches!(p.steps.last(), Some(PathStep::Attribute(_)))
143 );
144 Ok(FieldPath { paths, all_attribute })
145}
146
147fn parse_path(
148 raw: &str,
149 allow_attribute_tail: bool,
150 resolve_prefix: &dyn Fn(&str) -> Option<String>,
151) -> Result<PathExpr, String> {
152 let trimmed = raw.trim();
157 let normalized = strip_xpath_whitespace(trimmed);
158 let raw = normalized.as_str();
159 let (descendant, body) = if let Some(rest) = raw.strip_prefix(".//") {
160 (true, rest)
161 } else if let Some(rest) = raw.strip_prefix("./") {
162 (false, rest)
163 } else if raw == "." {
164 return Ok(PathExpr { descendant: false, steps: Vec::new() });
165 } else {
166 (false, raw)
167 };
168
169 let mut steps = Vec::new();
170 let parts: Vec<&str> = body.split('/').collect();
171 for (i, part) in parts.iter().enumerate() {
172 let normalized = part.trim()
177 .replace(" :: ", "::")
178 .replace(":: ", "::")
179 .replace(" ::", "::")
180 .replace("@ ", "@");
181 let p = normalized.trim();
182 if p.is_empty() {
183 return Err(format!("empty step in path {raw:?}"));
184 }
185 if p == "." { continue; }
191 let is_last = i == parts.len() - 1;
192
193 let step = if let Some(rest) = p.strip_prefix("attribute::") {
194 if !allow_attribute_tail || !is_last {
195 return Err(format!(
196 "attribute step not allowed here: {raw:?}"
197 ));
198 }
199 PathStep::Attribute(parse_name_test(rest, resolve_prefix)?)
200 } else if let Some(rest) = p.strip_prefix('@') {
201 if !allow_attribute_tail || !is_last {
202 return Err(format!(
203 "attribute step not allowed here: {raw:?}"
204 ));
205 }
206 PathStep::Attribute(parse_name_test(rest, resolve_prefix)?)
207 } else {
208 let bare = p.strip_prefix("child::").unwrap_or(p);
209 PathStep::Child(parse_name_test(bare, resolve_prefix)?)
210 };
211 steps.push(step);
212 }
213 if steps.is_empty() && !descendant {
214 return Err(format!("path has no steps: {raw:?}"));
215 }
216 Ok(PathExpr { descendant, steps })
217}
218
219fn parse_name_test(
220 s: &str,
221 resolve_prefix: &dyn Fn(&str) -> Option<String>,
222) -> Result<NameTest, String> {
223 if s == "*" {
224 return Ok(NameTest::Any);
225 }
226 if s.is_empty() {
227 return Err("empty name in identity-constraint XPath".to_string());
228 }
229 if let Some((prefix, local)) = s.split_once(':') {
230 ensure_ncname(prefix)?;
231 let ns = resolve_prefix(prefix).ok_or_else(||
232 format!("undeclared namespace prefix in identity-constraint XPath: {prefix:?}"))?;
233 if local == "*" {
234 return Ok(NameTest::AnyInNs(Arc::from(ns)));
235 }
236 ensure_ncname(local)?;
237 Ok(NameTest::Name(QName::new(Some(&ns), local)))
238 } else {
239 ensure_ncname(s)?;
245 Ok(NameTest::Name(QName::new(None, s)))
246 }
247}
248
249fn ensure_ncname(s: &str) -> Result<(), String> {
256 let mut chars = s.chars();
257 let Some(first) = chars.next() else {
258 return Err("empty NCName in identity-constraint XPath".to_string());
259 };
260 if !is_name_start(first) || first == ':' {
261 return Err(format!("invalid NCName start char {first:?} in identity-constraint XPath"));
262 }
263 for c in chars {
264 if !is_name_char(c) || c == ':' {
265 return Err(format!("invalid NCName char {c:?} in identity-constraint XPath"));
266 }
267 }
268 Ok(())
269}
270
271fn is_name_start(c: char) -> bool {
272 matches!(c,
273 'A'..='Z' | '_' | 'a'..='z'
274 | '\u{C0}'..='\u{D6}' | '\u{D8}'..='\u{F6}' | '\u{F8}'..='\u{2FF}'
275 | '\u{370}'..='\u{37D}' | '\u{37F}'..='\u{1FFF}'
276 | '\u{200C}'..='\u{200D}' | '\u{2070}'..='\u{218F}'
277 | '\u{2C00}'..='\u{2FEF}' | '\u{3001}'..='\u{D7FF}'
278 | '\u{F900}'..='\u{FDCF}' | '\u{FDF0}'..='\u{FFFD}'
279 | '\u{10000}'..='\u{EFFFF}'
280 )
281}
282
283fn is_name_char(c: char) -> bool {
284 is_name_start(c)
285 || matches!(c,
286 '-' | '.' | '0'..='9' | '\u{B7}'
287 | '\u{0300}'..='\u{036F}' | '\u{203F}'..='\u{2040}'
288 )
289}
290
291fn strip_xpath_whitespace(s: &str) -> String {
296 let mut out = String::with_capacity(s.len());
297 let mut prev_was_struct = false;
298 for tok in s.split_whitespace() {
299 if !out.is_empty() && !prev_was_struct && !tok.starts_with('/') {
300 out.push(' ');
301 }
302 out.push_str(tok);
303 prev_was_struct = tok.ends_with('/');
304 }
305 let mut compacted = String::with_capacity(out.len());
311 let bytes = out.as_bytes();
312 let mut i = 0;
313 while i < bytes.len() {
314 let c = bytes[i] as char;
315 if c == ' ' {
316 let prev = compacted.chars().last();
317 let next = bytes.get(i + 1).map(|b| *b as char);
318 let prev2: Option<char> = {
319 let s = compacted.as_str();
320 let mut chars = s.chars().rev();
321 chars.next(); chars.next()
322 };
323 let next2 = bytes.get(i + 2).map(|b| *b as char);
324 let prev_is_axis = matches!((prev2, prev), (Some(':'), Some(':')));
326 let next_is_axis = matches!((next, next2), (Some(':'), Some(':')));
327 let touches_struct = matches!(prev, Some('/') | Some('@'))
328 || matches!(next, Some('/') | Some('@'))
329 || prev_is_axis
330 || next_is_axis;
331 if touches_struct { i += 1; continue; }
332 }
333 compacted.push(c);
334 i += 1;
335 }
336 compacted
337}
338
339#[cfg(test)]
342mod tests {
343 use super::*;
344
345 fn no_prefixes(_: &str) -> Option<String> { None }
346
347 #[test]
348 fn parses_simple_child_path() {
349 let p = parse_selector("child", &no_prefixes).unwrap();
350 assert_eq!(p.paths.len(), 1);
351 assert_eq!(p.paths[0].steps.len(), 1);
352 assert!(matches!(&p.paths[0].steps[0], PathStep::Child(NameTest::Name(_))));
353 assert!(!p.paths[0].descendant);
354 }
355
356 #[test]
357 fn parses_descendant_prefix() {
358 let p = parse_selector(".//foo/bar", &no_prefixes).unwrap();
359 assert!(p.paths[0].descendant);
360 assert_eq!(p.paths[0].steps.len(), 2);
361 }
362
363 #[test]
364 fn parses_dot_self() {
365 let p = parse_selector(".", &no_prefixes).unwrap();
366 assert_eq!(p.paths[0].steps.len(), 0);
367 }
368
369 #[test]
370 fn parses_wildcard() {
371 let p = parse_selector("*", &no_prefixes).unwrap();
372 assert!(matches!(&p.paths[0].steps[0], PathStep::Child(NameTest::Any)));
373 }
374
375 #[test]
376 fn parses_field_with_attribute() {
377 let f = parse_field("@id", &no_prefixes).unwrap();
378 assert_eq!(f.paths[0].steps.len(), 1);
379 assert!(matches!(&f.paths[0].steps[0], PathStep::Attribute(_)));
380 assert!(f.all_attribute);
381 }
382
383 #[test]
384 fn parses_field_with_child_then_attribute() {
385 let f = parse_field("foo/@id", &no_prefixes).unwrap();
386 assert_eq!(f.paths[0].steps.len(), 2);
387 assert!(matches!(&f.paths[0].steps[0], PathStep::Child(_)));
388 assert!(matches!(&f.paths[0].steps[1], PathStep::Attribute(_)));
389 assert!(f.all_attribute);
390 }
391
392 #[test]
393 fn rejects_attribute_in_selector() {
394 assert!(parse_selector("@id", &no_prefixes).is_err());
395 assert!(parse_selector("foo/@id", &no_prefixes).is_err());
396 }
397
398 #[test]
399 fn rejects_attribute_mid_path() {
400 assert!(parse_field("@id/foo", &no_prefixes).is_err());
401 }
402
403 #[test]
404 fn parses_union() {
405 let p = parse_selector("foo | bar | baz", &no_prefixes).unwrap();
406 assert_eq!(p.paths.len(), 3);
407 }
408
409 #[test]
410 fn resolves_prefixed_names() {
411 let resolver = |p: &str| if p == "ns" { Some("urn:x".into()) } else { None };
412 let p = parse_selector("ns:foo", &resolver).unwrap();
413 match &p.paths[0].steps[0] {
414 PathStep::Child(NameTest::Name(qn)) => {
415 assert_eq!(qn.namespace.as_deref(), Some("urn:x"));
416 assert_eq!(qn.local.as_ref(), "foo");
417 }
418 _ => panic!(),
419 }
420 }
421
422 #[test]
423 fn rejects_undeclared_prefix() {
424 assert!(parse_selector("unknown:foo", &no_prefixes).is_err());
425 }
426
427 #[test]
428 fn parses_namespace_wildcard() {
429 let resolver = |p: &str| if p == "ns" { Some("urn:x".into()) } else { None };
430 let p = parse_selector("ns:*", &resolver).unwrap();
431 assert!(matches!(&p.paths[0].steps[0], PathStep::Child(NameTest::AnyInNs(_))));
432 }
433}