Skip to main content

xdoc/query/
mod.rs

1//! XPath-like query subset.
2//!
3//! Supported syntax is intentionally small: absolute paths (`/Root/Child`),
4//! descendant paths (`//Child`), attributes (`@id`), `text()`, namespace aliases
5//! in names (`doc:Root`), and simple attribute predicates
6//! (`/Root/Item[@code='A1']`). This is not XQuery.
7
8use std::collections::BTreeMap;
9
10use crate::core::{
11    Document, ElementData, ErrorKind, NamespaceUri, NodeId, NodeKind, QName, Span, XmlError,
12    XmlResult,
13};
14use crate::security::QuerySecurityConfig;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct Query {
18    steps: Vec<QueryStep>,
19    source: String,
20}
21
22impl Query {
23    pub fn parse(source: &str) -> XmlResult<Self> {
24        Parser::new(source).parse()
25    }
26
27    pub fn evaluate(&self, document: &Document) -> XmlResult<QueryResult> {
28        self.evaluate_with_context(document, &NamespaceContext::default())
29    }
30
31    pub fn evaluate_with_context(
32        &self,
33        document: &Document,
34        namespaces: &NamespaceContext,
35    ) -> XmlResult<QueryResult> {
36        self.evaluate_with_options(document, namespaces, &QuerySecurityConfig::default())
37    }
38
39    pub fn evaluate_with_options(
40        &self,
41        document: &Document,
42        namespaces: &NamespaceContext,
43        security: &QuerySecurityConfig,
44    ) -> XmlResult<QueryResult> {
45        let Some(root) = document.root() else {
46            return Ok(QueryResult::default());
47        };
48        let mut evaluator = Evaluator::new(document, namespaces, security);
49        evaluator.evaluate(root, &self.steps)
50    }
51
52    pub fn source(&self) -> &str {
53        &self.source
54    }
55}
56
57#[derive(Debug, Clone, Default, PartialEq, Eq)]
58pub struct QueryResult {
59    values: Vec<QueryValue>,
60}
61
62impl QueryResult {
63    pub fn values(&self) -> &[QueryValue] {
64        &self.values
65    }
66
67    pub fn nodes(&self) -> Vec<NodeId> {
68        self.values
69            .iter()
70            .filter_map(|value| match value {
71                QueryValue::Node(id) => Some(*id),
72                _ => None,
73            })
74            .collect()
75    }
76
77    pub fn strings(&self) -> Vec<&str> {
78        self.values
79            .iter()
80            .filter_map(|value| match value {
81                QueryValue::Text(value) | QueryValue::Attribute { value, .. } => {
82                    Some(value.as_str())
83                }
84                QueryValue::Node(_) => None,
85            })
86            .collect()
87    }
88
89    pub fn len(&self) -> usize {
90        self.values.len()
91    }
92
93    pub fn is_empty(&self) -> bool {
94        self.values.is_empty()
95    }
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub enum QueryValue {
100    Node(NodeId),
101    Text(String),
102    Attribute { name: QName, value: String },
103}
104
105#[derive(Debug, Clone, Default, PartialEq, Eq)]
106pub struct NamespaceContext {
107    aliases: BTreeMap<String, NamespaceUri>,
108}
109
110impl NamespaceContext {
111    pub fn new() -> Self {
112        Self::default()
113    }
114
115    pub fn with_alias(
116        mut self,
117        alias: impl Into<String>,
118        uri: impl Into<String>,
119    ) -> XmlResult<Self> {
120        self.aliases.insert(alias.into(), NamespaceUri::new(uri)?);
121        Ok(self)
122    }
123
124    pub fn resolve(&self, alias: &str) -> Option<&NamespaceUri> {
125        self.aliases.get(alias)
126    }
127}
128
129pub trait DocumentQueryExt {
130    fn query(&self, source: &str) -> XmlResult<QueryResult>;
131    fn query_with_context(
132        &self,
133        source: &str,
134        namespaces: &NamespaceContext,
135    ) -> XmlResult<QueryResult>;
136}
137
138impl DocumentQueryExt for Document {
139    fn query(&self, source: &str) -> XmlResult<QueryResult> {
140        Query::parse(source)?.evaluate(self)
141    }
142
143    fn query_with_context(
144        &self,
145        source: &str,
146        namespaces: &NamespaceContext,
147    ) -> XmlResult<QueryResult> {
148        Query::parse(source)?.evaluate_with_context(self, namespaces)
149    }
150}
151
152#[derive(Debug, Clone, PartialEq, Eq)]
153enum QueryStep {
154    Root,
155    Child(NodeTest),
156    Descendant(NodeTest),
157    Attribute(NameTest),
158    Text,
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
162struct NodeTest {
163    name: NameTest,
164    predicate: Option<Predicate>,
165}
166
167#[derive(Debug, Clone, PartialEq, Eq)]
168struct Predicate {
169    attribute: NameTest,
170    value: String,
171}
172
173#[derive(Debug, Clone, PartialEq, Eq)]
174struct NameTest {
175    prefix: Option<String>,
176    local: String,
177}
178
179#[derive(Debug, Clone, PartialEq, Eq)]
180enum TokenKind {
181    Slash,
182    DoubleSlash,
183    At,
184    LBracket,
185    RBracket,
186    Eq,
187    LParen,
188    RParen,
189    Name(String),
190    String(String),
191}
192
193#[derive(Debug, Clone, PartialEq, Eq)]
194struct Token {
195    kind: TokenKind,
196    position: usize,
197}
198
199fn lex(source: &str) -> XmlResult<Vec<Token>> {
200    let bytes = source.as_bytes();
201    let mut position = 0;
202    let mut tokens = Vec::new();
203
204    while position < bytes.len() {
205        match bytes[position] {
206            b'/' if bytes.get(position + 1) == Some(&b'/') => {
207                tokens.push(Token {
208                    kind: TokenKind::DoubleSlash,
209                    position,
210                });
211                position += 2;
212            }
213            b'/' => {
214                tokens.push(Token {
215                    kind: TokenKind::Slash,
216                    position,
217                });
218                position += 1;
219            }
220            b'@' => {
221                tokens.push(Token {
222                    kind: TokenKind::At,
223                    position,
224                });
225                position += 1;
226            }
227            b'[' => {
228                tokens.push(Token {
229                    kind: TokenKind::LBracket,
230                    position,
231                });
232                position += 1;
233            }
234            b']' => {
235                tokens.push(Token {
236                    kind: TokenKind::RBracket,
237                    position,
238                });
239                position += 1;
240            }
241            b'=' => {
242                tokens.push(Token {
243                    kind: TokenKind::Eq,
244                    position,
245                });
246                position += 1;
247            }
248            b'(' => {
249                tokens.push(Token {
250                    kind: TokenKind::LParen,
251                    position,
252                });
253                position += 1;
254            }
255            b')' => {
256                tokens.push(Token {
257                    kind: TokenKind::RParen,
258                    position,
259                });
260                position += 1;
261            }
262            b'\'' | b'"' => {
263                let quote = bytes[position];
264                let start = position;
265                position += 1;
266                let value_start = position;
267                while position < bytes.len() && bytes[position] != quote {
268                    position += 1;
269                }
270                if position >= bytes.len() {
271                    return Err(query_error(source, start, "unterminated string literal"));
272                }
273                let value = source[value_start..position].to_owned();
274                tokens.push(Token {
275                    kind: TokenKind::String(value),
276                    position: start,
277                });
278                position += 1;
279            }
280            ch if ch.is_ascii_whitespace() => position += 1,
281            _ => {
282                let start = position;
283                while position < bytes.len() && is_name_byte(bytes[position]) {
284                    position += 1;
285                }
286                if start == position {
287                    return Err(query_error(
288                        source,
289                        position,
290                        format!("unexpected character `{}`", bytes[position] as char),
291                    ));
292                }
293                tokens.push(Token {
294                    kind: TokenKind::Name(source[start..position].to_owned()),
295                    position: start,
296                });
297            }
298        }
299    }
300
301    Ok(tokens)
302}
303
304fn is_name_byte(byte: u8) -> bool {
305    byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.' | b':')
306}
307
308struct Parser<'a> {
309    source: &'a str,
310    tokens: Vec<Token>,
311    position: usize,
312}
313
314impl<'a> Parser<'a> {
315    fn new(source: &'a str) -> Self {
316        Self {
317            source,
318            tokens: lex(source).unwrap_or_default(),
319            position: 0,
320        }
321    }
322
323    fn parse(mut self) -> XmlResult<Query> {
324        self.tokens = lex(self.source)?;
325        if self.tokens.is_empty() {
326            return Err(query_error(self.source, 0, "query cannot be empty"));
327        }
328        self.expect_slash_like_start()?;
329        let mut steps = vec![QueryStep::Root];
330
331        while !self.is_eof() {
332            let axis = self.consume_axis()?;
333            let step = self.parse_step(axis)?;
334            steps.push(step);
335        }
336
337        Ok(Query {
338            steps,
339            source: self.source.to_owned(),
340        })
341    }
342
343    fn expect_slash_like_start(&mut self) -> XmlResult<()> {
344        match self.peek_kind() {
345            Some(TokenKind::Slash | TokenKind::DoubleSlash) => Ok(()),
346            _ => Err(query_error(
347                self.source,
348                self.peek_position(),
349                "query must start with `/` or `//`",
350            )),
351        }
352    }
353
354    fn consume_axis(&mut self) -> XmlResult<Axis> {
355        match self.next_kind() {
356            Some(TokenKind::Slash) => Ok(Axis::Child),
357            Some(TokenKind::DoubleSlash) => Ok(Axis::Descendant),
358            _ => Err(query_error(
359                self.source,
360                self.peek_position(),
361                "expected `/` or `//`",
362            )),
363        }
364    }
365
366    fn parse_step(&mut self, axis: Axis) -> XmlResult<QueryStep> {
367        if self.consume_at() {
368            let name = self.parse_name()?;
369            return Ok(QueryStep::Attribute(name));
370        }
371
372        let name = self.parse_name()?;
373        if name.prefix.is_none() && name.local == "text" && self.consume_lparen() {
374            self.expect_rparen()?;
375            return Ok(QueryStep::Text);
376        }
377
378        let predicate = if self.consume_lbracket() {
379            Some(self.parse_predicate()?)
380        } else {
381            None
382        };
383        let test = NodeTest { name, predicate };
384        Ok(match axis {
385            Axis::Child => QueryStep::Child(test),
386            Axis::Descendant => QueryStep::Descendant(test),
387        })
388    }
389
390    fn parse_predicate(&mut self) -> XmlResult<Predicate> {
391        if !self.consume_at() {
392            return Err(query_error(
393                self.source,
394                self.peek_position(),
395                "predicate must select an attribute with `@`",
396            ));
397        }
398        let attribute = self.parse_name()?;
399        self.expect_eq()?;
400        let value = self.parse_string()?;
401        self.expect_rbracket()?;
402        Ok(Predicate { attribute, value })
403    }
404
405    fn parse_name(&mut self) -> XmlResult<NameTest> {
406        match self.next() {
407            Some(Token {
408                kind: TokenKind::Name(name),
409                position,
410            }) => name_test(self.source, position, &name),
411            Some(token) => Err(query_error(
412                self.source,
413                token.position,
414                "expected XML name in query step",
415            )),
416            None => Err(query_error(
417                self.source,
418                self.source.len(),
419                "expected XML name in query step",
420            )),
421        }
422    }
423
424    fn parse_string(&mut self) -> XmlResult<String> {
425        match self.next() {
426            Some(Token {
427                kind: TokenKind::String(value),
428                ..
429            }) => Ok(value),
430            Some(token) => Err(query_error(
431                self.source,
432                token.position,
433                "expected string literal",
434            )),
435            None => Err(query_error(
436                self.source,
437                self.source.len(),
438                "expected string literal",
439            )),
440        }
441    }
442
443    fn consume_at(&mut self) -> bool {
444        self.consume(|kind| matches!(kind, TokenKind::At))
445    }
446
447    fn consume_lparen(&mut self) -> bool {
448        self.consume(|kind| matches!(kind, TokenKind::LParen))
449    }
450
451    fn consume_lbracket(&mut self) -> bool {
452        self.consume(|kind| matches!(kind, TokenKind::LBracket))
453    }
454
455    fn expect_rparen(&mut self) -> XmlResult<()> {
456        self.expect(|kind| matches!(kind, TokenKind::RParen), "expected `)`")
457    }
458
459    fn expect_rbracket(&mut self) -> XmlResult<()> {
460        self.expect(|kind| matches!(kind, TokenKind::RBracket), "expected `]`")
461    }
462
463    fn expect_eq(&mut self) -> XmlResult<()> {
464        self.expect(|kind| matches!(kind, TokenKind::Eq), "expected `=`")
465    }
466
467    fn expect(&mut self, matches: impl FnOnce(&TokenKind) -> bool, message: &str) -> XmlResult<()> {
468        match self.next() {
469            Some(token) if matches(&token.kind) => Ok(()),
470            Some(token) => Err(query_error(self.source, token.position, message)),
471            None => Err(query_error(self.source, self.source.len(), message)),
472        }
473    }
474
475    fn consume(&mut self, matches: impl FnOnce(&TokenKind) -> bool) -> bool {
476        if self
477            .tokens
478            .get(self.position)
479            .is_some_and(|token| matches(&token.kind))
480        {
481            self.position += 1;
482            true
483        } else {
484            false
485        }
486    }
487
488    fn next_kind(&mut self) -> Option<TokenKind> {
489        self.next().map(|token| token.kind)
490    }
491
492    fn next(&mut self) -> Option<Token> {
493        let token = self.tokens.get(self.position).cloned();
494        if token.is_some() {
495            self.position += 1;
496        }
497        token
498    }
499
500    fn peek_kind(&self) -> Option<&TokenKind> {
501        self.tokens.get(self.position).map(|token| &token.kind)
502    }
503
504    fn peek_position(&self) -> usize {
505        self.tokens
506            .get(self.position)
507            .map(|token| token.position)
508            .unwrap_or(self.source.len())
509    }
510
511    fn is_eof(&self) -> bool {
512        self.position >= self.tokens.len()
513    }
514}
515
516#[derive(Debug, Clone, Copy, PartialEq, Eq)]
517enum Axis {
518    Child,
519    Descendant,
520}
521
522fn name_test(source: &str, position: usize, raw: &str) -> XmlResult<NameTest> {
523    let mut parts = raw.split(':');
524    let first = parts.next().expect("split always yields one part");
525    match (parts.next(), parts.next()) {
526        (Some(local), None) if !first.is_empty() && !local.is_empty() => Ok(NameTest {
527            prefix: Some(first.to_owned()),
528            local: local.to_owned(),
529        }),
530        (None, None) if !first.is_empty() => Ok(NameTest {
531            prefix: None,
532            local: first.to_owned(),
533        }),
534        _ => Err(query_error(source, position, "invalid qualified name")),
535    }
536}
537
538struct Evaluator<'a> {
539    document: &'a Document,
540    namespaces: &'a NamespaceContext,
541    security: &'a QuerySecurityConfig,
542    steps: usize,
543}
544
545impl<'a> Evaluator<'a> {
546    fn new(
547        document: &'a Document,
548        namespaces: &'a NamespaceContext,
549        security: &'a QuerySecurityConfig,
550    ) -> Self {
551        Self {
552            document,
553            namespaces,
554            security,
555            steps: 0,
556        }
557    }
558
559    fn evaluate(&mut self, root: NodeId, steps: &[QueryStep]) -> XmlResult<QueryResult> {
560        let mut values = match steps.get(1) {
561            Some(step) => self.apply_first_step(root, step)?,
562            None => vec![QueryValue::Node(root)],
563        };
564        for step in steps.iter().skip(2) {
565            values = self.apply_step(values, step)?;
566        }
567        Ok(QueryResult { values })
568    }
569
570    fn apply_step(
571        &mut self,
572        values: Vec<QueryValue>,
573        step: &QueryStep,
574    ) -> XmlResult<Vec<QueryValue>> {
575        let mut next = Vec::new();
576        for value in values {
577            let QueryValue::Node(node_id) = value else {
578                continue;
579            };
580            match step {
581                QueryStep::Root => next.push(QueryValue::Node(node_id)),
582                QueryStep::Child(test) => {
583                    for child in self.element_children(node_id)? {
584                        if self.node_matches(child, test)? {
585                            next.push(QueryValue::Node(child));
586                        }
587                    }
588                }
589                QueryStep::Descendant(test) => {
590                    for descendant in self.descendants(node_id)? {
591                        if self.node_matches(descendant, test)? {
592                            next.push(QueryValue::Node(descendant));
593                        }
594                    }
595                }
596                QueryStep::Attribute(name) => {
597                    if let Some(element) = self.element(node_id)? {
598                        for attribute in element.attributes() {
599                            if self.name_matches(attribute.name(), name)? {
600                                next.push(QueryValue::Attribute {
601                                    name: attribute.name().clone(),
602                                    value: attribute.value().to_owned(),
603                                });
604                            }
605                        }
606                    }
607                }
608                QueryStep::Text => {
609                    for child in self.element_children(node_id)? {
610                        if let NodeKind::Text(value) = self.document.node(child)?.kind() {
611                            next.push(QueryValue::Text(value.clone()));
612                        }
613                    }
614                }
615            }
616        }
617        Ok(next)
618    }
619
620    fn apply_first_step(&mut self, root: NodeId, step: &QueryStep) -> XmlResult<Vec<QueryValue>> {
621        Ok(match step {
622            QueryStep::Root => vec![QueryValue::Node(root)],
623            QueryStep::Child(test) if self.node_matches(root, test)? => {
624                vec![QueryValue::Node(root)]
625            }
626            QueryStep::Descendant(test) => {
627                let mut matches = Vec::new();
628                if self.node_matches(root, test)? {
629                    matches.push(QueryValue::Node(root));
630                }
631                matches.extend(
632                    self.descendants(root)?
633                        .into_iter()
634                        .filter_map(|node| match self.node_matches(node, test) {
635                            Ok(true) => Some(Ok(QueryValue::Node(node))),
636                            Ok(false) => None,
637                            Err(error) => Some(Err(error)),
638                        })
639                        .collect::<XmlResult<Vec<_>>>()?,
640                );
641                matches
642            }
643            QueryStep::Attribute(_) | QueryStep::Text => Vec::new(),
644            QueryStep::Child(_) => Vec::new(),
645        })
646    }
647
648    fn element_children(&mut self, node_id: NodeId) -> XmlResult<Vec<NodeId>> {
649        self.bump()?;
650        match self.document.node(node_id)?.kind() {
651            NodeKind::Element(element) => Ok(element.children().to_vec()),
652            _ => Ok(Vec::new()),
653        }
654    }
655
656    fn descendants(&mut self, node_id: NodeId) -> XmlResult<Vec<NodeId>> {
657        let mut descendants = Vec::new();
658        let mut stack = self.element_children(node_id)?;
659        stack.reverse();
660        while let Some(current) = stack.pop() {
661            self.bump()?;
662            descendants.push(current);
663            let mut children = self.element_children(current)?;
664            children.reverse();
665            stack.extend(children);
666        }
667        Ok(descendants)
668    }
669
670    fn node_matches(&mut self, node_id: NodeId, test: &NodeTest) -> XmlResult<bool> {
671        self.bump()?;
672        let Some(element) = self.element(node_id)? else {
673            return Ok(false);
674        };
675        if !self.name_matches(element.name(), &test.name)? {
676            return Ok(false);
677        }
678        match &test.predicate {
679            Some(predicate) => self.predicate_matches(element, predicate),
680            None => Ok(true),
681        }
682    }
683
684    fn predicate_matches(&self, element: &ElementData, predicate: &Predicate) -> XmlResult<bool> {
685        for attribute in element.attributes() {
686            if self.name_matches(attribute.name(), &predicate.attribute)?
687                && attribute.value() == predicate.value
688            {
689                return Ok(true);
690            }
691        }
692        Ok(false)
693    }
694
695    fn name_matches(&self, name: &QName, test: &NameTest) -> XmlResult<bool> {
696        if name.local() != test.local {
697            return Ok(false);
698        }
699        match &test.prefix {
700            Some(prefix) => {
701                let uri = self.namespaces.resolve(prefix).ok_or_else(|| {
702                    XmlError::new(
703                        ErrorKind::UnknownNamespacePrefix,
704                        format!("namespace alias `{prefix}` is not declared"),
705                    )
706                })?;
707                Ok(name.namespace_uri().is_some_and(|name_uri| name_uri == uri))
708            }
709            None => Ok(name.prefix().is_none() && name.namespace_uri().is_none()),
710        }
711    }
712
713    fn element(&self, node_id: NodeId) -> XmlResult<Option<&ElementData>> {
714        Ok(match self.document.node(node_id)?.kind() {
715            NodeKind::Element(element) => Some(element),
716            _ => None,
717        })
718    }
719
720    fn bump(&mut self) -> XmlResult<()> {
721        self.steps += 1;
722        self.security.check_steps(self.steps)
723    }
724}
725
726fn query_error(source: &str, position: usize, message: impl Into<String>) -> XmlError {
727    XmlError::new(ErrorKind::Query, message).with_span(span_for_byte(source, position))
728}
729
730fn span_for_byte(source: &str, byte_position: usize) -> Span {
731    let mut line = 1;
732    let mut column = 1;
733    for (index, ch) in source.char_indices() {
734        if index >= byte_position {
735            break;
736        }
737        if ch == '\n' {
738            line += 1;
739            column = 1;
740        } else {
741            column += 1;
742        }
743    }
744    Span::new(line, column)
745}
746
747#[cfg(test)]
748mod tests {
749    use super::*;
750    use crate::core::{Attribute, NamespaceDeclaration};
751    use crate::parser;
752
753    fn sample_document() -> XmlResult<Document> {
754        let mut document = Document::new();
755        let root = document.add_root_element(QName::qualified("doc", "Root", "urn:doc")?)?;
756        document
757            .add_namespace_declaration(root, NamespaceDeclaration::prefixed("doc", "urn:doc")?)?;
758
759        let first = document.add_element(root, QName::qualified("doc", "Item", "urn:doc")?)?;
760        document.add_attribute(first, Attribute::new(QName::new("code")?, "A1"))?;
761        document.add_attribute(
762            first,
763            Attribute::new(QName::qualified("doc", "kind", "urn:doc")?, "primary"),
764        )?;
765        let first_name = document.add_element(first, QName::new("Name")?)?;
766        document.add_text(first_name, "Alpha")?;
767
768        let second = document.add_element(root, QName::qualified("doc", "Item", "urn:doc")?)?;
769        document.add_attribute(second, Attribute::new(QName::new("code")?, "B2"))?;
770        let second_name = document.add_element(second, QName::new("Name")?)?;
771        document.add_text(second_name, "Beta")?;
772
773        let note = document.add_element(root, QName::new("Note")?)?;
774        document.add_text(note, "Loose")?;
775
776        Ok(document)
777    }
778
779    fn ns() -> NamespaceContext {
780        NamespaceContext::new()
781            .with_alias("d", "urn:doc")
782            .expect("namespace alias")
783    }
784
785    #[test]
786    fn query_lexer_tokenizes_path() -> XmlResult<()> {
787        let tokens = lex("/d:Root//Name[@code='A1']/text()")?;
788
789        assert!(matches!(tokens[0].kind, TokenKind::Slash));
790        assert!(tokens
791            .iter()
792            .any(|token| matches!(token.kind, TokenKind::DoubleSlash)));
793        assert!(tokens
794            .iter()
795            .any(|token| matches!(token.kind, TokenKind::String(_))));
796        Ok(())
797    }
798
799    #[test]
800    fn query_parser_builds_absolute_path() -> XmlResult<()> {
801        let query = Query::parse("/Root/Child")?;
802
803        assert_eq!(query.steps.len(), 3);
804        assert_eq!(query.source(), "/Root/Child");
805        Ok(())
806    }
807
808    #[test]
809    fn query_evaluator_selects_absolute_path() -> XmlResult<()> {
810        let document = sample_document()?;
811        let result = document.query_with_context("/d:Root/d:Item", &ns())?;
812
813        assert_eq!(result.len(), 2);
814        Ok(())
815    }
816
817    #[test]
818    fn query_evaluator_selects_descendants() -> XmlResult<()> {
819        let document = sample_document()?;
820        let result = document.query("//Name")?;
821
822        assert_eq!(result.len(), 2);
823        Ok(())
824    }
825
826    #[test]
827    fn query_evaluator_selects_attribute() -> XmlResult<()> {
828        let document = sample_document()?;
829        let result = document.query_with_context("/d:Root/d:Item/@code", &ns())?;
830
831        assert_eq!(result.strings(), vec!["A1", "B2"]);
832        Ok(())
833    }
834
835    #[test]
836    fn query_evaluator_selects_text() -> XmlResult<()> {
837        let document = sample_document()?;
838        let result = document.query_with_context("/d:Root/d:Item/Name/text()", &ns())?;
839
840        assert_eq!(result.strings(), vec!["Alpha", "Beta"]);
841        Ok(())
842    }
843
844    #[test]
845    fn query_evaluator_filters_by_attribute_predicate() -> XmlResult<()> {
846        let document = sample_document()?;
847        let result =
848            document.query_with_context("/d:Root/d:Item[@code='A1']/Name/text()", &ns())?;
849
850        assert_eq!(result.strings(), vec!["Alpha"]);
851        Ok(())
852    }
853
854    #[test]
855    fn query_namespaces_alias_filters_by_namespaced_attribute_predicate() -> XmlResult<()> {
856        let document = sample_document()?;
857        let result =
858            document.query_with_context("/d:Root/d:Item[@d:kind='primary']/Name/text()", &ns())?;
859
860        assert_eq!(result.strings(), vec!["Alpha"]);
861        Ok(())
862    }
863
864    #[test]
865    fn query_namespaces_default_namespace_requires_alias() -> XmlResult<()> {
866        let document =
867            parser::parse_str(r#"<Root xmlns="urn:default"><Child>value</Child></Root>"#)?;
868        let namespaces = NamespaceContext::new().with_alias("d", "urn:default")?;
869
870        assert!(document.query("/Root")?.is_empty());
871        assert_eq!(
872            document
873                .query_with_context("/d:Root/d:Child/text()", &namespaces)?
874                .strings(),
875            vec!["value"]
876        );
877        Ok(())
878    }
879
880    #[test]
881    fn query_compiled_query_can_be_reused() -> XmlResult<()> {
882        let document = sample_document()?;
883        let query = Query::parse("//Name/text()")?;
884
885        assert_eq!(query.evaluate(&document)?.strings(), vec!["Alpha", "Beta"]);
886        assert_eq!(query.evaluate(&document)?.strings(), vec!["Alpha", "Beta"]);
887        Ok(())
888    }
889
890    #[test]
891    fn query_valid_cases_cover_mvp_surface() -> XmlResult<()> {
892        let document = sample_document()?;
893        let namespace = ns();
894        let cases = [
895            ("/d:Root", 1),
896            ("/d:Root/d:Item", 2),
897            ("/d:Root/Note", 1),
898            ("//d:Item", 2),
899            ("//Name", 2),
900            ("//Name/text()", 2),
901            ("/d:Root/d:Item/@code", 2),
902            ("/d:Root/d:Item[@code='B2']", 1),
903            ("/d:Root/d:Item[@code='B2']/Name/text()", 1),
904            ("/d:Root/d:Item/@d:kind", 1),
905        ];
906
907        for (source, expected_len) in cases {
908            assert_eq!(
909                document.query_with_context(source, &namespace)?.len(),
910                expected_len,
911                "query {source}"
912            );
913        }
914        Ok(())
915    }
916
917    #[test]
918    fn query_invalid_cases_have_structured_errors_with_span() {
919        let invalid = ["", "Root", "/", "/Root[", "/Root[@id]", "/Root/text("];
920
921        for source in invalid {
922            let error = Query::parse(source).expect_err("query must fail");
923            assert_eq!(error.kind(), &ErrorKind::Query);
924            assert!(error.span().is_some());
925        }
926    }
927
928    #[test]
929    fn query_namespace_alias_must_be_declared() {
930        let document = sample_document().expect("document");
931        let error = document
932            .query("/d:Root")
933            .expect_err("missing namespace alias must fail");
934
935        assert_eq!(error.kind(), &ErrorKind::UnknownNamespacePrefix);
936    }
937
938    #[test]
939    fn query_security_limits_steps() {
940        let document = sample_document().expect("document");
941        let query = Query::parse("//Name").expect("query");
942        let security = QuerySecurityConfig::default()
943            .with_limits(crate::security::SecurityLimits::default().with_max_query_steps(1));
944        let error = query
945            .evaluate_with_options(&document, &NamespaceContext::default(), &security)
946            .expect_err("query step limit must fail");
947
948        assert_eq!(error.kind(), &ErrorKind::Parse);
949    }
950}