Skip to main content

xsd_schema/xpath/
node_test.rs

1//! XPath node test matching helpers.
2//!
3//! Provides a unified node test type that can be used by axis iterators
4//! and type-based filters, aligning with `XPATH_ITERATOR_PORT_PLAN.md`.
5
6use crate::ids::TypeKey;
7use crate::namespace::qname::QualifiedName;
8use crate::schema::model::DerivationSet;
9use crate::types::value::XmlValue;
10use crate::types::{ItemType, NameTest, SequenceType};
11use crate::xpath::ast::{ItemTypeNode, KindTest};
12use crate::xpath::cast::type_matches;
13use crate::xpath::iterator::XmlItem;
14
15use super::context::XPathContext;
16use super::{DomNavigator, DomNodeType};
17
18/// Unified node test for axis iterators.
19#[derive(Debug, Clone)]
20pub enum NodeTest {
21    /// Name test (`*`, `*:local`, `prefix:*`, or QName).
22    Name(NameTest),
23    /// Sequence type test (`node()`, `element(...)`, etc.).
24    Type(SequenceType),
25}
26
27impl NodeTest {
28    pub fn matches<N: DomNavigator>(&self, nav: &N, ctx: &XPathContext<'_>) -> bool {
29        match self {
30            NodeTest::Name(test) => matches_name_test(test, nav, ctx),
31            NodeTest::Type(seq) => matches_sequence_type(seq, nav, ctx),
32        }
33    }
34}
35
36pub fn matches_name_test<N: DomNavigator>(
37    test: &NameTest,
38    nav: &N,
39    ctx: &XPathContext<'_>,
40) -> bool {
41    if nav.node_type() != DomNodeType::Element && nav.node_type() != DomNodeType::Attribute {
42        return false;
43    }
44
45    match test {
46        NameTest::Wildcard => true,
47        NameTest::NamespaceWildcard(local_id) => {
48            // *:local - match any namespace with specific local name
49            match ctx.resolve_name(*local_id) {
50                Some(local) => nav.local_name() == local,
51                None => false,
52            }
53        }
54        NameTest::LocalWildcard(ns_id) => {
55            // prefix:* - match any local name in specific namespace
56            match ctx.resolve_name(*ns_id) {
57                Some(ns) => nav.namespace_uri() == ns,
58                None => false,
59            }
60        }
61        NameTest::QName(qname) => qname_matches(qname, nav, ctx),
62    }
63}
64
65pub fn matches_sequence_type<N: DomNavigator>(
66    sequence: &SequenceType,
67    nav: &N,
68    ctx: &XPathContext<'_>,
69) -> bool {
70    matches_item_type(&sequence.item_type, nav, ctx)
71}
72
73fn matches_item_type<N: DomNavigator>(
74    item_type: &ItemType,
75    nav: &N,
76    ctx: &XPathContext<'_>,
77) -> bool {
78    match item_type {
79        ItemType::AnyItem | ItemType::AnyNode => true,
80        ItemType::Document(None) => nav.node_type() == DomNodeType::Root,
81        ItemType::Document(Some(inner)) => match_document_with_inner(inner, nav, ctx),
82        ItemType::Element(name_test, schema_type) => {
83            if nav.node_type() != DomNodeType::Element {
84                return false;
85            }
86            if let Some(test) = name_test {
87                if !matches_name_test(test, nav, ctx) {
88                    return false;
89                }
90            }
91            if let Some(expected) = schema_type {
92                // Use derivation checking if schema_set is available
93                if let Some(actual) = nav.schema_type() {
94                    if let Some(schema_set) = ctx.schema_set {
95                        // Check if actual type is derived from expected type
96                        // Using empty DerivationSet means any derivation method is allowed
97                        if !schema_set.is_type_derived_from(
98                            TypeKey::Simple(actual),
99                            TypeKey::Simple(*expected),
100                            DerivationSet::empty(),
101                        ) {
102                            return false;
103                        }
104                    } else {
105                        // Fallback to equality without schema set
106                        if actual != *expected {
107                            return false;
108                        }
109                    }
110                } else {
111                    // No schema type on node, fail the type match
112                    return false;
113                }
114            }
115            true
116        }
117        ItemType::Attribute(name_test, schema_type) => {
118            if nav.node_type() != DomNodeType::Attribute {
119                return false;
120            }
121            if let Some(test) = name_test {
122                if !matches_name_test(test, nav, ctx) {
123                    return false;
124                }
125            }
126            if let Some(expected) = schema_type {
127                // Use derivation checking if schema_set is available
128                if let Some(actual) = nav.schema_type() {
129                    if let Some(schema_set) = ctx.schema_set {
130                        // Check if actual type is derived from expected type
131                        if !schema_set.is_type_derived_from(
132                            TypeKey::Simple(actual),
133                            TypeKey::Simple(*expected),
134                            DerivationSet::empty(),
135                        ) {
136                            return false;
137                        }
138                    } else {
139                        // Fallback to equality without schema set
140                        if actual != *expected {
141                            return false;
142                        }
143                    }
144                } else {
145                    // No schema type on node, fail the type match
146                    return false;
147                }
148            }
149            true
150        }
151        ItemType::SchemaElement(name) => {
152            if nav.node_type() != DomNodeType::Element {
153                return false;
154            }
155            // Check element name matches
156            if !qname_matches(name, nav, ctx) {
157                return false;
158            }
159            // If schema_set available, validate declaration exists and type derivation
160            if let Some(schema_set) = ctx.schema_set {
161                // Lookup element declaration - must exist for schema-element() to match
162                let ns_id = name.namespace_uri;
163                let Some(elem_key) = schema_set.lookup_element(ns_id, name.local_name) else {
164                    // Declaration not found in schema - no match
165                    return false;
166                };
167                let Some(elem_data) = schema_set.arenas.elements.get(elem_key) else {
168                    return false;
169                };
170                // Check type derivation if declaration has resolved_type
171                if let Some(expected_type) = elem_data.resolved_type {
172                    let Some(actual_type) = nav.schema_type() else {
173                        // Node has no type annotation but declaration expects one
174                        return false;
175                    };
176                    // Node type must derive from declaration type
177                    return schema_set.is_type_derived_from(
178                        TypeKey::Simple(actual_type),
179                        expected_type,
180                        DerivationSet::empty(),
181                    );
182                }
183                // Declaration found, no type constraint - match
184                return true;
185            }
186            // No schema context - fall back to name-only match
187            true
188        }
189        ItemType::SchemaAttribute(name) => {
190            if nav.node_type() != DomNodeType::Attribute {
191                return false;
192            }
193            // Check attribute name matches
194            if !qname_matches(name, nav, ctx) {
195                return false;
196            }
197            // If schema_set available, validate declaration exists and type derivation
198            if let Some(schema_set) = ctx.schema_set {
199                // Lookup attribute declaration - must exist for schema-attribute() to match
200                let ns_id = name.namespace_uri;
201                let Some(attr_key) = schema_set.lookup_attribute(ns_id, name.local_name) else {
202                    // Declaration not found in schema - no match
203                    return false;
204                };
205                let Some(attr_data) = schema_set.arenas.attributes.get(attr_key) else {
206                    return false;
207                };
208                // Check type derivation if declaration has resolved_type
209                if let Some(expected_type) = attr_data.resolved_type {
210                    let Some(actual_type) = nav.schema_type() else {
211                        // Node has no type annotation but declaration expects one
212                        return false;
213                    };
214                    // Node type must derive from declaration type
215                    return schema_set.is_type_derived_from(
216                        TypeKey::Simple(actual_type),
217                        expected_type,
218                        DerivationSet::empty(),
219                    );
220                }
221                // Declaration found, no type constraint - match
222                return true;
223            }
224            // No schema context - fall back to name-only match
225            true
226        }
227        ItemType::Text => nav.node_type().is_text_like(),
228        ItemType::Comment => nav.node_type() == DomNodeType::Comment,
229        ItemType::ProcessingInstruction(target) => {
230            nav.node_type() == DomNodeType::ProcessingInstruction
231                && target.as_ref().is_none_or(|name| nav.local_name() == name)
232        }
233        ItemType::NamespaceNode => nav.node_type() == DomNodeType::Namespace,
234        ItemType::AtomicType(_) | ItemType::SchemaAtomicType(_) => false,
235    }
236}
237
238fn match_document_with_inner<N: DomNavigator>(
239    inner: &ItemType,
240    nav: &N,
241    ctx: &XPathContext<'_>,
242) -> bool {
243    if nav.node_type() != DomNodeType::Root {
244        return false;
245    }
246
247    let mut cursor = nav.clone();
248    if !cursor.move_to_first_child() {
249        return false;
250    }
251
252    loop {
253        if matches_item_type(inner, &cursor, ctx) {
254            return true;
255        }
256        if !cursor.move_to_next_sibling() {
257            break;
258        }
259    }
260
261    false
262}
263
264fn qname_matches<N: DomNavigator>(qname: &QualifiedName, nav: &N, ctx: &XPathContext<'_>) -> bool {
265    let local = match ctx.resolve_name(qname.local_name) {
266        Some(local) => local,
267        None => return false,
268    };
269    let ns = match qname.namespace_uri {
270        Some(id) => match ctx.resolve_name(id) {
271            Some(ns) => ns,
272            None => return false,
273        },
274        None => String::new(),
275    };
276
277    nav.local_name() == local && nav.namespace_uri() == ns
278}
279
280// ============================================================================
281// AST KindTest and ItemTypeNode Matching
282// ============================================================================
283
284/// Check if an XmlItem matches an AST ItemTypeNode.
285///
286/// This is used for `instance of` and `treat as` expressions to check
287/// if a value matches the target type specification.
288///
289/// # Arguments
290///
291/// * `item` - The item to check (node or atomic value)
292/// * `item_type` - The AST item type node to match against
293/// * `resolved_atomic_type` - The resolved QualifiedName for atomic types (from binding)
294/// * `ctx` - The XPath context for name resolution
295///
296/// # Returns
297///
298/// `true` if the item matches the item type, `false` otherwise.
299pub fn matches_item_type_node<N: DomNavigator>(
300    item: &XmlItem<N>,
301    item_type: &ItemTypeNode,
302    resolved_atomic_type: Option<&QualifiedName>,
303    ctx: &XPathContext<'_>,
304) -> bool {
305    match item_type {
306        ItemTypeNode::Item => {
307            // item() matches any item (node or atomic)
308            true
309        }
310        ItemTypeNode::Atomic(_) => {
311            // Atomic type - item must be an atomic value matching the type
312            match item {
313                XmlItem::Node(_) => false,
314                XmlItem::Atomic(value) => {
315                    // Use the resolved atomic type from binding
316                    if let Some(qname) = resolved_atomic_type {
317                        matches_atomic_type(value, qname, ctx)
318                    } else {
319                        // No resolved type - this shouldn't happen after binding
320                        false
321                    }
322                }
323            }
324        }
325        ItemTypeNode::Kind(kind_test) => {
326            // Kind test - item must be a node matching the kind test
327            match item {
328                XmlItem::Node(nav) => matches_kind_test(nav, kind_test, ctx),
329                XmlItem::Atomic(_) => false,
330            }
331        }
332    }
333}
334
335/// Check if an atomic value matches a resolved atomic type QualifiedName.
336fn matches_atomic_type(value: &XmlValue, qname: &QualifiedName, ctx: &XPathContext<'_>) -> bool {
337    use crate::namespace::table::well_known;
338    use crate::xpath::cast::resolved_type_to_type_code;
339
340    // Verify it's in XS namespace
341    match qname.namespace_uri {
342        Some(ns_id) if ns_id == well_known::XS_NAMESPACE => {}
343        _ => return false,
344    }
345
346    // Get the target type code
347    let target_type = match resolved_type_to_type_code(qname, ctx.names) {
348        Ok(tc) => tc,
349        Err(_) => return false,
350    };
351
352    // Check if the value's type matches
353    type_matches(value.type_code, target_type)
354}
355
356/// Check if a DOM node matches an AST KindTest.
357///
358/// This converts the AST KindTest to runtime type checks.
359pub fn matches_kind_test<N: DomNavigator>(
360    nav: &N,
361    kind_test: &KindTest,
362    ctx: &XPathContext<'_>,
363) -> bool {
364    match kind_test {
365        KindTest::AnyKind => {
366            // node() matches any node
367            true
368        }
369        KindTest::Text => nav.node_type().is_text_like(),
370        KindTest::Comment => nav.node_type() == DomNodeType::Comment,
371        KindTest::ProcessingInstruction(target) => {
372            if nav.node_type() != DomNodeType::ProcessingInstruction {
373                return false;
374            }
375            match target {
376                None => true,
377                Some(name) => nav.local_name() == *name,
378            }
379        }
380        KindTest::Document(inner) => {
381            if nav.node_type() != DomNodeType::Root {
382                return false;
383            }
384            match inner {
385                None => true,
386                Some(inner_kind) => {
387                    // document-node(element(...)) - check if document has matching element
388                    let mut cursor = nav.clone();
389                    if !cursor.move_to_first_child() {
390                        return false;
391                    }
392                    loop {
393                        if matches_kind_test(&cursor, inner_kind, ctx) {
394                            return true;
395                        }
396                        if !cursor.move_to_next_sibling() {
397                            break;
398                        }
399                    }
400                    false
401                }
402            }
403        }
404        KindTest::Element(elem_test) => {
405            if nav.node_type() != DomNodeType::Element {
406                return false;
407            }
408            // Check element name if specified
409            if let Some(ref qname) = elem_test.name {
410                if !ast_qname_matches(qname, nav, ctx) {
411                    return false;
412                }
413            }
414            // TODO: Check type annotation if specified (elem_test.type_name)
415            true
416        }
417        KindTest::Attribute(attr_test) => {
418            if nav.node_type() != DomNodeType::Attribute {
419                return false;
420            }
421            // Check attribute name if specified
422            if let Some(ref qname) = attr_test.name {
423                if !ast_qname_matches(qname, nav, ctx) {
424                    return false;
425                }
426            }
427            // TODO: Check type annotation if specified (attr_test.type_name)
428            true
429        }
430        KindTest::SchemaElement(name) => {
431            if nav.node_type() != DomNodeType::Element {
432                return false;
433            }
434            // Parse the QName string to extract prefix and local name
435            use crate::xpath::functions::qname::parse_lexical_qname;
436            let Ok((prefix_opt, local_name)) = parse_lexical_qname(name) else {
437                return false; // Invalid QName syntax
438            };
439            // Check local name matches
440            if nav.local_name() != local_name {
441                return false;
442            }
443            // Resolve namespace: use prefix if provided, otherwise default element namespace
444            let expected_ns = if let Some(prefix) = &prefix_opt {
445                ctx.resolve_prefix(prefix).unwrap_or_default()
446            } else {
447                ctx.default_element_ns
448                    .and_then(|id| ctx.names.try_resolve(id))
449                    .unwrap_or_default()
450            };
451            // Verify node's namespace matches expected
452            if nav.namespace_uri() != expected_ns {
453                return false;
454            }
455            // If schema_set available, validate declaration exists and type
456            if let Some(schema_set) = ctx.schema_set {
457                // Get local name as NameId - if not found, declaration doesn't exist
458                let Some(local_id) = ctx.names.get(&local_name) else {
459                    return false;
460                };
461                // Get namespace as NameId
462                let ns_id = if expected_ns.is_empty() {
463                    None
464                } else {
465                    ctx.names.get(&expected_ns)
466                };
467                // Lookup element declaration - must exist for schema-element() to match
468                let Some(elem_key) = schema_set.lookup_element(ns_id, local_id) else {
469                    return false;
470                };
471                let Some(elem_data) = schema_set.arenas.elements.get(elem_key) else {
472                    return false;
473                };
474                // Check type derivation if declaration has resolved_type
475                if let Some(expected_type) = elem_data.resolved_type {
476                    let Some(actual_type) = nav.schema_type() else {
477                        return false;
478                    };
479                    return schema_set.is_type_derived_from(
480                        TypeKey::Simple(actual_type),
481                        expected_type,
482                        DerivationSet::empty(),
483                    );
484                }
485                // Declaration found, no type constraint - match
486                return true;
487            }
488            // No schema context - name and namespace already verified
489            true
490        }
491        KindTest::SchemaAttribute(name) => {
492            if nav.node_type() != DomNodeType::Attribute {
493                return false;
494            }
495            // Parse the QName string to extract prefix and local name
496            use crate::xpath::functions::qname::parse_lexical_qname;
497            let Ok((prefix_opt, local_name)) = parse_lexical_qname(name) else {
498                return false; // Invalid QName syntax
499            };
500            // Check local name matches
501            if nav.local_name() != local_name {
502                return false;
503            }
504            // Resolve namespace: use prefix if provided, otherwise empty (attributes default to no namespace)
505            let expected_ns = if let Some(prefix) = &prefix_opt {
506                ctx.resolve_prefix(prefix).unwrap_or_default()
507            } else {
508                String::new() // Unprefixed attributes have no namespace
509            };
510            // Verify node's namespace matches expected
511            if nav.namespace_uri() != expected_ns {
512                return false;
513            }
514            // If schema_set available, validate declaration exists and type
515            if let Some(schema_set) = ctx.schema_set {
516                // Get local name as NameId - if not found, declaration doesn't exist
517                let Some(local_id) = ctx.names.get(&local_name) else {
518                    return false;
519                };
520                // Get namespace as NameId
521                let ns_id = if expected_ns.is_empty() {
522                    None
523                } else {
524                    ctx.names.get(&expected_ns)
525                };
526                // Lookup attribute declaration - must exist for schema-attribute() to match
527                let Some(attr_key) = schema_set.lookup_attribute(ns_id, local_id) else {
528                    return false;
529                };
530                let Some(attr_data) = schema_set.arenas.attributes.get(attr_key) else {
531                    return false;
532                };
533                // Check type derivation if declaration has resolved_type
534                if let Some(expected_type) = attr_data.resolved_type {
535                    let Some(actual_type) = nav.schema_type() else {
536                        return false;
537                    };
538                    return schema_set.is_type_derived_from(
539                        TypeKey::Simple(actual_type),
540                        expected_type,
541                        DerivationSet::empty(),
542                    );
543                }
544                // Declaration found, no type constraint - match
545                return true;
546            }
547            // No schema context - name and namespace already verified
548            true
549        }
550    }
551}
552
553/// Check if a node matches an AST QName (from paths.rs).
554fn ast_qname_matches<N: DomNavigator>(
555    qname: &crate::xpath::ast::QName,
556    nav: &N,
557    ctx: &XPathContext<'_>,
558) -> bool {
559    // For AST QName, prefix is stored directly as a string
560    // Local name must match
561    if nav.local_name() != qname.local {
562        return false;
563    }
564
565    // Resolve prefix to namespace URI
566    if qname.prefix.is_empty() {
567        // No prefix - match empty namespace
568        nav.namespace_uri().is_empty()
569    } else {
570        // Resolve the prefix to namespace URI
571        match ctx.resolve_prefix(&qname.prefix) {
572            Some(ns_uri) => nav.namespace_uri() == ns_uri,
573            None => false,
574        }
575    }
576}
577
578#[cfg(test)]
579#[path = "node_test_tests.rs"]
580mod tests;