xsd_schema/xpath/api.rs
1//! High-level, ergonomic XPath evaluation API.
2//!
3//! This module provides a user-friendly interface for compiling and evaluating
4//! XPath expressions with Rust-idiomatic patterns:
5//!
6//! - **Separation of compilation and execution**: Compile once, evaluate many times
7//! - **Builder pattern**: Fluent API for setting variables and options
8//! - **`From` traits**: Ergonomic value conversion (use `42` instead of `XPathValue::integer(42)`)
9//! - **No lifetimes in compiled expressions**: Store `XPathExpr` anywhere
10//!
11//! # Example
12//!
13//! ```no_run
14//! use xsd_schema::xpath::api::{XPathExpr, EvalValue};
15//! use xsd_schema::xpath::{XPathContext, RoXmlNavigator};
16//! use xsd_schema::namespace::table::NameTable;
17//!
18//! // Setup context
19//! let names = NameTable::new();
20//! let ctx = XPathContext::new(&names);
21//!
22//! // Compile once with external variables
23//! let expr = XPathExpr::compile_with_vars("$x + $y", &ctx, &["x", "y"]).unwrap();
24//!
25//! // Evaluate with fluent builder
26//! let result = expr.evaluator(&ctx)
27//! .with_variable("x", 10).unwrap()
28//! .with_variable("y", 32).unwrap()
29//! .run::<RoXmlNavigator<'static>>().unwrap();
30//!
31//! // Convenience methods for common return types
32//! let sum = expr.evaluator(&ctx)
33//! .with_variable("x", 20).unwrap()
34//! .with_variable("y", 22).unwrap()
35//! .run_number::<RoXmlNavigator<'static>>().unwrap();
36//! assert_eq!(sum, 42.0);
37//! ```
38
39use num_bigint::BigInt;
40
41use crate::namespace::qname::QualifiedName;
42
43use super::arena::{AstArena, AstNodeId, SourceSpan};
44use super::bind::bind_node;
45use super::context::{DynamicContext, NameBinder, VarSlotId, XPathContext};
46use super::error::XPathError;
47use super::eval::eval_node;
48use super::functions::{effective_boolean_value, XPathValue};
49use super::iterator::XmlItem;
50use super::parser::{parse, parse_with_mode};
51use super::DomNavigator;
52
53// ============================================================================
54// ExternalVar - Information about a declared external variable
55// ============================================================================
56
57/// Information about an external variable declared at compile time.
58///
59/// External variables are those declared by the user (via `compile_with_vars`)
60/// as opposed to variables introduced by the expression itself (like `for $x in ...`).
61#[derive(Debug, Clone)]
62pub struct ExternalVar {
63 /// The qualified name of the variable
64 pub name: QualifiedName,
65 /// The slot ID for storing the variable's value
66 pub slot: VarSlotId,
67}
68
69// ============================================================================
70// XPathExpr - Compiled XPath expression (owns its AST)
71// ============================================================================
72
73/// A compiled XPath expression that can be evaluated multiple times.
74///
75/// `XPathExpr` owns its AST and contains no lifetimes, so it can be stored
76/// in structs, sent across threads (if using appropriate synchronization),
77/// or cached for repeated evaluation.
78///
79/// # Compilation vs Evaluation
80///
81/// Compilation (`compile()` or `compile_with_vars()`) parses and binds the expression,
82/// resolving function names, variable slots, and namespace prefixes. This is the
83/// expensive step.
84///
85/// Evaluation (`evaluator().run()`) executes the compiled AST, which is much faster.
86/// You can evaluate the same compiled expression many times with different variable
87/// values or context nodes.
88///
89/// # External Variables
90///
91/// Use `compile_with_vars()` to declare variables that will be provided at evaluation time:
92///
93/// ```no_run
94/// # use xsd_schema::xpath::api::XPathExpr;
95/// # use xsd_schema::xpath::XPathContext;
96/// # use xsd_schema::namespace::table::NameTable;
97/// let names = NameTable::new();
98/// let ctx = XPathContext::new(&names);
99///
100/// // Declare $x and $y as external variables
101/// let expr = XPathExpr::compile_with_vars("$x + $y", &ctx, &["x", "y"]).unwrap();
102/// ```
103#[derive(Debug, Clone)]
104pub struct XPathExpr {
105 /// Original source expression
106 source: String,
107 /// The AST arena containing all nodes
108 arena: AstArena,
109 /// Root node ID of the expression
110 root: AstNodeId,
111 /// Source span of the entire expression
112 span: SourceSpan,
113 /// Total number of variable slots needed
114 var_slots: usize,
115 /// External variables declared at compile time
116 external_vars: Vec<ExternalVar>,
117}
118
119impl XPathExpr {
120 /// Compile an XPath expression without external variables.
121 ///
122 /// Use this when your expression doesn't reference any variables, or when
123 /// all variables are provided by the expression itself (e.g., `for $x in 1 to 10`).
124 ///
125 /// # Errors
126 ///
127 /// Returns an error if:
128 /// - The expression has syntax errors
129 /// - A function is not found
130 /// - A variable is referenced but not defined
131 /// - A namespace prefix is not bound
132 ///
133 /// # Example
134 ///
135 /// ```no_run
136 /// # use xsd_schema::xpath::api::XPathExpr;
137 /// # use xsd_schema::xpath::XPathContext;
138 /// # use xsd_schema::namespace::table::NameTable;
139 /// let names = NameTable::new();
140 /// let ctx = XPathContext::new(&names);
141 /// let expr = XPathExpr::compile("1 + 2 * 3", &ctx).unwrap();
142 /// ```
143 pub fn compile(expr: &str, ctx: &XPathContext<'_>) -> Result<Self, XPathError> {
144 Self::compile_with_vars(expr, ctx, &[])
145 }
146
147 /// Compile an XPath expression with declared external variables.
148 ///
149 /// External variables must be provided at evaluation time via `with_variable()`.
150 /// Variable names should be provided without the `$` prefix.
151 ///
152 /// # Errors
153 ///
154 /// Returns an error if:
155 /// - The expression has syntax errors
156 /// - A function is not found
157 /// - A variable is referenced that wasn't declared
158 /// - A namespace prefix is not bound
159 ///
160 /// # Example
161 ///
162 /// ```no_run
163 /// # use xsd_schema::xpath::api::XPathExpr;
164 /// # use xsd_schema::xpath::XPathContext;
165 /// # use xsd_schema::namespace::table::NameTable;
166 /// let names = NameTable::new();
167 /// let ctx = XPathContext::new(&names);
168 ///
169 /// // Compile expression that uses $x and $y
170 /// let expr = XPathExpr::compile_with_vars("$x + $y", &ctx, &["x", "y"]).unwrap();
171 /// ```
172 pub fn compile_with_vars(
173 expr: &str,
174 ctx: &XPathContext<'_>,
175 vars: &[&str],
176 ) -> Result<Self, XPathError> {
177 // Parse the expression using the mode from context (ParseError → XPathError via From)
178 let parsed = if ctx.mode() == super::XPathMode::XPath20 {
179 parse(expr)?
180 } else {
181 parse_with_mode(expr, ctx.mode())?
182 };
183
184 let mut arena = parsed.arena;
185 let root = parsed.root;
186 let span = parsed.span;
187
188 // Create name binder and push external variables
189 let mut binder = NameBinder::new();
190 let mut external_vars = Vec::with_capacity(vars.len());
191
192 for var_name in vars {
193 // Parse variable name - support both "local" and "prefix:local" formats
194 let qname = parse_variable_name(var_name, ctx)?;
195
196 // Check for duplicate variable declarations
197 if external_vars.iter().any(|v: &ExternalVar| v.name == qname) {
198 return Err(XPathError::XPST0003 {
199 message: format!("Duplicate external variable declaration: ${}", var_name),
200 });
201 }
202
203 let var_ref = binder.push_var(qname.clone());
204 external_vars.push(ExternalVar {
205 name: qname,
206 slot: var_ref.slot,
207 });
208 }
209
210 // Mark boundary between external vars and expression-internal vars
211 binder.mark_external_boundary();
212
213 // Bind the expression (resolves functions, variables, namespaces)
214 bind_node(&mut arena, root, ctx, &mut binder)?;
215
216 let var_slots = binder.len();
217
218 Ok(Self {
219 source: expr.to_string(),
220 arena,
221 root,
222 span,
223 var_slots,
224 external_vars,
225 })
226 }
227
228 /// Get the original source expression.
229 pub fn source(&self) -> &str {
230 &self.source
231 }
232
233 /// Get the source span of the expression.
234 pub fn span(&self) -> SourceSpan {
235 self.span
236 }
237
238 /// Get the external variables declared for this expression.
239 pub fn external_vars(&self) -> &[ExternalVar] {
240 &self.external_vars
241 }
242
243 /// Borrow the bound AST arena of this expression.
244 ///
245 /// Useful for callers that want to inspect the compiled tree
246 /// (e.g. CTA schema-time validation walking type expressions).
247 pub fn arena(&self) -> &AstArena {
248 &self.arena
249 }
250
251 /// Create an evaluator for this expression.
252 ///
253 /// The evaluator uses a builder pattern to set variables and other options
254 /// before running the expression.
255 ///
256 /// # Example
257 ///
258 /// ```no_run
259 /// # use xsd_schema::xpath::api::XPathExpr;
260 /// # use xsd_schema::xpath::{XPathContext, RoXmlNavigator};
261 /// # use xsd_schema::namespace::table::NameTable;
262 /// let names = NameTable::new();
263 /// let ctx = XPathContext::new(&names);
264 /// let expr = XPathExpr::compile_with_vars("$x * 2", &ctx, &["x"]).unwrap();
265 ///
266 /// let result = expr.evaluator(&ctx)
267 /// .with_variable("x", 21).unwrap()
268 /// .run_number::<RoXmlNavigator<'static>>().unwrap();
269 /// assert_eq!(result, 42.0);
270 /// ```
271 pub fn evaluator<'a, 'ctx>(
272 &'a self,
273 ctx: &'ctx XPathContext<'ctx>,
274 ) -> XPathEvaluator<'a, 'ctx> {
275 XPathEvaluator::new(self, ctx)
276 }
277}
278
279// ============================================================================
280// EvalValue - Ergonomic value type for variable binding
281// ============================================================================
282
283/// A value that can be bound to an XPath variable at evaluation time.
284///
285/// This enum provides ergonomic conversion from Rust types to XPath values
286/// via the `From` trait implementations. You can use Rust literals directly:
287///
288/// ```no_run
289/// # use xsd_schema::xpath::api::{XPathExpr, EvalValue};
290/// # use xsd_schema::xpath::XPathContext;
291/// # use xsd_schema::namespace::table::NameTable;
292/// # let names = NameTable::new();
293/// # let ctx = XPathContext::new(&names);
294/// # let expr = XPathExpr::compile_with_vars("$x", &ctx, &["x"]).unwrap();
295/// // All of these work:
296/// expr.evaluator(&ctx).with_variable("x", 42); // i32 -> Integer
297/// expr.evaluator(&ctx).with_variable("x", 3.14); // f64 -> Double
298/// expr.evaluator(&ctx).with_variable("x", true); // bool -> Bool
299/// expr.evaluator(&ctx).with_variable("x", "hello"); // &str -> String
300/// ```
301#[derive(Debug, Clone)]
302pub enum EvalValue {
303 /// Boolean value
304 Bool(bool),
305 /// Small integer (converted to BigInt internally)
306 Integer(i64),
307 /// Big integer
308 BigInteger(BigInt),
309 /// Double-precision floating point
310 Double(f64),
311 /// String value
312 String(String),
313}
314
315impl From<bool> for EvalValue {
316 fn from(b: bool) -> Self {
317 EvalValue::Bool(b)
318 }
319}
320
321impl From<i32> for EvalValue {
322 fn from(i: i32) -> Self {
323 EvalValue::Integer(i as i64)
324 }
325}
326
327impl From<i64> for EvalValue {
328 fn from(i: i64) -> Self {
329 EvalValue::Integer(i)
330 }
331}
332
333impl From<f32> for EvalValue {
334 fn from(f: f32) -> Self {
335 EvalValue::Double(f as f64)
336 }
337}
338
339impl From<f64> for EvalValue {
340 fn from(f: f64) -> Self {
341 EvalValue::Double(f)
342 }
343}
344
345impl From<String> for EvalValue {
346 fn from(s: String) -> Self {
347 EvalValue::String(s)
348 }
349}
350
351impl From<&str> for EvalValue {
352 fn from(s: &str) -> Self {
353 EvalValue::String(s.to_string())
354 }
355}
356
357impl From<BigInt> for EvalValue {
358 fn from(i: BigInt) -> Self {
359 EvalValue::BigInteger(i)
360 }
361}
362
363// ============================================================================
364// PendingValue - Internal type for deferred XPathValue construction
365// ============================================================================
366
367/// Internal representation of a value waiting to be converted to XPathValue<N>.
368///
369/// Since XPathValue is generic over the navigator type N, and we don't know N
370/// until `run()` is called, we store values in this intermediate form.
371#[derive(Debug, Clone)]
372enum PendingValue {
373 Bool(bool),
374 Integer(i64),
375 BigInteger(BigInt),
376 Double(f64),
377 String(String),
378}
379
380impl PendingValue {
381 /// Convert this pending value to an XPathValue for the given navigator type.
382 fn into_xpath_value<N: DomNavigator>(self) -> XPathValue<N> {
383 match self {
384 PendingValue::Bool(b) => XPathValue::boolean(b),
385 PendingValue::Integer(i) => XPathValue::integer(BigInt::from(i)),
386 PendingValue::BigInteger(i) => XPathValue::integer(i),
387 PendingValue::Double(d) => XPathValue::double(d),
388 PendingValue::String(s) => XPathValue::string(s),
389 }
390 }
391}
392
393impl From<EvalValue> for PendingValue {
394 fn from(v: EvalValue) -> Self {
395 match v {
396 EvalValue::Bool(b) => PendingValue::Bool(b),
397 EvalValue::Integer(i) => PendingValue::Integer(i),
398 EvalValue::BigInteger(i) => PendingValue::BigInteger(i),
399 EvalValue::Double(d) => PendingValue::Double(d),
400 EvalValue::String(s) => PendingValue::String(s),
401 }
402 }
403}
404
405// ============================================================================
406// XPathEvaluator - Builder for expression evaluation
407// ============================================================================
408
409/// Builder for evaluating a compiled XPath expression.
410///
411/// Use this to set variables, context nodes, and other evaluation options
412/// before running the expression. The builder pattern allows fluent API usage:
413///
414/// ```no_run
415/// # use xsd_schema::xpath::api::XPathExpr;
416/// # use xsd_schema::xpath::{XPathContext, RoXmlNavigator};
417/// # use xsd_schema::namespace::table::NameTable;
418/// # let names = NameTable::new();
419/// # let ctx = XPathContext::new(&names);
420/// # let expr = XPathExpr::compile_with_vars("$x + $y", &ctx, &["x", "y"]).unwrap();
421/// let result = expr.evaluator(&ctx)
422/// .with_variable("x", 10).unwrap()
423/// .with_variable("y", 32).unwrap()
424/// .run::<RoXmlNavigator<'static>>().unwrap();
425/// ```
426pub struct XPathEvaluator<'expr, 'ctx> {
427 /// The compiled expression to evaluate
428 expr: &'expr XPathExpr,
429 /// The static context
430 static_ctx: &'ctx XPathContext<'ctx>,
431 /// Variables to set before evaluation (slot -> value)
432 pending_vars: Vec<(VarSlotId, PendingValue)>,
433}
434
435impl<'expr, 'ctx> XPathEvaluator<'expr, 'ctx> {
436 /// Create a new evaluator for the given expression and context.
437 fn new(expr: &'expr XPathExpr, static_ctx: &'ctx XPathContext<'ctx>) -> Self {
438 Self {
439 expr,
440 static_ctx,
441 pending_vars: Vec::new(),
442 }
443 }
444
445 /// Set an external variable's value.
446 ///
447 /// The variable must have been declared when compiling the expression
448 /// (via `compile_with_vars()`). Variable names should not include the `$` prefix.
449 ///
450 /// # Errors
451 ///
452 /// Returns `XPST0008` if the variable was not declared at compile time.
453 ///
454 /// # Example
455 ///
456 /// ```no_run
457 /// # use xsd_schema::xpath::api::XPathExpr;
458 /// # use xsd_schema::xpath::XPathContext;
459 /// # use xsd_schema::namespace::table::NameTable;
460 /// # let names = NameTable::new();
461 /// # let ctx = XPathContext::new(&names);
462 /// let expr = XPathExpr::compile_with_vars("$price * $qty", &ctx, &["price", "qty"]).unwrap();
463 ///
464 /// let eval = expr.evaluator(&ctx)
465 /// .with_variable("price", 19.99).unwrap()
466 /// .with_variable("qty", 3).unwrap();
467 /// ```
468 pub fn with_variable(
469 mut self,
470 name: &str,
471 value: impl Into<EvalValue>,
472 ) -> Result<Self, XPathError> {
473 let slot = find_external_var(name, &self.expr.external_vars, self.static_ctx)?;
474 self.pending_vars.push((slot, value.into().into()));
475 Ok(self)
476 }
477
478 /// Evaluate the expression and return the full result.
479 ///
480 /// This is the most flexible method, returning the raw `XPathValue` which
481 /// can be empty, a single item, or a sequence.
482 ///
483 /// # Type Parameter
484 ///
485 /// - `N`: The navigator type (e.g., `RoXmlNavigator<'doc>`)
486 ///
487 /// # Errors
488 ///
489 /// Returns an error if evaluation fails (e.g., type errors, undefined context).
490 pub fn run<N: DomNavigator>(self) -> Result<XPathValue<N>, XPathError> {
491 self.run_internal(None)
492 }
493
494 /// Evaluate the expression with a context node.
495 ///
496 /// Sets the context item to the given node before evaluation. This is
497 /// necessary for expressions that use `.` or axis steps like `child::*`.
498 ///
499 /// # Example
500 ///
501 /// ```no_run
502 /// # use xsd_schema::xpath::api::XPathExpr;
503 /// # use xsd_schema::xpath::{XPathContext, RoXmlNavigator, DomNavigator};
504 /// # use xsd_schema::namespace::table::NameTable;
505 /// # let names = NameTable::new();
506 /// # let ctx = XPathContext::new(&names);
507 /// let expr = XPathExpr::compile("child::item", &ctx).unwrap();
508 ///
509 /// // Parse some XML and get a navigator
510 /// let doc = roxmltree::Document::parse("<root><item/></root>").unwrap();
511 /// let mut nav = RoXmlNavigator::new(&doc);
512 /// nav.move_to_first_child(); // move to <root>
513 ///
514 /// let result = expr.evaluator(&ctx)
515 /// .run_with_node(nav).unwrap();
516 /// ```
517 pub fn run_with_node<N: DomNavigator>(self, node: N) -> Result<XPathValue<N>, XPathError> {
518 self.run_internal(Some(node))
519 }
520
521 /// Internal evaluation implementation.
522 fn run_internal<N: DomNavigator>(
523 self,
524 context_node: Option<N>,
525 ) -> Result<XPathValue<N>, XPathError> {
526 // Create dynamic context
527 let mut dyn_ctx = DynamicContext::new(self.static_ctx, self.expr.var_slots);
528
529 // Set context node if provided
530 if let Some(node) = context_node {
531 dyn_ctx = dyn_ctx.with_context_node(node);
532 }
533
534 // Set pending variables
535 for (slot, pending) in self.pending_vars {
536 let value: XPathValue<N> = pending.into_xpath_value();
537 dyn_ctx.set_variable(slot, value);
538 }
539
540 // Evaluate the expression
541 eval_node(&self.expr.arena, self.expr.root, &mut dyn_ctx)
542 }
543
544 /// Evaluate and return the result as a boolean.
545 ///
546 /// Uses the XPath effective boolean value rules:
547 /// - Empty sequence → `false`
548 /// - Boolean → its value
549 /// - String → `false` if empty, `true` otherwise
550 /// - Number → `false` if 0 or NaN, `true` otherwise
551 /// - Node sequence → `true` if non-empty
552 ///
553 /// # Errors
554 ///
555 /// Returns an error if evaluation fails or if effective boolean value
556 /// cannot be computed (e.g., sequence of multiple atomic values).
557 pub fn run_bool<N: DomNavigator>(self) -> Result<bool, XPathError> {
558 let value = self.run::<N>()?;
559 effective_boolean_value(&value)
560 }
561
562 /// Evaluate and return the result as a string.
563 ///
564 /// Atomizes the result and converts to string. For sequences, returns
565 /// the string value of the first item (or empty string for empty sequence).
566 ///
567 /// # Errors
568 ///
569 /// Returns an error if evaluation fails.
570 pub fn run_string<N: DomNavigator>(self) -> Result<String, XPathError> {
571 let value = self.run::<N>()?;
572 Ok(xpath_value_to_string(&value))
573 }
574
575 /// Evaluate and return the result as a number (f64).
576 ///
577 /// Atomizes the result and converts to double. Returns `NaN` for
578 /// values that cannot be converted to numbers.
579 ///
580 /// # Errors
581 ///
582 /// Returns an error if evaluation fails.
583 pub fn run_number<N: DomNavigator>(self) -> Result<f64, XPathError> {
584 let value = self.run::<N>()?;
585 Ok(xpath_value_to_number(&value))
586 }
587
588 /// Evaluate and return the result as a vector of nodes.
589 ///
590 /// Filters the result to include only nodes, discarding atomic values.
591 /// Useful for path expressions that return node sequences.
592 ///
593 /// # Errors
594 ///
595 /// Returns an error if evaluation fails.
596 pub fn run_nodes<N: DomNavigator>(self) -> Result<Vec<N>, XPathError> {
597 let value = self.run::<N>()?;
598 let items = value.into_vec();
599 let nodes = items
600 .into_iter()
601 .filter_map(|item| match item {
602 XmlItem::Node(n) => Some(n),
603 XmlItem::Atomic(_) => None,
604 })
605 .collect();
606 Ok(nodes)
607 }
608
609 /// Evaluate with a setup callback for advanced variable binding.
610 ///
611 /// This method allows binding variables that cannot be represented as `EvalValue`,
612 /// such as:
613 /// - Node values
614 /// - Sequences of items
615 /// - Empty sequences
616 ///
617 /// The setup callback receives a mutable reference to the `DynamicContext`
618 /// and can use `set_variable_by_name()` or `context.set_variable()` directly.
619 ///
620 /// # Example
621 ///
622 /// ```no_run
623 /// # use xsd_schema::xpath::api::XPathExpr;
624 /// # use xsd_schema::xpath::{XPathContext, RoXmlNavigator, XPathValue};
625 /// # use xsd_schema::namespace::table::NameTable;
626 /// # let names = NameTable::new();
627 /// # let ctx = XPathContext::new(&names);
628 /// let expr = XPathExpr::compile_with_vars("count($items)", &ctx, &["items"]).unwrap();
629 ///
630 /// // Bind a sequence of integers
631 /// let result = expr.evaluator(&ctx)
632 /// .run_with::<RoXmlNavigator<'static>, _>(|eval| {
633 /// // Create a sequence value
634 /// let seq = XPathValue::from_sequence(vec![
635 /// xsd_schema::xpath::XmlItem::Atomic(xsd_schema::types::XmlValue::integer(1.into())),
636 /// xsd_schema::xpath::XmlItem::Atomic(xsd_schema::types::XmlValue::integer(2.into())),
637 /// xsd_schema::xpath::XmlItem::Atomic(xsd_schema::types::XmlValue::integer(3.into())),
638 /// ]);
639 /// eval.set_variable_by_name("items", seq).unwrap();
640 /// })
641 /// .unwrap();
642 /// ```
643 pub fn run_with<N, F>(self, setup: F) -> Result<XPathValue<N>, XPathError>
644 where
645 N: DomNavigator,
646 F: for<'a> FnOnce(&mut TypedEvaluator<'_, '_, 'a, N>),
647 {
648 self.run_with_node_and_setup(None, setup)
649 }
650
651 /// Evaluate with a context node and setup callback for advanced variable binding.
652 ///
653 /// Combines `run_with_node` and `run_with` functionality.
654 pub fn run_with_node_and_setup<N, F>(
655 self,
656 context_node: Option<N>,
657 setup: F,
658 ) -> Result<XPathValue<N>, XPathError>
659 where
660 N: DomNavigator,
661 F: for<'a> FnOnce(&mut TypedEvaluator<'_, '_, 'a, N>),
662 {
663 // Create dynamic context
664 let mut dyn_ctx = DynamicContext::new(self.static_ctx, self.expr.var_slots);
665
666 // Set context node if provided
667 if let Some(node) = context_node {
668 dyn_ctx = dyn_ctx.with_context_node(node);
669 }
670
671 // Set pending variables (from with_variable calls)
672 for (slot, pending) in self.pending_vars {
673 let value: XPathValue<N> = pending.into_xpath_value();
674 dyn_ctx.set_variable(slot, value);
675 }
676
677 // Create typed evaluator and run setup callback
678 {
679 let mut typed_eval = TypedEvaluator {
680 expr: self.expr,
681 static_ctx: self.static_ctx,
682 dyn_ctx: &mut dyn_ctx,
683 };
684 setup(&mut typed_eval);
685 } // typed_eval dropped here, releasing the borrow
686
687 // Evaluate the expression
688 eval_node(&self.expr.arena, self.expr.root, &mut dyn_ctx)
689 }
690}
691
692// ============================================================================
693// TypedEvaluator - For advanced variable binding with known navigator type
694// ============================================================================
695
696/// A typed evaluator that allows binding arbitrary `XPathValue<N>` values.
697///
698/// This is used within `run_with` callbacks to set variables that cannot be
699/// represented as simple `EvalValue` (like nodes or sequences).
700pub struct TypedEvaluator<'expr, 'ctx, 'dyn_ctx, N: DomNavigator> {
701 expr: &'expr XPathExpr,
702 static_ctx: &'ctx XPathContext<'ctx>,
703 dyn_ctx: &'dyn_ctx mut DynamicContext<'ctx, N>,
704}
705
706impl<'expr, 'ctx, 'dyn_ctx, N: DomNavigator> TypedEvaluator<'expr, 'ctx, 'dyn_ctx, N> {
707 /// Set a variable by name to an arbitrary XPath value.
708 ///
709 /// This allows binding nodes, sequences, empty sequences, or any other
710 /// `XPathValue<N>` to an external variable.
711 ///
712 /// # Errors
713 ///
714 /// Returns `XPST0008` if the variable was not declared at compile time.
715 pub fn set_variable_by_name(
716 &mut self,
717 name: &str,
718 value: XPathValue<N>,
719 ) -> Result<(), XPathError> {
720 let slot = find_external_var(name, &self.expr.external_vars, self.static_ctx)?;
721 self.dyn_ctx.set_variable(slot, value);
722 Ok(())
723 }
724
725 /// Set a variable by slot ID directly.
726 ///
727 /// Use this when you already know the slot ID (e.g., from `ExternalVar::slot`).
728 pub fn set_variable(&mut self, slot: VarSlotId, value: XPathValue<N>) {
729 self.dyn_ctx.set_variable(slot, value);
730 }
731
732 /// Get a reference to the dynamic context for advanced manipulation.
733 pub fn context(&mut self) -> &mut DynamicContext<'ctx, N> {
734 &mut *self.dyn_ctx
735 }
736}
737
738// ============================================================================
739// Helper Functions
740// ============================================================================
741
742/// Parse a variable name string into a QualifiedName.
743///
744/// Supports both simple names ("x") and prefixed names ("prefix:local").
745/// The prefix must be bound in the static context.
746fn parse_variable_name(name: &str, ctx: &XPathContext<'_>) -> Result<QualifiedName, XPathError> {
747 if let Some(colon_pos) = name.find(':') {
748 // Prefixed name: "prefix:local"
749 let prefix = &name[..colon_pos];
750 let local = &name[colon_pos + 1..];
751
752 if prefix.is_empty() || local.is_empty() {
753 return Err(XPathError::XPST0003 {
754 message: format!("Invalid variable name: '{}'", name),
755 });
756 }
757
758 let prefix_id = ctx.names.add(prefix);
759 let local_id = ctx.names.add(local);
760
761 // Resolve prefix to namespace
762 let ns_id = ctx
763 .resolve_prefix_id(prefix_id)
764 .ok_or_else(|| XPathError::undefined_prefix(prefix))?;
765
766 Ok(QualifiedName::new(Some(ns_id), local_id, Some(prefix_id)))
767 } else {
768 // Simple name: "x"
769 let local_id = ctx.names.add(name);
770 Ok(QualifiedName::local(local_id))
771 }
772}
773
774/// Find an external variable by name, searching from the end to match binder resolution.
775///
776/// Returns the slot ID if found, or an error if not declared.
777fn find_external_var(
778 name: &str,
779 external_vars: &[ExternalVar],
780 ctx: &XPathContext<'_>,
781) -> Result<VarSlotId, XPathError> {
782 let qname = parse_variable_name(name, ctx)?;
783
784 // Search from the end to match the binder's last-in-first-out resolution
785 external_vars
786 .iter()
787 .rev()
788 .find(|v| v.name == qname)
789 .map(|v| v.slot)
790 .ok_or_else(|| XPathError::XPST0008 {
791 qname: format!("${}", name),
792 })
793}
794
795/// Convert an XPathValue to a string.
796fn xpath_value_to_string<N: DomNavigator>(value: &XPathValue<N>) -> String {
797 match value {
798 XPathValue::Empty => String::new(),
799 XPathValue::Item(item) => item_to_string(item),
800 XPathValue::Sequence(items) => {
801 if let Some(first) = items.first() {
802 item_to_string(first)
803 } else {
804 String::new()
805 }
806 }
807 }
808}
809
810/// Convert an XmlItem to a string.
811fn item_to_string<N: DomNavigator>(item: &XmlItem<N>) -> String {
812 match item {
813 XmlItem::Node(nav) => nav.value(),
814 XmlItem::Atomic(val) => val.to_string_value(),
815 }
816}
817
818/// Convert an XPathValue to a number.
819fn xpath_value_to_number<N: DomNavigator>(value: &XPathValue<N>) -> f64 {
820 match value {
821 XPathValue::Empty => f64::NAN,
822 XPathValue::Item(item) => item_to_number(item),
823 XPathValue::Sequence(items) => {
824 if let Some(first) = items.first() {
825 item_to_number(first)
826 } else {
827 f64::NAN
828 }
829 }
830 }
831}
832
833/// Convert an XmlItem to a number.
834fn item_to_number<N: DomNavigator>(item: &XmlItem<N>) -> f64 {
835 match item {
836 XmlItem::Node(nav) => nav.value().trim().parse().unwrap_or(f64::NAN),
837 XmlItem::Atomic(val) => {
838 if let Some(d) = val.as_double() {
839 d
840 } else if let Some(i) = val.as_integer() {
841 // Convert BigInt to f64 (may lose precision for very large numbers)
842 i.to_string().parse().unwrap_or(f64::NAN)
843 } else {
844 // Try string conversion
845 val.to_string_value().trim().parse().unwrap_or(f64::NAN)
846 }
847 }
848 }
849}
850
851// ============================================================================
852// Tests
853// ============================================================================
854
855#[cfg(test)]
856mod tests {
857 use super::*;
858 use crate::namespace::table::NameTable;
859 use crate::xpath::RoXmlNavigator;
860
861 #[test]
862 fn test_compile_simple_expression() {
863 let names = NameTable::new();
864 let ctx = XPathContext::new(&names);
865
866 let expr = XPathExpr::compile("1 + 2", &ctx);
867 assert!(expr.is_ok());
868
869 let expr = expr.unwrap();
870 assert_eq!(expr.source(), "1 + 2");
871 assert!(expr.external_vars().is_empty());
872 }
873
874 #[test]
875 fn test_compile_with_variables() {
876 let names = NameTable::new();
877 let ctx = XPathContext::new(&names);
878
879 let expr = XPathExpr::compile_with_vars("$x + $y", &ctx, &["x", "y"]);
880 assert!(expr.is_ok());
881
882 let expr = expr.unwrap();
883 assert_eq!(expr.external_vars().len(), 2);
884 }
885
886 #[test]
887 fn test_eval_simple_arithmetic() {
888 let names = NameTable::new();
889 let ctx = XPathContext::new(&names);
890
891 let expr = XPathExpr::compile("1 + 2", &ctx).unwrap();
892 let result = expr
893 .evaluator(&ctx)
894 .run_number::<RoXmlNavigator<'static>>()
895 .unwrap();
896
897 assert_eq!(result, 3.0);
898 }
899
900 #[test]
901 fn test_eval_with_variable() {
902 let names = NameTable::new();
903 let ctx = XPathContext::new(&names);
904
905 let expr = XPathExpr::compile_with_vars("$x + 1", &ctx, &["x"]).unwrap();
906 let result = expr
907 .evaluator(&ctx)
908 .with_variable("x", 41)
909 .unwrap()
910 .run_number::<RoXmlNavigator<'static>>()
911 .unwrap();
912
913 assert_eq!(result, 42.0);
914 }
915
916 #[test]
917 fn test_eval_with_string_variable() {
918 let names = NameTable::new();
919 let ctx = XPathContext::new(&names);
920
921 let expr = XPathExpr::compile_with_vars(
922 "concat($greeting, ' ', $name)",
923 &ctx,
924 &["greeting", "name"],
925 )
926 .unwrap();
927 let result = expr
928 .evaluator(&ctx)
929 .with_variable("greeting", "Hello")
930 .unwrap()
931 .with_variable("name", "World")
932 .unwrap()
933 .run_string::<RoXmlNavigator<'static>>()
934 .unwrap();
935
936 assert_eq!(result, "Hello World");
937 }
938
939 #[test]
940 fn test_eval_run_bool() {
941 let names = NameTable::new();
942 let ctx = XPathContext::new(&names);
943
944 let expr = XPathExpr::compile("1 < 2", &ctx).unwrap();
945 let result = expr
946 .evaluator(&ctx)
947 .run_bool::<RoXmlNavigator<'static>>()
948 .unwrap();
949 assert!(result);
950
951 let expr = XPathExpr::compile("2 < 1", &ctx).unwrap();
952 let result = expr
953 .evaluator(&ctx)
954 .run_bool::<RoXmlNavigator<'static>>()
955 .unwrap();
956 assert!(!result);
957 }
958
959 #[test]
960 fn test_eval_run_number() {
961 let names = NameTable::new();
962 let ctx = XPathContext::new(&names);
963
964 let expr = XPathExpr::compile("2.5", &ctx).unwrap();
965 let result = expr
966 .evaluator(&ctx)
967 .run_number::<RoXmlNavigator<'static>>()
968 .unwrap();
969 assert!((result - 2.5).abs() < 0.001);
970 }
971
972 #[test]
973 fn test_undefined_variable_error() {
974 let names = NameTable::new();
975 let ctx = XPathContext::new(&names);
976
977 // Try to compile expression with undefined variable
978 let result = XPathExpr::compile("$x", &ctx);
979 assert!(result.is_err());
980
981 if let Err(XPathError::XPST0008 { qname }) = result {
982 assert!(qname.contains("x"));
983 } else {
984 panic!("Expected XPST0008 error");
985 }
986 }
987
988 #[test]
989 fn test_setting_undeclared_variable_error() {
990 let names = NameTable::new();
991 let ctx = XPathContext::new(&names);
992
993 // Compile with only $x declared
994 let expr = XPathExpr::compile_with_vars("$x", &ctx, &["x"]).unwrap();
995
996 // Try to set undeclared variable $y
997 let result = expr
998 .evaluator(&ctx)
999 .with_variable("x", 1)
1000 .unwrap()
1001 .with_variable("y", 2); // $y was not declared
1002
1003 assert!(result.is_err());
1004 if let Err(XPathError::XPST0008 { qname }) = result {
1005 assert!(qname.contains("y"));
1006 } else {
1007 panic!("Expected XPST0008 error");
1008 }
1009 }
1010
1011 #[test]
1012 fn test_eval_with_context_node() {
1013 let names = NameTable::new();
1014 let ctx = XPathContext::new(&names);
1015
1016 let expr = XPathExpr::compile("child::item", &ctx).unwrap();
1017
1018 let doc = roxmltree::Document::parse("<root><item>value</item></root>").unwrap();
1019 let mut nav = RoXmlNavigator::new(&doc);
1020 nav.move_to_first_child(); // move to <root>
1021
1022 let result = expr.evaluator(&ctx).run_with_node(nav).unwrap();
1023 assert_eq!(result.len(), 1);
1024 }
1025
1026 #[test]
1027 fn test_expr_is_clone() {
1028 let names = NameTable::new();
1029 let ctx = XPathContext::new(&names);
1030
1031 let expr1 = XPathExpr::compile("1 + 2", &ctx).unwrap();
1032 let expr2 = expr1.clone();
1033
1034 // Both should evaluate to the same result
1035 let result1 = expr1
1036 .evaluator(&ctx)
1037 .run_number::<RoXmlNavigator<'static>>()
1038 .unwrap();
1039 let result2 = expr2
1040 .evaluator(&ctx)
1041 .run_number::<RoXmlNavigator<'static>>()
1042 .unwrap();
1043
1044 assert_eq!(result1, result2);
1045 }
1046
1047 #[test]
1048 fn test_eval_value_conversions() {
1049 // Test that all From implementations work
1050 let _: EvalValue = true.into();
1051 let _: EvalValue = 42i32.into();
1052 let _: EvalValue = 42i64.into();
1053 let _: EvalValue = 2.5f32.into();
1054 let _: EvalValue = 2.5f64.into();
1055 let _: EvalValue = "hello".into();
1056 let _: EvalValue = String::from("hello").into();
1057 let _: EvalValue = BigInt::from(1000000000000i64).into();
1058 }
1059
1060 #[test]
1061 fn test_multiple_evaluations_same_expr() {
1062 let names = NameTable::new();
1063 let ctx = XPathContext::new(&names);
1064
1065 let expr = XPathExpr::compile_with_vars("$x * 2", &ctx, &["x"]).unwrap();
1066
1067 // Evaluate multiple times with different values
1068 let result1 = expr
1069 .evaluator(&ctx)
1070 .with_variable("x", 5)
1071 .unwrap()
1072 .run_number::<RoXmlNavigator<'static>>()
1073 .unwrap();
1074
1075 let result2 = expr
1076 .evaluator(&ctx)
1077 .with_variable("x", 10)
1078 .unwrap()
1079 .run_number::<RoXmlNavigator<'static>>()
1080 .unwrap();
1081
1082 let result3 = expr
1083 .evaluator(&ctx)
1084 .with_variable("x", 21)
1085 .unwrap()
1086 .run_number::<RoXmlNavigator<'static>>()
1087 .unwrap();
1088
1089 assert_eq!(result1, 10.0);
1090 assert_eq!(result2, 20.0);
1091 assert_eq!(result3, 42.0);
1092 }
1093
1094 #[test]
1095 fn test_duplicate_variable_error() {
1096 let names = NameTable::new();
1097 let ctx = XPathContext::new(&names);
1098
1099 // Duplicate variable names should cause an error
1100 let result = XPathExpr::compile_with_vars("$x + $x", &ctx, &["x", "x"]);
1101 assert!(result.is_err());
1102
1103 if let Err(XPathError::XPST0003 { message }) = result {
1104 assert!(message.contains("Duplicate"));
1105 } else {
1106 panic!("Expected XPST0003 error for duplicate variable");
1107 }
1108 }
1109
1110 #[test]
1111 fn test_prefixed_variable() {
1112 let names = NameTable::new();
1113
1114 // Create a context with a namespace binding
1115 let my_ns = names.add("http://example.com/my");
1116 let my_prefix = names.add("my");
1117
1118 let mut namespaces = crate::namespace::context::NamespaceContextSnapshot::default();
1119 namespaces.bindings.push((my_prefix, my_ns));
1120
1121 let ctx = XPathContext::new(&names).with_namespaces(namespaces);
1122
1123 // Compile with a prefixed variable
1124 let expr = XPathExpr::compile_with_vars("$my:value + 1", &ctx, &["my:value"]).unwrap();
1125
1126 // Set the prefixed variable
1127 let result = expr
1128 .evaluator(&ctx)
1129 .with_variable("my:value", 41)
1130 .unwrap()
1131 .run_number::<RoXmlNavigator<'static>>()
1132 .unwrap();
1133
1134 assert_eq!(result, 42.0);
1135 }
1136
1137 #[test]
1138 fn test_run_with_sequence() {
1139 use crate::types::XmlValue;
1140
1141 let names = NameTable::new();
1142 let ctx = XPathContext::new(&names);
1143
1144 let expr = XPathExpr::compile_with_vars("count($items)", &ctx, &["items"]).unwrap();
1145
1146 // Use run_with to bind a sequence
1147 let result = expr
1148 .evaluator(&ctx)
1149 .run_with::<RoXmlNavigator<'static>, _>(|eval| {
1150 let seq = XPathValue::from_sequence(vec![
1151 XmlItem::Atomic(XmlValue::integer(1.into())),
1152 XmlItem::Atomic(XmlValue::integer(2.into())),
1153 XmlItem::Atomic(XmlValue::integer(3.into())),
1154 ]);
1155 eval.set_variable_by_name("items", seq).unwrap();
1156 })
1157 .unwrap();
1158
1159 // count() should return 3
1160 assert_eq!(
1161 result.as_integer().map(|i| i.to_string()),
1162 Some("3".to_string())
1163 );
1164 }
1165
1166 #[test]
1167 fn test_run_with_empty_sequence() {
1168 let names = NameTable::new();
1169 let ctx = XPathContext::new(&names);
1170
1171 let expr = XPathExpr::compile_with_vars("empty($items)", &ctx, &["items"]).unwrap();
1172
1173 // Use run_with to bind an empty sequence
1174 let result = expr
1175 .evaluator(&ctx)
1176 .run_with::<RoXmlNavigator<'static>, _>(|eval| {
1177 eval.set_variable_by_name("items", XPathValue::empty())
1178 .unwrap();
1179 })
1180 .unwrap();
1181
1182 // empty() should return true for empty sequence
1183 assert_eq!(result.as_bool(), Some(true));
1184 }
1185}