Skip to main content

xsd_schema/xpath/functions/
mod.rs

1//! XPath 2.0 function registry and dispatch.
2//!
3//! This module provides:
4//! - `FunctionId` - Enum identifying all built-in XPath functions
5//! - `XPathValue` - Result type for function evaluation
6//! - Helper functions for argument atomization and conversion
7//! - `eval_function` - Main dispatch function
8//!
9//! ## Architecture
10//!
11//! Functions are identified by `FunctionId` which allows non-generic registry
12//! lookup at bind time. Function dispatch uses a match on `FunctionId` to call
13//! the appropriate implementation.
14
15pub mod aggregate;
16pub mod datetime;
17pub mod extensible;
18pub mod node;
19pub mod numeric;
20pub mod qname;
21pub mod regex;
22pub mod registry;
23pub mod sequence;
24pub mod signature;
25pub mod special;
26pub mod string;
27pub mod uri;
28
29pub use extensible::{
30    BuiltinCatalog, BuiltinEvaluator, CustomFn, DynamicFunctionSignature, FunctionCatalog,
31    FunctionEvaluator, FunctionHandle, FunctionSet, XPath10Catalog, XPath10Evaluator,
32};
33pub use registry::{FunctionEntry, FunctionKey, FunctionRegistry, FUNCTION_REGISTRY};
34pub use signature::{FunctionArity, FunctionSignature, FN_2010_NAMESPACE, FN_NAMESPACE};
35
36use num_bigint::BigInt;
37
38use crate::types::value::XmlValue;
39use crate::types::XmlTypeCode;
40use crate::xpath::atomize;
41use crate::xpath::error::XPathError;
42use crate::xpath::iterator::XmlItem;
43use crate::xpath::DomNavigator;
44
45use super::context::DynamicContext;
46
47/// XPath function identifiers.
48///
49/// Each variant corresponds to a built-in XPath 2.0 function.
50/// This enum allows bind-time function resolution without generic type parameters.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
52#[repr(u16)]
53pub enum FunctionId {
54    // ========== String Functions ==========
55    /// fn:concat($arg1, $arg2, ...)
56    Concat = 1,
57    /// fn:string-join($arg1, $arg2)
58    StringJoin,
59    /// fn:substring($sourceString, $start, $length?)
60    Substring,
61    /// fn:string-length($arg?)
62    StringLength,
63    /// fn:normalize-space($arg?)
64    NormalizeSpace,
65    /// fn:normalize-unicode($arg, $normalizationForm?)
66    NormalizeUnicode,
67    /// fn:upper-case($arg)
68    UpperCase,
69    /// fn:lower-case($arg)
70    LowerCase,
71    /// fn:translate($arg, $mapString, $transString)
72    Translate,
73    /// fn:encode-for-uri($uri-part)
74    EncodeForUri,
75    /// fn:iri-to-uri($iri)
76    IriToUri,
77    /// fn:escape-html-uri($uri)
78    EscapeHtmlUri,
79    /// fn:contains($arg1, $arg2, $collation?)
80    Contains,
81    /// fn:starts-with($arg1, $arg2, $collation?)
82    StartsWith,
83    /// fn:ends-with($arg1, $arg2, $collation?)
84    EndsWith,
85    /// fn:substring-before($arg1, $arg2, $collation?)
86    SubstringBefore,
87    /// fn:substring-after($arg1, $arg2, $collation?)
88    SubstringAfter,
89    /// fn:string-to-codepoints($arg)
90    StringToCodepoints,
91    /// fn:codepoints-to-string($arg)
92    CodepointsToString,
93    /// fn:compare($comparand1, $comparand2, $collation?)
94    Compare,
95    /// fn:codepoint-equal($comparand1, $comparand2)
96    CodepointEqual,
97
98    // ========== Numeric Functions ==========
99    /// fn:abs($arg)
100    Abs = 100,
101    /// fn:ceiling($arg)
102    Ceiling,
103    /// fn:floor($arg)
104    Floor,
105    /// fn:round($arg)
106    Round,
107    /// fn:round-half-to-even($arg, $precision?)
108    RoundHalfToEven,
109
110    // ========== Sequence Functions ==========
111    /// fn:empty($arg)
112    Empty = 200,
113    /// fn:exists($arg)
114    Exists,
115    /// fn:reverse($arg)
116    Reverse,
117    /// fn:index-of($seq, $search, $collation?)
118    IndexOf,
119    /// fn:remove($target, $position)
120    Remove,
121    /// fn:insert-before($target, $position, $inserts)
122    InsertBefore,
123    /// fn:subsequence($sourceSeq, $startingLoc, $length?)
124    Subsequence,
125    /// fn:unordered($sourceSeq)
126    Unordered,
127    /// fn:zero-or-one($arg)
128    ZeroOrOne,
129    /// fn:one-or-more($arg)
130    OneOrMore,
131    /// fn:exactly-one($arg)
132    ExactlyOne,
133    /// fn:distinct-values($arg, $collation?)
134    DistinctValues,
135    /// fn:deep-equal($parameter1, $parameter2, $collation?)
136    DeepEqual,
137    /// fn:count($arg)
138    Count,
139
140    // ========== Aggregate Functions ==========
141    /// fn:sum($arg, $zero?)
142    Sum = 300,
143    /// fn:avg($arg)
144    Avg,
145    /// fn:min($arg, $collation?)
146    Min,
147    /// fn:max($arg, $collation?)
148    Max,
149
150    // ========== Node Functions ==========
151    /// fn:name($arg?)
152    Name = 400,
153    /// fn:local-name($arg?)
154    LocalName,
155    /// fn:namespace-uri($arg?)
156    NamespaceUri,
157    /// fn:node-name($arg?)
158    NodeName,
159    /// fn:nilled($arg)
160    Nilled,
161    /// fn:base-uri($arg?)
162    BaseUri,
163    /// fn:document-uri($arg)
164    DocumentUri,
165    /// fn:lang($testlang, $node?)
166    Lang,
167    /// fn:root($arg?)
168    Root,
169    /// fn:id($arg as xs:string*, $node as node()) as element()*
170    Id,
171    /// fn:collection($arg as xs:string?) as node()*
172    Collection,
173
174    // ========== DateTime Functions ==========
175    /// fn:dateTime($arg1, $arg2)
176    DateTime = 500,
177    /// fn:current-dateTime()
178    CurrentDateTime,
179    /// fn:current-date()
180    CurrentDate,
181    /// fn:current-time()
182    CurrentTime,
183    /// fn:implicit-timezone()
184    ImplicitTimezone,
185    /// fn:years-from-duration($arg)
186    YearsFromDuration,
187    /// fn:months-from-duration($arg)
188    MonthsFromDuration,
189    /// fn:days-from-duration($arg)
190    DaysFromDuration,
191    /// fn:hours-from-duration($arg)
192    HoursFromDuration,
193    /// fn:minutes-from-duration($arg)
194    MinutesFromDuration,
195    /// fn:seconds-from-duration($arg)
196    SecondsFromDuration,
197    /// fn:year-from-dateTime($arg)
198    YearFromDateTime,
199    /// fn:month-from-dateTime($arg)
200    MonthFromDateTime,
201    /// fn:day-from-dateTime($arg)
202    DayFromDateTime,
203    /// fn:hours-from-dateTime($arg)
204    HoursFromDateTime,
205    /// fn:minutes-from-dateTime($arg)
206    MinutesFromDateTime,
207    /// fn:seconds-from-dateTime($arg)
208    SecondsFromDateTime,
209    /// fn:timezone-from-dateTime($arg)
210    TimezoneFromDateTime,
211    /// fn:year-from-date($arg)
212    YearFromDate,
213    /// fn:month-from-date($arg)
214    MonthFromDate,
215    /// fn:day-from-date($arg)
216    DayFromDate,
217    /// fn:timezone-from-date($arg)
218    TimezoneFromDate,
219    /// fn:hours-from-time($arg)
220    HoursFromTime,
221    /// fn:minutes-from-time($arg)
222    MinutesFromTime,
223    /// fn:seconds-from-time($arg)
224    SecondsFromTime,
225    /// fn:timezone-from-time($arg)
226    TimezoneFromTime,
227    /// fn:adjust-dateTime-to-timezone($arg, $timezone?)
228    AdjustDateTimeToTimezone,
229    /// fn:adjust-date-to-timezone($arg, $timezone?)
230    AdjustDateToTimezone,
231    /// fn:adjust-time-to-timezone($arg, $timezone?)
232    AdjustTimeToTimezone,
233
234    // ========== QName Functions ==========
235    /// fn:resolve-QName($qname, $element)
236    ResolveQName = 600,
237    /// fn:QName($paramURI, $paramLocal)
238    QName,
239    /// fn:prefix-from-QName($arg)
240    PrefixFromQName,
241    /// fn:local-name-from-QName($arg)
242    LocalNameFromQName,
243    /// fn:namespace-uri-from-QName($arg)
244    NamespaceUriFromQName,
245    /// fn:namespace-uri-for-prefix($prefix, $element)
246    NamespaceUriForPrefix,
247    /// fn:in-scope-prefixes($element)
248    InScopePrefixes,
249
250    // ========== URI Functions ==========
251    /// fn:resolve-uri($relative, $base?)
252    ResolveUri = 700,
253    /// fn:static-base-uri()
254    StaticBaseUri,
255
256    // ========== Regex Functions ==========
257    /// fn:matches($input, $pattern, $flags?)
258    Matches = 800,
259    /// fn:replace($input, $pattern, $replacement, $flags?)
260    Replace,
261    /// fn:tokenize($input, $pattern, $flags?)
262    Tokenize,
263
264    // ========== Special/Context Functions ==========
265    /// fn:position()
266    Position = 900,
267    /// fn:last()
268    Last,
269    /// fn:trace($value, $label?)
270    Trace,
271    /// fn:data($arg)
272    Data,
273    /// fn:default-collation()
274    DefaultCollation,
275
276    // ========== Boolean Functions ==========
277    /// fn:true()
278    True = 1000,
279    /// fn:false()
280    False,
281    /// fn:not($arg)
282    Not,
283    /// fn:boolean($arg)
284    Boolean,
285
286    // ========== Conversion Functions ==========
287    /// fn:string($arg?)
288    String = 1100,
289    /// fn:number($arg?)
290    Number,
291}
292
293/// XPath value representing a function result.
294///
295/// This enum represents the result of evaluating an XPath expression or function.
296/// It can be empty, a single item, or a sequence of items.
297#[derive(Debug, Clone)]
298pub enum XPathValue<N: DomNavigator> {
299    /// Empty sequence
300    Empty,
301    /// Single item (node or atomic value)
302    Item(XmlItem<N>),
303    /// Sequence of items (materialized)
304    Sequence(Vec<XmlItem<N>>),
305}
306
307impl<N: DomNavigator> XPathValue<N> {
308    /// Create an empty value
309    pub fn empty() -> Self {
310        Self::Empty
311    }
312
313    /// Create a value from a single item
314    pub fn from_item(item: XmlItem<N>) -> Self {
315        Self::Item(item)
316    }
317
318    /// Create a value from an atomic XmlValue
319    pub fn from_atomic(value: XmlValue) -> Self {
320        Self::Item(XmlItem::Atomic(value))
321    }
322
323    /// Create a value from a node
324    pub fn from_node(node: N) -> Self {
325        Self::Item(XmlItem::Node(node))
326    }
327
328    /// Create a value from a sequence of items
329    pub fn from_sequence(items: Vec<XmlItem<N>>) -> Self {
330        match items.len() {
331            0 => Self::Empty,
332            1 => Self::Item(items.into_iter().next().unwrap()),
333            _ => Self::Sequence(items),
334        }
335    }
336
337    /// Create a boolean value
338    pub fn boolean(b: bool) -> Self {
339        Self::from_atomic(XmlValue::boolean(b))
340    }
341
342    /// Create a string value
343    pub fn string(s: impl Into<String>) -> Self {
344        Self::from_atomic(XmlValue::string(s))
345    }
346
347    /// Create an integer value
348    pub fn integer(i: impl Into<num_bigint::BigInt>) -> Self {
349        Self::from_atomic(XmlValue::integer(i.into()))
350    }
351
352    /// Create a decimal value
353    pub fn decimal(d: rust_decimal::Decimal) -> Self {
354        Self::from_atomic(XmlValue::decimal(d))
355    }
356
357    /// Create a double value
358    pub fn double(d: f64) -> Self {
359        Self::from_atomic(XmlValue::double(d))
360    }
361
362    /// Check if this value is empty
363    pub fn is_empty(&self) -> bool {
364        matches!(self, Self::Empty)
365    }
366
367    /// Get the count of items
368    pub fn len(&self) -> usize {
369        match self {
370            Self::Empty => 0,
371            Self::Item(_) => 1,
372            Self::Sequence(items) => items.len(),
373        }
374    }
375
376    /// Check if this is a single item
377    pub fn is_single(&self) -> bool {
378        matches!(self, Self::Item(_))
379    }
380
381    /// Convert to a Vec of items
382    pub fn into_vec(self) -> Vec<XmlItem<N>> {
383        match self {
384            Self::Empty => Vec::new(),
385            Self::Item(item) => vec![item],
386            Self::Sequence(items) => items,
387        }
388    }
389
390    /// Get a reference to items as a slice
391    pub fn as_slice(&self) -> &[XmlItem<N>] {
392        match self {
393            Self::Empty => &[],
394            Self::Item(_) => {
395                // Can't return a slice to a single owned item safely
396                // This is a limitation - callers should use into_vec() for this case
397                &[]
398            }
399            Self::Sequence(items) => items,
400        }
401    }
402
403    /// Try to get the first item
404    pub fn first(&self) -> Option<&XmlItem<N>> {
405        match self {
406            Self::Empty => None,
407            Self::Item(item) => Some(item),
408            Self::Sequence(items) => items.first(),
409        }
410    }
411
412    // ========================================================================
413    // Atomic Value Extraction Methods
414    // ========================================================================
415
416    /// Try to extract a string from a single atomic item.
417    ///
418    /// Returns `None` if:
419    /// - The value is empty
420    /// - The value is a sequence
421    /// - The item is a node (not atomic)
422    /// - The atomic value is not a string type
423    pub fn as_str(&self) -> Option<String> {
424        match self {
425            Self::Item(XmlItem::Atomic(v)) => v.as_string().map(|s| s.to_string()),
426            _ => None,
427        }
428    }
429
430    /// Try to extract a boolean from a single atomic item.
431    ///
432    /// Returns `None` if the value is not a single atomic boolean.
433    pub fn as_bool(&self) -> Option<bool> {
434        match self {
435            Self::Item(XmlItem::Atomic(v)) => v.as_boolean(),
436            _ => None,
437        }
438    }
439
440    /// Try to extract a double from a single atomic item.
441    ///
442    /// Returns `None` if the value is not a single atomic numeric value.
443    pub fn as_f64(&self) -> Option<f64> {
444        match self {
445            Self::Item(XmlItem::Atomic(v)) => v.as_double(),
446            _ => None,
447        }
448    }
449
450    /// Try to extract an integer from a single atomic item.
451    ///
452    /// Returns `None` if the value is not a single atomic integer.
453    pub fn as_integer(&self) -> Option<num_bigint::BigInt> {
454        match self {
455            Self::Item(XmlItem::Atomic(v)) => v.as_integer().cloned(),
456            _ => None,
457        }
458    }
459}
460
461// ============================================================================
462// Helper Functions for Argument Processing
463// ============================================================================
464
465/// Atomize a value and convert to string.
466///
467/// This handles:
468/// - Empty value -> empty string
469/// - Single item -> atomized string value
470/// - Sequence -> error (XPTY0004)
471pub fn atomize_to_string<N: DomNavigator>(value: XPathValue<N>) -> Result<String, XPathError> {
472    match value {
473        XPathValue::Empty => Ok(String::new()),
474        XPathValue::Item(item) => item_to_string(item),
475        XPathValue::Sequence(items) => {
476            if items.len() == 1 {
477                item_to_string(items.into_iter().next().unwrap())
478            } else {
479                Err(XPathError::more_than_one_item())
480            }
481        }
482    }
483}
484
485/// Atomize a value and convert to required string.
486///
487/// Returns error if the value is empty or contains more than one item.
488pub fn atomize_to_string_required<N: DomNavigator>(
489    value: XPathValue<N>,
490) -> Result<String, XPathError> {
491    match value {
492        XPathValue::Empty => Err(XPathError::XPTY0004 {
493            expected: "xs:string".to_string(),
494            found: "empty-sequence()".to_string(),
495        }),
496        other => atomize_to_string(other),
497    }
498}
499
500/// Atomize a value and convert to string with strict type checking.
501///
502/// Per XPath 2.0, only xs:string, xs:untypedAtomic, and xs:anyURI can be
503/// promoted to xs:string. Other types (e.g., xs:integer) raise XPTY0004.
504/// Empty value returns empty string.
505pub fn atomize_to_string_strict<N: DomNavigator>(
506    value: XPathValue<N>,
507) -> Result<String, XPathError> {
508    match value {
509        XPathValue::Empty => Ok(String::new()),
510        XPathValue::Item(item) => item_to_string_strict(item),
511        XPathValue::Sequence(items) => {
512            if items.len() == 1 {
513                item_to_string_strict(items.into_iter().next().unwrap())
514            } else {
515                Err(XPathError::more_than_one_item())
516            }
517        }
518    }
519}
520
521/// Atomize a value and convert to optional string with strict type checking.
522///
523/// Returns None for empty sequences.
524pub fn atomize_to_string_strict_opt<N: DomNavigator>(
525    value: XPathValue<N>,
526) -> Result<Option<String>, XPathError> {
527    match value {
528        XPathValue::Empty => Ok(None),
529        other => atomize_to_string_strict(other).map(Some),
530    }
531}
532
533/// Convert an XmlItem to string with strict type checking.
534/// Only xs:string, xs:untypedAtomic, and xs:anyURI types are accepted.
535fn item_to_string_strict<N: DomNavigator>(item: XmlItem<N>) -> Result<String, XPathError> {
536    match item {
537        XmlItem::Atomic(value) => match value.type_code {
538            XmlTypeCode::String
539            | XmlTypeCode::UntypedAtomic
540            | XmlTypeCode::AnyUri
541            | XmlTypeCode::NormalizedString
542            | XmlTypeCode::Token
543            | XmlTypeCode::Language
544            | XmlTypeCode::NmToken
545            | XmlTypeCode::Name
546            | XmlTypeCode::NCName
547            | XmlTypeCode::Id
548            | XmlTypeCode::IdRef
549            | XmlTypeCode::Entity => Ok(atomize::string_value(&value)),
550            _ => Err(XPathError::XPTY0004 {
551                expected: "xs:string".to_string(),
552                found: crate::xpath::type_info::type_code_to_name(value.type_code).to_string(),
553            }),
554        },
555        XmlItem::Node(nav) => Ok(nav.value()),
556    }
557}
558
559/// Atomize a value and convert to optional string.
560///
561/// Returns None for empty sequences.
562pub fn atomize_to_string_opt<N: DomNavigator>(
563    value: XPathValue<N>,
564) -> Result<Option<String>, XPathError> {
565    match value {
566        XPathValue::Empty => Ok(None),
567        other => atomize_to_string(other).map(Some),
568    }
569}
570
571/// Convert an XmlItem to string
572fn item_to_string<N: DomNavigator>(item: XmlItem<N>) -> Result<String, XPathError> {
573    match item {
574        XmlItem::Atomic(value) => Ok(atomize::string_value(&value)),
575        XmlItem::Node(nav) => Ok(nav.value()),
576    }
577}
578
579/// Atomize a value and convert to double.
580///
581/// This handles:
582/// - Empty value -> NaN
583/// - Single item -> atomized double value
584/// - Sequence -> error (XPTY0004)
585pub fn atomize_to_double<N: DomNavigator>(value: XPathValue<N>) -> Result<f64, XPathError> {
586    match value {
587        XPathValue::Empty => Ok(f64::NAN),
588        XPathValue::Item(item) => item_to_double(item),
589        XPathValue::Sequence(items) => {
590            if items.len() == 1 {
591                item_to_double(items.into_iter().next().unwrap())
592            } else {
593                Err(XPathError::more_than_one_item())
594            }
595        }
596    }
597}
598
599/// Convert an XmlItem to double
600fn item_to_double<N: DomNavigator>(item: XmlItem<N>) -> Result<f64, XPathError> {
601    match item {
602        XmlItem::Atomic(value) => Ok(atomize::to_number(&value)),
603        XmlItem::Node(nav) => {
604            let s = nav.value();
605            Ok(s.trim().parse().unwrap_or(f64::NAN))
606        }
607    }
608}
609
610/// Atomize a value to a single XmlValue.
611///
612/// Returns error if the value is empty or contains more than one item.
613pub fn atomize_to_single<N: DomNavigator>(value: XPathValue<N>) -> Result<XmlValue, XPathError> {
614    match value {
615        XPathValue::Empty => Err(XPathError::XPTY0004 {
616            expected: "item()".to_string(),
617            found: "empty-sequence()".to_string(),
618        }),
619        XPathValue::Item(item) => item_to_atomic(item),
620        XPathValue::Sequence(items) => {
621            if items.len() == 1 {
622                item_to_atomic(items.into_iter().next().unwrap())
623            } else {
624                Err(XPathError::more_than_one_item())
625            }
626        }
627    }
628}
629
630/// Atomize a value to an optional XmlValue.
631pub fn atomize_to_single_opt<N: DomNavigator>(
632    value: XPathValue<N>,
633) -> Result<Option<XmlValue>, XPathError> {
634    match value {
635        XPathValue::Empty => Ok(None),
636        other => atomize_to_single(other).map(Some),
637    }
638}
639
640/// Convert an XmlItem to an atomic XmlValue.
641///
642/// For nodes, uses `atomize_node()` which may return `None` for nilled elements.
643/// In a single-item context, `None` is promoted to an error.
644fn item_to_atomic<N: DomNavigator>(item: XmlItem<N>) -> Result<XmlValue, XPathError> {
645    match item {
646        XmlItem::Atomic(value) => atomize::atomize(&value),
647        XmlItem::Node(nav) => atomize::atomize_node(&nav)?
648            .ok_or_else(|| XPathError::type_mismatch("item()", "empty-sequence()")),
649    }
650}
651
652/// Atomize all items in a value to a sequence of XmlValues.
653///
654/// Nilled elements (which atomize to `None`) are silently skipped.
655pub fn atomize_sequence<N: DomNavigator>(
656    value: XPathValue<N>,
657) -> Result<Vec<XmlValue>, XPathError> {
658    match value {
659        XPathValue::Empty => Ok(Vec::new()),
660        XPathValue::Item(item) => match item {
661            XmlItem::Atomic(value) => Ok(vec![atomize::atomize(&value)?]),
662            XmlItem::Node(nav) => match atomize::atomize_node(&nav)? {
663                Some(v) => Ok(vec![v]),
664                None => Ok(Vec::new()),
665            },
666        },
667        XPathValue::Sequence(items) => {
668            let mut result = Vec::with_capacity(items.len());
669            for item in items {
670                match item {
671                    XmlItem::Atomic(value) => result.push(atomize::atomize(&value)?),
672                    XmlItem::Node(nav) => {
673                        if let Some(v) = atomize::atomize_node(&nav)? {
674                            result.push(v);
675                        }
676                    }
677                }
678            }
679            Ok(result)
680        }
681    }
682}
683
684/// Materialize a value to a Vec of XmlItems.
685pub fn materialize<N: DomNavigator>(value: XPathValue<N>) -> Vec<XmlItem<N>> {
686    value.into_vec()
687}
688
689// ============================================================================
690// Function Dispatch
691// ============================================================================
692
693/// Evaluate a function by its ID.
694///
695/// This is the main dispatch function that routes to the appropriate
696/// function implementation based on the FunctionId.
697pub fn eval_function<N: DomNavigator>(
698    id: FunctionId,
699    context: &mut DynamicContext<'_, N>,
700    args: Vec<XPathValue<N>>,
701) -> Result<XPathValue<N>, XPathError> {
702    match id {
703        // ====================================================================
704        // Boolean functions
705        // ====================================================================
706        FunctionId::True => Ok(XPathValue::boolean(true)),
707        FunctionId::False => Ok(XPathValue::boolean(false)),
708        FunctionId::Not => eval_not(args),
709
710        // ====================================================================
711        // Context/Special functions
712        // ====================================================================
713        FunctionId::Position => special::position(context, args),
714        FunctionId::Last => special::last(context, args),
715        FunctionId::Trace => special::trace(context, args),
716        FunctionId::Data => special::data(context, args),
717        FunctionId::DefaultCollation => special::default_collation(context, args),
718
719        // ====================================================================
720        // Sequence functions (basic)
721        // ====================================================================
722        FunctionId::Empty => eval_empty(args),
723        FunctionId::Exists => eval_exists(args),
724        FunctionId::Count => eval_count(args),
725
726        // ====================================================================
727        // String functions (Phase 2)
728        // ====================================================================
729        FunctionId::Concat => string::concat(context, args),
730        FunctionId::StringJoin => string::string_join(context, args),
731        FunctionId::Substring => string::substring(context, args),
732        FunctionId::StringLength => string::string_length(context, args),
733        FunctionId::NormalizeSpace => string::normalize_space(context, args),
734        FunctionId::NormalizeUnicode => string::normalize_unicode(context, args),
735        FunctionId::UpperCase => string::upper_case(context, args),
736        FunctionId::LowerCase => string::lower_case(context, args),
737        FunctionId::Translate => string::translate(context, args),
738        FunctionId::EncodeForUri => string::encode_for_uri(context, args),
739        FunctionId::IriToUri => string::iri_to_uri(context, args),
740        FunctionId::EscapeHtmlUri => string::escape_html_uri(context, args),
741        FunctionId::Contains => string::contains(context, args),
742        FunctionId::StartsWith => string::starts_with(context, args),
743        FunctionId::EndsWith => string::ends_with(context, args),
744        FunctionId::SubstringBefore => string::substring_before(context, args),
745        FunctionId::SubstringAfter => string::substring_after(context, args),
746        FunctionId::StringToCodepoints => string::string_to_codepoints(context, args),
747        FunctionId::CodepointsToString => string::codepoints_to_string(context, args),
748        FunctionId::Compare => string::compare(context, args),
749        FunctionId::CodepointEqual => string::codepoint_equal(context, args),
750
751        // ====================================================================
752        // Numeric functions (Phase 3)
753        // ====================================================================
754        FunctionId::Abs => numeric::abs(context, args),
755        FunctionId::Ceiling => numeric::ceiling(context, args),
756        FunctionId::Floor => numeric::floor(context, args),
757        FunctionId::Round => numeric::round(context, args),
758        FunctionId::RoundHalfToEven => numeric::round_half_to_even(context, args),
759
760        // ====================================================================
761        // Sequence functions (Phase 3)
762        // ====================================================================
763        FunctionId::Reverse => sequence::reverse(context, args),
764        FunctionId::ZeroOrOne => sequence::zero_or_one(context, args),
765        FunctionId::OneOrMore => sequence::one_or_more(context, args),
766        FunctionId::ExactlyOne => sequence::exactly_one(context, args),
767        FunctionId::DistinctValues => sequence::distinct_values(context, args),
768        FunctionId::IndexOf => sequence::index_of(context, args),
769        FunctionId::Remove => sequence::remove(context, args),
770        FunctionId::InsertBefore => sequence::insert_before(context, args),
771        FunctionId::Subsequence => sequence::subsequence(context, args),
772        FunctionId::Unordered => sequence::unordered(context, args),
773        FunctionId::DeepEqual => sequence::deep_equal(context, args),
774
775        // ====================================================================
776        // Aggregate functions (Phase 5)
777        // ====================================================================
778        FunctionId::Sum => aggregate::sum(context, args),
779        FunctionId::Avg => aggregate::avg(context, args),
780        FunctionId::Min => aggregate::min(context, args),
781        FunctionId::Max => aggregate::max(context, args),
782
783        // ====================================================================
784        // Node functions (Phase 5)
785        // ====================================================================
786        FunctionId::Name => node::name(context, args),
787        FunctionId::LocalName => node::local_name(context, args),
788        FunctionId::NamespaceUri => node::namespace_uri(context, args),
789        FunctionId::NodeName => node::node_name(context, args),
790        FunctionId::Nilled => node::nilled(context, args),
791        FunctionId::BaseUri => node::base_uri(context, args),
792        FunctionId::DocumentUri => node::document_uri(context, args),
793        FunctionId::Lang => node::lang(context, args),
794        FunctionId::Root => node::root(context, args),
795        FunctionId::Id => node::id(context, args),
796        FunctionId::Collection => {
797            // fn:collection($arg?) — without a registered default collection
798            // or URI handler, both forms return the empty sequence per
799            // XPath/XQuery F&O §15.5.6. Sufficient for CTA tests that
800            // only probe `empty(collection())`.
801            if args.len() > 1 {
802                return Err(XPathError::wrong_number_of_arguments(
803                    "collection",
804                    1,
805                    args.len(),
806                ));
807            }
808            Ok(XPathValue::Empty)
809        }
810
811        // ====================================================================
812        // DateTime functions (Phase 6)
813        // ====================================================================
814        FunctionId::DateTime => datetime::create_datetime(context, args),
815        FunctionId::CurrentDateTime => datetime::current_datetime(context, args),
816        FunctionId::CurrentDate => datetime::current_date(context, args),
817        FunctionId::CurrentTime => datetime::current_time(context, args),
818        FunctionId::ImplicitTimezone => datetime::implicit_timezone(context, args),
819        // Duration component extraction
820        FunctionId::YearsFromDuration => datetime::years_from_duration(context, args),
821        FunctionId::MonthsFromDuration => datetime::months_from_duration(context, args),
822        FunctionId::DaysFromDuration => datetime::days_from_duration(context, args),
823        FunctionId::HoursFromDuration => datetime::hours_from_duration(context, args),
824        FunctionId::MinutesFromDuration => datetime::minutes_from_duration(context, args),
825        FunctionId::SecondsFromDuration => datetime::seconds_from_duration(context, args),
826        // DateTime component extraction
827        FunctionId::YearFromDateTime => datetime::year_from_datetime(context, args),
828        FunctionId::MonthFromDateTime => datetime::month_from_datetime(context, args),
829        FunctionId::DayFromDateTime => datetime::day_from_datetime(context, args),
830        FunctionId::HoursFromDateTime => datetime::hours_from_datetime(context, args),
831        FunctionId::MinutesFromDateTime => datetime::minutes_from_datetime(context, args),
832        FunctionId::SecondsFromDateTime => datetime::seconds_from_datetime(context, args),
833        FunctionId::TimezoneFromDateTime => datetime::timezone_from_datetime(context, args),
834        // Date component extraction
835        FunctionId::YearFromDate => datetime::year_from_date(context, args),
836        FunctionId::MonthFromDate => datetime::month_from_date(context, args),
837        FunctionId::DayFromDate => datetime::day_from_date(context, args),
838        FunctionId::TimezoneFromDate => datetime::timezone_from_date(context, args),
839        // Time component extraction
840        FunctionId::HoursFromTime => datetime::hours_from_time(context, args),
841        FunctionId::MinutesFromTime => datetime::minutes_from_time(context, args),
842        FunctionId::SecondsFromTime => datetime::seconds_from_time(context, args),
843        FunctionId::TimezoneFromTime => datetime::timezone_from_time(context, args),
844        // Timezone adjustment
845        FunctionId::AdjustDateTimeToTimezone => {
846            datetime::adjust_datetime_to_timezone(context, args)
847        }
848        FunctionId::AdjustDateToTimezone => datetime::adjust_date_to_timezone(context, args),
849        FunctionId::AdjustTimeToTimezone => datetime::adjust_time_to_timezone(context, args),
850
851        // ====================================================================
852        // QName functions (Phase 7)
853        // ====================================================================
854        FunctionId::ResolveQName => qname::resolve_qname(context, args),
855        FunctionId::QName => qname::qname_constructor(context, args),
856        FunctionId::PrefixFromQName => qname::prefix_from_qname(context, args),
857        FunctionId::LocalNameFromQName => qname::local_name_from_qname(context, args),
858        FunctionId::NamespaceUriFromQName => qname::namespace_uri_from_qname(context, args),
859        FunctionId::NamespaceUriForPrefix => qname::namespace_uri_for_prefix(context, args),
860        FunctionId::InScopePrefixes => qname::in_scope_prefixes(context, args),
861
862        // ====================================================================
863        // URI functions (Phase 7)
864        // ====================================================================
865        FunctionId::ResolveUri => uri::resolve_uri(context, args),
866        FunctionId::StaticBaseUri => uri::static_base_uri(context, args),
867
868        // ====================================================================
869        // Regex functions (Phase 7)
870        // ====================================================================
871        FunctionId::Matches => regex::matches(context, args),
872        FunctionId::Replace => regex::replace(context, args),
873        FunctionId::Tokenize => regex::tokenize(context, args),
874
875        // ====================================================================
876        // Conversion functions (fn:string, fn:number, fn:boolean)
877        // ====================================================================
878        FunctionId::String => eval_fn_string(context, args),
879        FunctionId::Number => eval_fn_number(context, args),
880        FunctionId::Boolean => eval_boolean(args),
881    }
882}
883
884// Simple function implementations for Phase 1
885
886fn eval_not<N: DomNavigator>(mut args: Vec<XPathValue<N>>) -> Result<XPathValue<N>, XPathError> {
887    if args.len() != 1 {
888        return Err(XPathError::wrong_number_of_arguments("not", 1, args.len()));
889    }
890    let arg = args.remove(0);
891    let ebv = effective_boolean_value(&arg)?;
892    Ok(XPathValue::boolean(!ebv))
893}
894
895fn eval_empty<N: DomNavigator>(mut args: Vec<XPathValue<N>>) -> Result<XPathValue<N>, XPathError> {
896    if args.len() != 1 {
897        return Err(XPathError::wrong_number_of_arguments(
898            "empty",
899            1,
900            args.len(),
901        ));
902    }
903    let arg = args.remove(0);
904    Ok(XPathValue::boolean(arg.is_empty()))
905}
906
907fn eval_exists<N: DomNavigator>(mut args: Vec<XPathValue<N>>) -> Result<XPathValue<N>, XPathError> {
908    if args.len() != 1 {
909        return Err(XPathError::wrong_number_of_arguments(
910            "exists",
911            1,
912            args.len(),
913        ));
914    }
915    let arg = args.remove(0);
916    Ok(XPathValue::boolean(!arg.is_empty()))
917}
918
919fn eval_count<N: DomNavigator>(mut args: Vec<XPathValue<N>>) -> Result<XPathValue<N>, XPathError> {
920    if args.len() != 1 {
921        return Err(XPathError::wrong_number_of_arguments(
922            "count",
923            1,
924            args.len(),
925        ));
926    }
927    let arg = args.remove(0);
928    Ok(XPathValue::integer(arg.len() as i64))
929}
930
931fn eval_fn_string<N: DomNavigator>(
932    context: &mut DynamicContext<'_, N>,
933    mut args: Vec<XPathValue<N>>,
934) -> Result<XPathValue<N>, XPathError> {
935    match args.len() {
936        0 => {
937            // 0 args: string-value of context item
938            let item = context.require_context_item()?.clone();
939            let s = match item {
940                XmlItem::Node(nav) => nav.value(),
941                XmlItem::Atomic(v) => atomize::string_value(&v),
942            };
943            Ok(XPathValue::string(s))
944        }
945        1 => {
946            let arg = args.remove(0);
947            let s = atomize_to_string(arg)?;
948            Ok(XPathValue::string(s))
949        }
950        _ => Err(XPathError::wrong_number_of_arguments(
951            "string",
952            1,
953            args.len(),
954        )),
955    }
956}
957
958fn eval_fn_number<N: DomNavigator>(
959    context: &mut DynamicContext<'_, N>,
960    mut args: Vec<XPathValue<N>>,
961) -> Result<XPathValue<N>, XPathError> {
962    match args.len() {
963        0 => {
964            // 0 args: to_number of context item
965            let item = context.require_context_item()?.clone();
966            let d = match item {
967                XmlItem::Node(nav) => {
968                    let s = nav.value();
969                    s.trim().parse().unwrap_or(f64::NAN)
970                }
971                XmlItem::Atomic(v) => atomize::to_number(&v),
972            };
973            Ok(XPathValue::double(d))
974        }
975        1 => {
976            let arg = args.remove(0);
977            let d = atomize_to_double(arg)?;
978            Ok(XPathValue::double(d))
979        }
980        _ => Err(XPathError::wrong_number_of_arguments(
981            "number",
982            1,
983            args.len(),
984        )),
985    }
986}
987
988fn eval_boolean<N: DomNavigator>(
989    mut args: Vec<XPathValue<N>>,
990) -> Result<XPathValue<N>, XPathError> {
991    if args.len() != 1 {
992        return Err(XPathError::wrong_number_of_arguments(
993            "boolean",
994            1,
995            args.len(),
996        ));
997    }
998    let arg = args.remove(0);
999    let ebv = effective_boolean_value(&arg)?;
1000    Ok(XPathValue::boolean(ebv))
1001}
1002
1003/// Compute the effective boolean value of an XPathValue.
1004pub fn effective_boolean_value<N: DomNavigator>(value: &XPathValue<N>) -> Result<bool, XPathError> {
1005    match value {
1006        XPathValue::Empty => Ok(false),
1007        XPathValue::Item(item) => item_boolean_value(item),
1008        XPathValue::Sequence(items) => {
1009            if items.is_empty() {
1010                Ok(false)
1011            } else if let Some(XmlItem::Node(_)) = items.first() {
1012                // Non-empty sequence starting with a node is true
1013                Ok(true)
1014            } else if items.len() == 1 {
1015                item_boolean_value(&items[0])
1016            } else {
1017                // Sequence of multiple atomics is an error
1018                Err(XPathError::FORG0006 {
1019                    message:
1020                        "Effective boolean value not defined for sequence of multiple atomic values"
1021                            .to_string(),
1022                })
1023            }
1024        }
1025    }
1026}
1027
1028/// Compute the effective boolean value in XPath 1.0 mode.
1029///
1030/// Differences from XPath 2.0:
1031/// - Multi-item sequences don't raise FORG0006; non-empty sequences are `true`
1032/// - Node-sets: true if non-empty
1033pub fn effective_boolean_value_10<N: DomNavigator>(
1034    value: &XPathValue<N>,
1035) -> Result<bool, XPathError> {
1036    match value {
1037        XPathValue::Empty => Ok(false),
1038        XPathValue::Item(item) => item_boolean_value(item),
1039        XPathValue::Sequence(items) => {
1040            if items.is_empty() {
1041                Ok(false)
1042            } else if items.len() == 1 {
1043                item_boolean_value(&items[0])
1044            } else {
1045                // XPath 1.0: non-empty node-set/sequence is true (no FORG0006 error)
1046                Ok(true)
1047            }
1048        }
1049    }
1050}
1051
1052fn item_boolean_value<N: DomNavigator>(item: &XmlItem<N>) -> Result<bool, XPathError> {
1053    match item {
1054        XmlItem::Node(_) => Ok(true),
1055        XmlItem::Atomic(value) => {
1056            match value.as_boolean() {
1057                Some(b) => Ok(b),
1058                None => {
1059                    // For strings (and untypedAtomic), empty is false, non-empty is true
1060                    if let Some(s) = value.as_string() {
1061                        Ok(!s.is_empty())
1062                    } else if value.type_code == crate::types::XmlTypeCode::AnyUri {
1063                        // xs:anyURI promoted to string for EBV
1064                        let s = value.to_string_value();
1065                        Ok(!s.is_empty())
1066                    } else if let Some(d) = value.as_double() {
1067                        // For numbers, 0 and NaN are false
1068                        Ok(!d.is_nan() && d != 0.0)
1069                    } else if let Some(i) = value.as_integer() {
1070                        Ok(*i != BigInt::from(0))
1071                    } else {
1072                        // Per XPath 2.0: EBV is only defined for boolean, string,
1073                        // numeric, node, untypedAtomic, anyURI. Other types raise FORG0006.
1074                        Err(XPathError::FORG0006 {
1075                            message: format!(
1076                                "Effective boolean value not defined for type {:?}",
1077                                value.type_code
1078                            ),
1079                        })
1080                    }
1081                }
1082            }
1083        }
1084    }
1085}
1086
1087// ============================================================================
1088// From Trait Implementations for XPathValue
1089// ============================================================================
1090
1091impl<N: DomNavigator> From<bool> for XPathValue<N> {
1092    fn from(b: bool) -> Self {
1093        XPathValue::boolean(b)
1094    }
1095}
1096
1097impl<N: DomNavigator> From<i32> for XPathValue<N> {
1098    fn from(i: i32) -> Self {
1099        XPathValue::integer(BigInt::from(i))
1100    }
1101}
1102
1103impl<N: DomNavigator> From<i64> for XPathValue<N> {
1104    fn from(i: i64) -> Self {
1105        XPathValue::integer(BigInt::from(i))
1106    }
1107}
1108
1109impl<N: DomNavigator> From<f64> for XPathValue<N> {
1110    fn from(d: f64) -> Self {
1111        XPathValue::double(d)
1112    }
1113}
1114
1115impl<N: DomNavigator> From<f32> for XPathValue<N> {
1116    fn from(f: f32) -> Self {
1117        XPathValue::double(f as f64)
1118    }
1119}
1120
1121impl<N: DomNavigator> From<String> for XPathValue<N> {
1122    fn from(s: String) -> Self {
1123        XPathValue::string(s)
1124    }
1125}
1126
1127impl<N: DomNavigator> From<&str> for XPathValue<N> {
1128    fn from(s: &str) -> Self {
1129        XPathValue::string(s)
1130    }
1131}
1132
1133impl<N: DomNavigator> From<()> for XPathValue<N> {
1134    fn from(_: ()) -> Self {
1135        XPathValue::empty()
1136    }
1137}
1138
1139impl<N: DomNavigator> From<BigInt> for XPathValue<N> {
1140    fn from(i: BigInt) -> Self {
1141        XPathValue::integer(i)
1142    }
1143}
1144
1145#[cfg(test)]
1146mod tests {
1147    use super::*;
1148    use crate::xpath::RoXmlNavigator;
1149
1150    #[test]
1151    fn test_xpath_value_empty() {
1152        let value: XPathValue<RoXmlNavigator<'static>> = XPathValue::empty();
1153        assert!(value.is_empty());
1154        assert_eq!(value.len(), 0);
1155    }
1156
1157    #[test]
1158    fn test_xpath_value_single() {
1159        let value: XPathValue<RoXmlNavigator<'static>> = XPathValue::boolean(true);
1160        assert!(!value.is_empty());
1161        assert_eq!(value.len(), 1);
1162        assert!(value.is_single());
1163    }
1164
1165    #[test]
1166    fn test_xpath_value_from_sequence() {
1167        let items: Vec<XmlItem<RoXmlNavigator<'static>>> = vec![
1168            XmlItem::Atomic(XmlValue::integer(1.into())),
1169            XmlItem::Atomic(XmlValue::integer(2.into())),
1170        ];
1171        let value = XPathValue::from_sequence(items);
1172        assert_eq!(value.len(), 2);
1173        assert!(!value.is_single());
1174    }
1175
1176    #[test]
1177    fn test_effective_boolean_value_empty() {
1178        let value: XPathValue<RoXmlNavigator<'static>> = XPathValue::empty();
1179        assert!(!effective_boolean_value(&value).unwrap());
1180    }
1181
1182    #[test]
1183    fn test_effective_boolean_value_boolean() {
1184        let value: XPathValue<RoXmlNavigator<'static>> = XPathValue::boolean(true);
1185        assert!(effective_boolean_value(&value).unwrap());
1186
1187        let value: XPathValue<RoXmlNavigator<'static>> = XPathValue::boolean(false);
1188        assert!(!effective_boolean_value(&value).unwrap());
1189    }
1190
1191    #[test]
1192    fn test_effective_boolean_value_string() {
1193        let value: XPathValue<RoXmlNavigator<'static>> = XPathValue::string("hello");
1194        assert!(effective_boolean_value(&value).unwrap());
1195
1196        let value: XPathValue<RoXmlNavigator<'static>> = XPathValue::string("");
1197        assert!(!effective_boolean_value(&value).unwrap());
1198    }
1199
1200    #[test]
1201    fn test_effective_boolean_value_number() {
1202        let value: XPathValue<RoXmlNavigator<'static>> = XPathValue::double(1.0);
1203        assert!(effective_boolean_value(&value).unwrap());
1204
1205        let value: XPathValue<RoXmlNavigator<'static>> = XPathValue::double(0.0);
1206        assert!(!effective_boolean_value(&value).unwrap());
1207
1208        let value: XPathValue<RoXmlNavigator<'static>> = XPathValue::double(f64::NAN);
1209        assert!(!effective_boolean_value(&value).unwrap());
1210    }
1211
1212    // ========================================================================
1213    // fn:string, fn:number, fn:boolean tests (XPath 2.0 semantics)
1214    // ========================================================================
1215
1216    use crate::namespace::table::NameTable;
1217    use crate::xpath::context::{DynamicContext, XPathContext};
1218
1219    #[test]
1220    fn test_eval_fn_string_integer() {
1221        let names = NameTable::new();
1222        let static_ctx = XPathContext::new(&names);
1223        let mut ctx: DynamicContext<'_, RoXmlNavigator<'static>> =
1224            DynamicContext::new(&static_ctx, 0);
1225        let args = vec![XPathValue::integer(42i64)];
1226        let result = eval_fn_string(&mut ctx, args).unwrap();
1227        assert_eq!(result.as_str(), Some("42".to_string()));
1228    }
1229
1230    #[test]
1231    fn test_eval_fn_string_string() {
1232        let names = NameTable::new();
1233        let static_ctx = XPathContext::new(&names);
1234        let mut ctx: DynamicContext<'_, RoXmlNavigator<'static>> =
1235            DynamicContext::new(&static_ctx, 0);
1236        let args = vec![XPathValue::string("hello")];
1237        let result = eval_fn_string(&mut ctx, args).unwrap();
1238        assert_eq!(result.as_str(), Some("hello".to_string()));
1239    }
1240
1241    #[test]
1242    fn test_eval_fn_number_string() {
1243        let names = NameTable::new();
1244        let static_ctx = XPathContext::new(&names);
1245        let mut ctx: DynamicContext<'_, RoXmlNavigator<'static>> =
1246            DynamicContext::new(&static_ctx, 0);
1247        let args = vec![XPathValue::string("42.5")];
1248        let result = eval_fn_number(&mut ctx, args).unwrap();
1249        assert_eq!(result.as_f64(), Some(42.5));
1250    }
1251
1252    #[test]
1253    fn test_eval_fn_number_invalid() {
1254        let names = NameTable::new();
1255        let static_ctx = XPathContext::new(&names);
1256        let mut ctx: DynamicContext<'_, RoXmlNavigator<'static>> =
1257            DynamicContext::new(&static_ctx, 0);
1258        let args = vec![XPathValue::string("abc")];
1259        let result = eval_fn_number(&mut ctx, args).unwrap();
1260        assert!(result.as_f64().unwrap().is_nan());
1261    }
1262
1263    #[test]
1264    fn test_eval_boolean_false_empty_string() {
1265        let args: Vec<XPathValue<RoXmlNavigator<'static>>> = vec![XPathValue::string("")];
1266        let result = eval_boolean(args).unwrap();
1267        assert_eq!(result.as_bool(), Some(false));
1268    }
1269
1270    #[test]
1271    fn test_eval_boolean_true_nonempty_string() {
1272        let args: Vec<XPathValue<RoXmlNavigator<'static>>> = vec![XPathValue::string("x")];
1273        let result = eval_boolean(args).unwrap();
1274        assert_eq!(result.as_bool(), Some(true));
1275    }
1276
1277    #[test]
1278    fn test_eval_boolean_false_zero() {
1279        let args: Vec<XPathValue<RoXmlNavigator<'static>>> = vec![XPathValue::double(0.0)];
1280        let result = eval_boolean(args).unwrap();
1281        assert_eq!(result.as_bool(), Some(false));
1282    }
1283}