Skip to main content

xsd_schema/xpath/functions/
extensible.rs

1//! Extensible function support for XPath 2.0.
2//!
3//! This module provides traits and types for extending the XPath function library
4//! with custom user-defined functions.
5//!
6//! ## Architecture
7//!
8//! The extensibility system uses two main traits:
9//! - `FunctionCatalog` - Bind-time function lookup by namespace/name/arity
10//! - `FunctionEvaluator` - Eval-time function dispatch
11//!
12//! The `FunctionHandle` type provides an opaque reference to functions that works
13//! for both built-in and custom functions.
14//!
15//! ## Usage
16//!
17//! For most use cases, the default `BuiltinCatalog` and `BuiltinEvaluator` provide
18//! access to all standard XPath 2.0 functions. To add custom functions, use `FunctionSet`:
19//!
20//! ```text
21//! let mut functions = FunctionSet::with_builtins();
22//! functions.register(
23//!     DynamicFunctionSignature { ... },
24//!     |ctx, args| { /* implementation */ }
25//! );
26//! ```
27
28use std::sync::Arc;
29
30use super::signature::{FunctionArity, FunctionSignature};
31use super::{eval_function, FunctionId, XPathValue, FUNCTION_REGISTRY};
32use crate::types::sequence::SequenceType;
33use crate::xpath::context::DynamicContext;
34use crate::xpath::error::XPathError;
35use crate::xpath::DomNavigator;
36
37// ============================================================================
38// FunctionHandle - Opaque identifier for function dispatch
39// ============================================================================
40
41/// Base value for custom function handles (built-in handles use FunctionId values).
42const CUSTOM_HANDLE_BASE: u32 = 0x1000_0000;
43
44/// Opaque handle for function dispatch.
45///
46/// Replaces `FunctionId` in the AST to support both built-in and custom functions.
47/// Built-in functions use handles with values matching their `FunctionId` discriminant.
48/// Custom functions use handles starting at `CUSTOM_HANDLE_BASE`.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50pub struct FunctionHandle(pub(crate) u32);
51
52impl FunctionHandle {
53    /// Check if this handle refers to a built-in function.
54    #[inline]
55    pub fn is_builtin(&self) -> bool {
56        self.0 < CUSTOM_HANDLE_BASE
57    }
58
59    /// Check if this handle refers to a custom function.
60    #[inline]
61    pub fn is_custom(&self) -> bool {
62        self.0 >= CUSTOM_HANDLE_BASE
63    }
64
65    /// Get the custom function index (only valid for custom handles).
66    #[inline]
67    pub(crate) fn custom_index(&self) -> Option<usize> {
68        if self.is_custom() {
69            Some((self.0 - CUSTOM_HANDLE_BASE) as usize)
70        } else {
71            None
72        }
73    }
74}
75
76impl From<FunctionId> for FunctionHandle {
77    fn from(id: FunctionId) -> Self {
78        FunctionHandle(id as u32)
79    }
80}
81
82// ============================================================================
83// DynamicFunctionSignature - Owned signature for external registration
84// ============================================================================
85
86/// Function signature with owned strings for external registration.
87///
88/// Unlike `FunctionSignature` which uses `&'static str` for built-in functions,
89/// this type owns its strings, allowing dynamic registration of custom functions.
90#[derive(Debug, Clone)]
91pub struct DynamicFunctionSignature {
92    /// The function namespace URI.
93    pub namespace: Arc<str>,
94    /// The local name of the function.
95    pub local_name: Arc<str>,
96    /// The arity specification.
97    pub arity: FunctionArity,
98    /// Parameter types (may be shorter than actual args for variadic functions).
99    pub param_types: Vec<SequenceType>,
100    /// Return type.
101    pub return_type: SequenceType,
102}
103
104impl DynamicFunctionSignature {
105    /// Create a new dynamic signature with exact arity.
106    pub fn new(
107        namespace: impl Into<Arc<str>>,
108        local_name: impl Into<Arc<str>>,
109        param_types: Vec<SequenceType>,
110        return_type: SequenceType,
111    ) -> Self {
112        let arity = FunctionArity::Exact(param_types.len());
113        Self {
114            namespace: namespace.into(),
115            local_name: local_name.into(),
116            arity,
117            param_types,
118            return_type,
119        }
120    }
121
122    /// Create a variadic function signature.
123    pub fn variadic(
124        namespace: impl Into<Arc<str>>,
125        local_name: impl Into<Arc<str>>,
126        min_args: usize,
127        param_types: Vec<SequenceType>,
128        return_type: SequenceType,
129    ) -> Self {
130        Self {
131            namespace: namespace.into(),
132            local_name: local_name.into(),
133            arity: FunctionArity::Variadic(min_args),
134            param_types,
135            return_type,
136        }
137    }
138
139    /// Create a function signature with range arity.
140    pub fn range(
141        namespace: impl Into<Arc<str>>,
142        local_name: impl Into<Arc<str>>,
143        min_args: usize,
144        max_args: usize,
145        param_types: Vec<SequenceType>,
146        return_type: SequenceType,
147    ) -> Self {
148        Self {
149            namespace: namespace.into(),
150            local_name: local_name.into(),
151            arity: FunctionArity::Range(min_args, max_args),
152            param_types,
153            return_type,
154        }
155    }
156
157    /// Check if this signature matches the given arity.
158    pub fn matches_arity(&self, count: usize) -> bool {
159        self.arity.matches(count)
160    }
161}
162
163impl From<&FunctionSignature> for DynamicFunctionSignature {
164    fn from(sig: &FunctionSignature) -> Self {
165        Self {
166            namespace: Arc::from(sig.namespace),
167            local_name: Arc::from(sig.local_name),
168            arity: sig.arity,
169            param_types: sig.param_types.clone(),
170            return_type: sig.return_type.clone(),
171        }
172    }
173}
174
175// ============================================================================
176// FunctionCatalog - Bind-time function lookup trait
177// ============================================================================
178
179/// Trait for bind-time function lookup.
180///
181/// Implementors provide function resolution by namespace, local name, and arity.
182/// The returned `FunctionHandle` is stored in the AST for later evaluation.
183pub trait FunctionCatalog: std::fmt::Debug {
184    /// Look up a function by namespace URI, local name, and arity.
185    ///
186    /// Returns `Some(handle)` if a matching function is found, `None` otherwise.
187    fn lookup(&self, namespace: &str, local_name: &str, arity: usize) -> Option<FunctionHandle>;
188
189    /// Get the signature for a function handle.
190    ///
191    /// Returns `None` if the handle is invalid.
192    fn get_signature(&self, handle: FunctionHandle) -> Option<DynamicFunctionSignature>;
193}
194
195// ============================================================================
196// FunctionEvaluator - Eval-time function dispatch trait
197// ============================================================================
198
199/// Trait for eval-time function dispatch.
200///
201/// Implementors execute functions identified by `FunctionHandle`.
202pub trait FunctionEvaluator<N: DomNavigator> {
203    /// Evaluate a function with the given arguments.
204    ///
205    /// # Arguments
206    /// * `handle` - The function handle from bind-time lookup
207    /// * `ctx` - The dynamic evaluation context
208    /// * `args` - The evaluated argument values
209    ///
210    /// # Errors
211    /// Returns an error if the function execution fails.
212    fn eval(
213        &self,
214        handle: FunctionHandle,
215        ctx: &mut DynamicContext<'_, N>,
216        args: Vec<XPathValue<N>>,
217    ) -> Result<XPathValue<N>, XPathError>;
218}
219
220// ============================================================================
221// BuiltinCatalog - Static wrapper for FUNCTION_REGISTRY
222// ============================================================================
223
224/// Catalog wrapper for the static `FUNCTION_REGISTRY`.
225///
226/// Provides `FunctionCatalog` implementation using only built-in XPath functions.
227/// This is the default catalog when no custom functions are registered.
228#[derive(Debug, Clone, Copy, Default)]
229pub struct BuiltinCatalog;
230
231impl FunctionCatalog for BuiltinCatalog {
232    fn lookup(&self, namespace: &str, local_name: &str, arity: usize) -> Option<FunctionHandle> {
233        FUNCTION_REGISTRY
234            .lookup(namespace, local_name, arity)
235            .map(|entry| FunctionHandle::from(entry.id))
236    }
237
238    fn get_signature(&self, handle: FunctionHandle) -> Option<DynamicFunctionSignature> {
239        if !handle.is_builtin() {
240            return None;
241        }
242        // Convert handle back to FunctionId and look up
243        FUNCTION_REGISTRY
244            .by_id(handle_to_function_id(handle).ok()?)
245            .map(|entry| DynamicFunctionSignature::from(&entry.signature))
246    }
247}
248
249// ============================================================================
250// BuiltinEvaluator - Static wrapper for eval_function
251// ============================================================================
252
253/// Evaluator wrapper for the static `eval_function` dispatch.
254///
255/// Provides `FunctionEvaluator` implementation using only built-in XPath functions.
256/// This is the default evaluator when no custom functions are registered.
257#[derive(Debug, Clone, Copy, Default)]
258pub struct BuiltinEvaluator;
259
260impl<N: DomNavigator> FunctionEvaluator<N> for BuiltinEvaluator {
261    fn eval(
262        &self,
263        handle: FunctionHandle,
264        ctx: &mut DynamicContext<'_, N>,
265        args: Vec<XPathValue<N>>,
266    ) -> Result<XPathValue<N>, XPathError> {
267        let id = handle_to_function_id(handle)?;
268        eval_function(id, ctx, args)
269    }
270}
271
272// ============================================================================
273// Helper functions
274// ============================================================================
275
276/// Convert a FunctionHandle back to a FunctionId.
277///
278/// Returns an error if the handle is not a valid built-in function handle.
279pub(crate) fn handle_to_function_id(handle: FunctionHandle) -> Result<FunctionId, XPathError> {
280    if !handle.is_builtin() {
281        return Err(XPathError::Internal(format!(
282            "Cannot convert custom handle {:?} to FunctionId",
283            handle
284        )));
285    }
286
287    // The handle value is the FunctionId discriminant
288    // We need to find the matching FunctionId
289    let value = handle.0 as u16;
290
291    // Use the registry to find the function with matching ID
292    // This is safe because we only create builtin handles from FunctionIds
293    let entry = FUNCTION_REGISTRY
294        .by_id_value(value)
295        .ok_or_else(|| XPathError::Internal(format!("Invalid function handle: {}", value)))?;
296
297    Ok(entry.id)
298}
299
300// ============================================================================
301// FunctionSet - Combined catalog and evaluator with custom function support
302// ============================================================================
303
304use std::collections::HashMap;
305
306/// Type alias for custom function implementation.
307///
308/// Custom functions receive the dynamic context and evaluated arguments,
309/// and return an XPath value or error.
310pub type CustomFn<N> = Arc<
311    dyn Fn(&mut DynamicContext<'_, N>, Vec<XPathValue<N>>) -> Result<XPathValue<N>, XPathError>
312        + Send
313        + Sync,
314>;
315
316/// Entry for a custom function in FunctionSet.
317struct CustomFunctionEntry<N: DomNavigator> {
318    signature: DynamicFunctionSignature,
319    implementation: CustomFn<N>,
320}
321
322/// A function set that combines built-in and custom functions.
323///
324/// `FunctionSet<N>` implements both `FunctionCatalog` and `FunctionEvaluator<N>`,
325/// allowing users to register custom XPath functions alongside the built-in ones.
326///
327/// ## Example
328///
329/// ```text
330/// use xsd_schema::xpath::functions::{FunctionSet, DynamicFunctionSignature, XPathValue};
331/// use xsd_schema::types::sequence::SequenceType;
332///
333/// let mut functions = FunctionSet::with_builtins();
334///
335/// // Register a custom function
336/// let sig = DynamicFunctionSignature::new(
337///     "http://example.com/ext",
338///     "my-upper",
339///     vec![SequenceType::string()],
340///     SequenceType::string(),
341/// );
342///
343/// functions.register(sig, |_ctx, mut args| {
344///     let s = args.remove(0);
345///     // ... implementation
346///     Ok(XPathValue::string("RESULT"))
347/// });
348/// ```
349pub struct FunctionSet<N: DomNavigator> {
350    /// Custom function entries (indexed by custom handle offset)
351    custom_functions: Vec<CustomFunctionEntry<N>>,
352    /// Lookup map: (namespace, local_name, arity) -> handle
353    lookup: HashMap<(Arc<str>, Arc<str>, usize), FunctionHandle>,
354    /// Variadic lookup: (namespace, local_name) -> (handle, min_arity)
355    variadic_lookup: HashMap<(Arc<str>, Arc<str>), (FunctionHandle, usize)>,
356}
357
358impl<N: DomNavigator> FunctionSet<N> {
359    /// Create an empty function set with no built-in functions.
360    ///
361    /// Use `with_builtins()` to include standard XPath 2.0 functions.
362    pub fn new() -> Self {
363        Self {
364            custom_functions: Vec::new(),
365            lookup: HashMap::new(),
366            variadic_lookup: HashMap::new(),
367        }
368    }
369
370    /// Create a function set with all built-in XPath 2.0 functions.
371    ///
372    /// Built-in functions are looked up via the global `FUNCTION_REGISTRY`.
373    /// Custom functions registered with `register()` will take precedence
374    /// over built-in functions with the same signature.
375    pub fn with_builtins() -> Self {
376        // We don't need to populate the lookup maps with builtins;
377        // we fall back to FUNCTION_REGISTRY for those.
378        Self::new()
379    }
380
381    /// Register a custom function.
382    ///
383    /// The function will be available for lookup by its namespace, local name,
384    /// and arity. If a function with the same signature already exists (either
385    /// built-in or previously registered), the new function takes precedence.
386    ///
387    /// Returns the `FunctionHandle` for the registered function.
388    ///
389    /// ## Example
390    ///
391    /// ```text
392    /// let sig = DynamicFunctionSignature::new(
393    ///     "http://example.com/ext",
394    ///     "double",
395    ///     vec![SequenceType::double()],
396    ///     SequenceType::double(),
397    /// );
398    ///
399    /// functions.register(sig, |_ctx, mut args| {
400    ///     let val = args.remove(0);
401    ///     let d = val.as_f64().unwrap_or(0.0);
402    ///     Ok(XPathValue::double(d * 2.0))
403    /// });
404    /// ```
405    pub fn register<F>(
406        &mut self,
407        signature: DynamicFunctionSignature,
408        implementation: F,
409    ) -> FunctionHandle
410    where
411        F: Fn(&mut DynamicContext<'_, N>, Vec<XPathValue<N>>) -> Result<XPathValue<N>, XPathError>
412            + Send
413            + Sync
414            + 'static,
415    {
416        let index = self.custom_functions.len();
417        let handle = FunctionHandle(CUSTOM_HANDLE_BASE + index as u32);
418
419        // Register in lookup maps based on arity
420        let ns = signature.namespace.clone();
421        let local = signature.local_name.clone();
422
423        match signature.arity {
424            FunctionArity::Exact(n) => {
425                self.lookup.insert((ns.clone(), local.clone(), n), handle);
426            }
427            FunctionArity::Range(min, max) => {
428                for arity in min..=max {
429                    self.lookup
430                        .insert((ns.clone(), local.clone(), arity), handle);
431                }
432            }
433            FunctionArity::Variadic(min) => {
434                self.variadic_lookup
435                    .insert((ns.clone(), local.clone()), (handle, min));
436            }
437        }
438
439        // Store the entry
440        self.custom_functions.push(CustomFunctionEntry {
441            signature,
442            implementation: Arc::new(implementation),
443        });
444
445        handle
446    }
447
448    /// Get the number of custom functions registered.
449    pub fn custom_count(&self) -> usize {
450        self.custom_functions.len()
451    }
452
453    /// Check if this set has any custom functions.
454    pub fn has_custom_functions(&self) -> bool {
455        !self.custom_functions.is_empty()
456    }
457}
458
459impl<N: DomNavigator> Default for FunctionSet<N> {
460    fn default() -> Self {
461        Self::with_builtins()
462    }
463}
464
465impl<N: DomNavigator> std::fmt::Debug for FunctionSet<N> {
466    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
467        f.debug_struct("FunctionSet")
468            .field("custom_count", &self.custom_functions.len())
469            .field("lookup_count", &self.lookup.len())
470            .finish()
471    }
472}
473
474impl<N: DomNavigator> FunctionCatalog for FunctionSet<N> {
475    fn lookup(&self, namespace: &str, local_name: &str, arity: usize) -> Option<FunctionHandle> {
476        // First check custom functions (exact arity)
477        let ns: Arc<str> = Arc::from(namespace);
478        let local: Arc<str> = Arc::from(local_name);
479
480        if let Some(&handle) = self.lookup.get(&(ns.clone(), local.clone(), arity)) {
481            return Some(handle);
482        }
483
484        // Check variadic custom functions
485        if let Some(&(handle, min_arity)) = self.variadic_lookup.get(&(ns.clone(), local.clone())) {
486            if arity >= min_arity {
487                return Some(handle);
488            }
489        }
490
491        // Fall back to built-in registry
492        FUNCTION_REGISTRY
493            .lookup(namespace, local_name, arity)
494            .map(|entry| FunctionHandle::from(entry.id))
495    }
496
497    fn get_signature(&self, handle: FunctionHandle) -> Option<DynamicFunctionSignature> {
498        if let Some(index) = handle.custom_index() {
499            // Custom function
500            self.custom_functions
501                .get(index)
502                .map(|e| e.signature.clone())
503        } else {
504            // Built-in function
505            FUNCTION_REGISTRY
506                .by_id(handle_to_function_id(handle).ok()?)
507                .map(|entry| DynamicFunctionSignature::from(&entry.signature))
508        }
509    }
510}
511
512impl<N: DomNavigator> FunctionEvaluator<N> for FunctionSet<N> {
513    fn eval(
514        &self,
515        handle: FunctionHandle,
516        ctx: &mut DynamicContext<'_, N>,
517        args: Vec<XPathValue<N>>,
518    ) -> Result<XPathValue<N>, XPathError> {
519        if let Some(index) = handle.custom_index() {
520            // Custom function
521            let entry = self.custom_functions.get(index).ok_or_else(|| {
522                XPathError::Internal(format!("Invalid custom function handle: {:?}", handle))
523            })?;
524            (entry.implementation)(ctx, args)
525        } else {
526            // Built-in function
527            let id = handle_to_function_id(handle)?;
528            eval_function(id, ctx, args)
529        }
530    }
531}
532
533// ============================================================================
534// XPath10Catalog - Function catalog restricting to XPath 1.0 core functions
535// ============================================================================
536
537/// Static list of XPath 1.0 core function names (27 functions).
538const XPATH10_FUNCTIONS: &[&str] = &[
539    "last",
540    "position",
541    "count",
542    "id",
543    "name",
544    "local-name",
545    "namespace-uri",
546    "lang",
547    "string",
548    "concat",
549    "starts-with",
550    "contains",
551    "substring-before",
552    "substring-after",
553    "substring",
554    "string-length",
555    "normalize-space",
556    "translate",
557    "boolean",
558    "not",
559    "true",
560    "false",
561    "number",
562    "sum",
563    "floor",
564    "ceiling",
565    "round",
566];
567
568/// Catalog that restricts available functions to the XPath 1.0 core set.
569///
570/// For empty-namespace lookups (XPath 1.0 mode), only the 27 core functions
571/// are allowed, and they are resolved via `FUNCTION_REGISTRY` using `FN_NAMESPACE`.
572/// For non-empty namespace lookups, delegates to `BuiltinCatalog` unchanged.
573#[derive(Debug, Clone, Copy, Default)]
574pub struct XPath10Catalog;
575
576impl FunctionCatalog for XPath10Catalog {
577    fn lookup(&self, namespace: &str, local_name: &str, arity: usize) -> Option<FunctionHandle> {
578        if namespace.is_empty() {
579            // XPath 1.0 mode: only allow core 1.0 functions, resolve via FN_NAMESPACE
580            if XPATH10_FUNCTIONS.contains(&local_name) {
581                FUNCTION_REGISTRY
582                    .lookup(super::signature::FN_NAMESPACE, local_name, arity)
583                    .map(|entry| FunctionHandle::from(entry.id))
584            } else {
585                None
586            }
587        } else {
588            // Non-empty namespace: delegate to builtin catalog
589            BuiltinCatalog.lookup(namespace, local_name, arity)
590        }
591    }
592
593    fn get_signature(&self, handle: FunctionHandle) -> Option<DynamicFunctionSignature> {
594        BuiltinCatalog.get_signature(handle)
595    }
596}
597
598// ============================================================================
599// XPath10Evaluator - Evaluator applying XPath 1.0 semantics
600// ============================================================================
601
602/// Convert an XPathValue result to double if it's an integer.
603///
604/// XPath 1.0 has no integer type — all numeric results are doubles.
605fn wrap_as_double<N: DomNavigator>(result: XPathValue<N>) -> XPathValue<N> {
606    // Check integer first — as_f64() also succeeds for integers via conversion
607    if let Some(i) = result.as_integer() {
608        return XPathValue::double(i.to_string().parse::<f64>().unwrap_or(f64::NAN));
609    }
610    result
611}
612
613/// Evaluator that applies XPath 1.0 semantics to function results.
614///
615/// Intercepts specific functions to:
616/// - Use first-node-string-value rule for `fn:string`
617/// - Use first-node-to-number rule for `fn:number`
618/// - Convert integer results to double for `count`, `string-length`, `last`, `position`
619/// - Ensure `sum`, `floor`, `ceiling`, `round` return doubles
620///
621/// All other functions delegate to `BuiltinEvaluator` unchanged.
622#[derive(Debug, Clone, Copy, Default)]
623pub struct XPath10Evaluator;
624
625impl<N: DomNavigator> FunctionEvaluator<N> for XPath10Evaluator {
626    fn eval(
627        &self,
628        handle: FunctionHandle,
629        ctx: &mut DynamicContext<'_, N>,
630        args: Vec<XPathValue<N>>,
631    ) -> Result<XPathValue<N>, XPathError> {
632        let id = handle_to_function_id(handle)?;
633        match id {
634            // fn:string — use XPath 1.0 first-node rule
635            FunctionId::String => {
636                use crate::xpath::atomize;
637                match args.len() {
638                    0 => {
639                        let item = ctx.require_context_item()?.clone();
640                        let s = match item {
641                            crate::xpath::iterator::XmlItem::Node(nav) => nav.value(),
642                            crate::xpath::iterator::XmlItem::Atomic(v) => atomize::string_value(&v),
643                        };
644                        Ok(XPathValue::string(s))
645                    }
646                    1 => {
647                        let s = atomize::to_string_10(&args[0]);
648                        Ok(XPathValue::string(s))
649                    }
650                    _ => Err(XPathError::wrong_number_of_arguments(
651                        "string",
652                        1,
653                        args.len(),
654                    )),
655                }
656            }
657
658            // fn:number — use XPath 1.0 first-node rule
659            FunctionId::Number => {
660                use crate::xpath::atomize;
661                match args.len() {
662                    0 => {
663                        let item = ctx.require_context_item()?.clone();
664                        let d = match item {
665                            crate::xpath::iterator::XmlItem::Node(nav) => {
666                                let s = nav.value();
667                                s.trim().parse().unwrap_or(f64::NAN)
668                            }
669                            crate::xpath::iterator::XmlItem::Atomic(v) => atomize::to_number(&v),
670                        };
671                        Ok(XPathValue::double(d))
672                    }
673                    1 => {
674                        let d = atomize::to_number_10(&args[0]);
675                        Ok(XPathValue::double(d))
676                    }
677                    _ => Err(XPathError::wrong_number_of_arguments(
678                        "number",
679                        1,
680                        args.len(),
681                    )),
682                }
683            }
684
685            // Functions that return integer in 2.0 but should return double in 1.0
686            FunctionId::Count
687            | FunctionId::StringLength
688            | FunctionId::Last
689            | FunctionId::Position => {
690                let result = eval_function(id, ctx, args)?;
691                Ok(wrap_as_double(result))
692            }
693
694            // Numeric functions — ensure double result in 1.0
695            FunctionId::Sum | FunctionId::Floor | FunctionId::Ceiling | FunctionId::Round => {
696                let result = eval_function(id, ctx, args)?;
697                Ok(wrap_as_double(result))
698            }
699
700            // All other functions: delegate unchanged
701            _ => eval_function(id, ctx, args),
702        }
703    }
704}
705
706#[cfg(test)]
707mod tests {
708    use super::*;
709    use crate::xpath::RoXmlNavigator;
710
711    #[test]
712    fn test_function_handle_from_id() {
713        let handle = FunctionHandle::from(FunctionId::Count);
714        assert!(handle.is_builtin());
715        assert!(!handle.is_custom());
716    }
717
718    #[test]
719    fn test_custom_handle() {
720        let handle = FunctionHandle(CUSTOM_HANDLE_BASE);
721        assert!(!handle.is_builtin());
722        assert!(handle.is_custom());
723        assert_eq!(handle.custom_index(), Some(0));
724
725        let handle2 = FunctionHandle(CUSTOM_HANDLE_BASE + 5);
726        assert_eq!(handle2.custom_index(), Some(5));
727    }
728
729    #[test]
730    fn test_builtin_catalog_lookup() {
731        let catalog = BuiltinCatalog;
732        let handle = catalog.lookup("http://www.w3.org/2005/xpath-functions", "count", 1);
733        assert!(handle.is_some());
734        assert!(handle.unwrap().is_builtin());
735    }
736
737    #[test]
738    fn test_builtin_catalog_not_found() {
739        let catalog = BuiltinCatalog;
740        let handle = catalog.lookup("http://example.com", "my-func", 1);
741        assert!(handle.is_none());
742    }
743
744    #[test]
745    fn test_dynamic_signature_from_static() {
746        let catalog = BuiltinCatalog;
747        let handle = catalog
748            .lookup("http://www.w3.org/2005/xpath-functions", "count", 1)
749            .unwrap();
750        let sig = catalog.get_signature(handle);
751        assert!(sig.is_some());
752        let sig = sig.unwrap();
753        assert_eq!(&*sig.local_name, "count");
754        assert_eq!(sig.arity, FunctionArity::Exact(1));
755    }
756
757    // ========================================================================
758    // FunctionSet tests
759    // ========================================================================
760
761    #[test]
762    fn test_function_set_builtins_accessible() {
763        let functions: FunctionSet<RoXmlNavigator<'static>> = FunctionSet::with_builtins();
764
765        // Built-in function should be found
766        let handle = functions.lookup("http://www.w3.org/2005/xpath-functions", "count", 1);
767        assert!(handle.is_some());
768        assert!(handle.unwrap().is_builtin());
769    }
770
771    #[test]
772    fn test_function_set_register_custom() {
773        let mut functions: FunctionSet<RoXmlNavigator<'static>> = FunctionSet::with_builtins();
774
775        let sig = DynamicFunctionSignature::new(
776            "http://example.com/ext",
777            "my-func",
778            vec![SequenceType::string()],
779            SequenceType::string(),
780        );
781
782        let handle = functions.register(sig, |_ctx, _args| Ok(XPathValue::string("custom result")));
783
784        assert!(handle.is_custom());
785        assert_eq!(functions.custom_count(), 1);
786    }
787
788    #[test]
789    fn test_function_set_lookup_custom() {
790        let mut functions: FunctionSet<RoXmlNavigator<'static>> = FunctionSet::with_builtins();
791
792        let sig = DynamicFunctionSignature::new(
793            "http://example.com/ext",
794            "my-upper",
795            vec![SequenceType::string()],
796            SequenceType::string(),
797        );
798
799        let registered_handle = functions.register(sig, |_ctx, mut args| {
800            let s = super::super::atomize_to_string(args.remove(0))?;
801            Ok(XPathValue::string(s.to_uppercase()))
802        });
803
804        // Lookup should find our custom function
805        let found_handle = functions.lookup("http://example.com/ext", "my-upper", 1);
806        assert!(found_handle.is_some());
807        assert_eq!(found_handle.unwrap(), registered_handle);
808        assert!(found_handle.unwrap().is_custom());
809    }
810
811    #[test]
812    fn test_function_set_get_custom_signature() {
813        let mut functions: FunctionSet<RoXmlNavigator<'static>> = FunctionSet::with_builtins();
814
815        let sig = DynamicFunctionSignature::new(
816            "http://example.com/ext",
817            "test-func",
818            vec![SequenceType::integer(), SequenceType::integer()],
819            SequenceType::integer(),
820        );
821
822        let handle = functions.register(sig, |_ctx, _args| Ok(XPathValue::integer(42)));
823
824        let retrieved_sig = functions.get_signature(handle);
825        assert!(retrieved_sig.is_some());
826        let retrieved_sig = retrieved_sig.unwrap();
827        assert_eq!(&*retrieved_sig.namespace, "http://example.com/ext");
828        assert_eq!(&*retrieved_sig.local_name, "test-func");
829        assert_eq!(retrieved_sig.arity, FunctionArity::Exact(2));
830    }
831
832    #[test]
833    fn test_function_set_custom_overrides_builtin() {
834        let mut functions: FunctionSet<RoXmlNavigator<'static>> = FunctionSet::with_builtins();
835
836        // Register a custom function with the same name as a builtin
837        let sig = DynamicFunctionSignature::new(
838            "http://www.w3.org/2005/xpath-functions",
839            "count",
840            vec![SequenceType::any()],
841            SequenceType::integer(),
842        );
843
844        let custom_handle = functions.register(sig, |_ctx, _args| {
845            // Custom count always returns 999
846            Ok(XPathValue::integer(999))
847        });
848
849        // Lookup should now find the custom function
850        let found_handle = functions.lookup("http://www.w3.org/2005/xpath-functions", "count", 1);
851        assert!(found_handle.is_some());
852        assert_eq!(found_handle.unwrap(), custom_handle);
853        assert!(found_handle.unwrap().is_custom());
854    }
855
856    #[test]
857    fn test_function_set_eval_custom() {
858        use crate::namespace::table::NameTable;
859        use crate::xpath::context::{DynamicContext, XPathContext};
860
861        let mut functions: FunctionSet<RoXmlNavigator<'static>> = FunctionSet::with_builtins();
862
863        // Register a simple doubling function
864        let sig = DynamicFunctionSignature::new(
865            "http://example.com/ext",
866            "double",
867            vec![SequenceType::double()],
868            SequenceType::double(),
869        );
870
871        let handle = functions.register(sig, |_ctx, mut args| {
872            let val = args.remove(0);
873            let d = val.as_f64().unwrap_or(0.0);
874            Ok(XPathValue::double(d * 2.0))
875        });
876
877        // Create a minimal context for evaluation
878        let names = NameTable::new();
879        let static_ctx = XPathContext::new(&names);
880        let mut dyn_ctx: DynamicContext<'_, RoXmlNavigator<'static>> =
881            DynamicContext::new(&static_ctx, 0);
882
883        // Evaluate the custom function
884        let args = vec![XPathValue::double(21.0)];
885        let result = functions.eval(handle, &mut dyn_ctx, args).unwrap();
886
887        assert_eq!(result.as_f64(), Some(42.0));
888    }
889
890    #[test]
891    fn test_function_set_eval_builtin() {
892        use crate::namespace::table::NameTable;
893        use crate::types::value::XmlValue;
894        use crate::xpath::context::{DynamicContext, XPathContext};
895        use crate::xpath::iterator::XmlItem;
896
897        let functions: FunctionSet<RoXmlNavigator<'static>> = FunctionSet::with_builtins();
898
899        // Get handle for builtin count function
900        let handle = functions
901            .lookup("http://www.w3.org/2005/xpath-functions", "count", 1)
902            .unwrap();
903
904        // Create context
905        let names = NameTable::new();
906        let static_ctx = XPathContext::new(&names);
907        let mut dyn_ctx: DynamicContext<'_, RoXmlNavigator<'static>> =
908            DynamicContext::new(&static_ctx, 0);
909
910        // Create a sequence of 3 items
911        let items = vec![
912            XmlItem::Atomic(XmlValue::integer(1.into())),
913            XmlItem::Atomic(XmlValue::integer(2.into())),
914            XmlItem::Atomic(XmlValue::integer(3.into())),
915        ];
916        let args = vec![XPathValue::from_sequence(items)];
917
918        // Evaluate
919        let result = functions.eval(handle, &mut dyn_ctx, args).unwrap();
920        assert_eq!(
921            result.as_integer().map(|i| i.to_string()),
922            Some("3".to_string())
923        );
924    }
925
926    #[test]
927    fn test_function_set_range_arity() {
928        let mut functions: FunctionSet<RoXmlNavigator<'static>> = FunctionSet::with_builtins();
929
930        // Register a function with range arity (1-3 arguments)
931        let sig = DynamicFunctionSignature::range(
932            "http://example.com/ext",
933            "multi",
934            1,
935            3,
936            vec![
937                SequenceType::string(),
938                SequenceType::string(),
939                SequenceType::string(),
940            ],
941            SequenceType::string(),
942        );
943
944        let handle =
945            functions.register(sig, |_ctx, args| Ok(XPathValue::integer(args.len() as i64)));
946
947        // Should match arities 1, 2, and 3
948        assert_eq!(
949            functions.lookup("http://example.com/ext", "multi", 1),
950            Some(handle)
951        );
952        assert_eq!(
953            functions.lookup("http://example.com/ext", "multi", 2),
954            Some(handle)
955        );
956        assert_eq!(
957            functions.lookup("http://example.com/ext", "multi", 3),
958            Some(handle)
959        );
960
961        // Should not match arity 0 or 4
962        assert!(functions
963            .lookup("http://example.com/ext", "multi", 0)
964            .is_none());
965        assert!(functions
966            .lookup("http://example.com/ext", "multi", 4)
967            .is_none());
968    }
969
970    #[test]
971    fn test_function_set_variadic() {
972        let mut functions: FunctionSet<RoXmlNavigator<'static>> = FunctionSet::with_builtins();
973
974        // Register a variadic function (min 2 arguments)
975        let sig = DynamicFunctionSignature::variadic(
976            "http://example.com/ext",
977            "varargs",
978            2,
979            vec![SequenceType::any_atomic()],
980            SequenceType::integer(),
981        );
982
983        let handle =
984            functions.register(sig, |_ctx, args| Ok(XPathValue::integer(args.len() as i64)));
985
986        // Should match arities >= 2
987        assert_eq!(
988            functions.lookup("http://example.com/ext", "varargs", 2),
989            Some(handle)
990        );
991        assert_eq!(
992            functions.lookup("http://example.com/ext", "varargs", 5),
993            Some(handle)
994        );
995        assert_eq!(
996            functions.lookup("http://example.com/ext", "varargs", 100),
997            Some(handle)
998        );
999
1000        // Should not match arities < 2
1001        assert!(functions
1002            .lookup("http://example.com/ext", "varargs", 0)
1003            .is_none());
1004        assert!(functions
1005            .lookup("http://example.com/ext", "varargs", 1)
1006            .is_none());
1007    }
1008
1009    // ========================================================================
1010    // XPath10Catalog tests
1011    // ========================================================================
1012
1013    #[test]
1014    fn test_xpath10_catalog_resolves_core_function() {
1015        let catalog = XPath10Catalog;
1016        // count is in the 1.0 core set — empty namespace should resolve
1017        let handle = catalog.lookup("", "count", 1);
1018        assert!(handle.is_some());
1019        assert!(handle.unwrap().is_builtin());
1020    }
1021
1022    #[test]
1023    fn test_xpath10_catalog_rejects_non_core_function() {
1024        let catalog = XPath10Catalog;
1025        // deep-equal is XPath 2.0 only — should be rejected in empty namespace
1026        let handle = catalog.lookup("", "deep-equal", 2);
1027        assert!(handle.is_none());
1028    }
1029
1030    #[test]
1031    fn test_xpath10_catalog_non_empty_ns_delegates() {
1032        let catalog = XPath10Catalog;
1033        // Non-empty namespace delegates to BuiltinCatalog unchanged
1034        let handle = catalog.lookup("http://www.w3.org/2005/xpath-functions", "count", 1);
1035        assert!(handle.is_some());
1036    }
1037
1038    #[test]
1039    fn test_xpath10_catalog_all_core_functions() {
1040        let catalog = XPath10Catalog;
1041        // Verify all 27 core functions resolve
1042        let functions_with_arity: &[(&str, usize)] = &[
1043            ("last", 0),
1044            ("position", 0),
1045            ("count", 1),
1046            ("id", 1),
1047            ("name", 0),
1048            ("local-name", 0),
1049            ("namespace-uri", 0),
1050            ("lang", 1),
1051            ("string", 0),
1052            ("concat", 2),
1053            ("starts-with", 2),
1054            ("contains", 2),
1055            ("substring-before", 2),
1056            ("substring-after", 2),
1057            ("substring", 2),
1058            ("string-length", 0),
1059            ("normalize-space", 0),
1060            ("translate", 3),
1061            ("boolean", 1),
1062            ("not", 1),
1063            ("true", 0),
1064            ("false", 0),
1065            ("number", 0),
1066            ("sum", 1),
1067            ("floor", 1),
1068            ("ceiling", 1),
1069            ("round", 1),
1070        ];
1071        for (name, arity) in functions_with_arity {
1072            let handle = catalog.lookup("", name, *arity);
1073            assert!(
1074                handle.is_some(),
1075                "XPath 1.0 function '{}' with arity {} not found",
1076                name,
1077                arity
1078            );
1079        }
1080    }
1081
1082    // ========================================================================
1083    // XPath10Evaluator tests
1084    // ========================================================================
1085
1086    #[test]
1087    fn test_xpath10_evaluator_count_returns_double() {
1088        use crate::namespace::table::NameTable;
1089        use crate::types::value::XmlValue;
1090        use crate::xpath::context::{DynamicContext, XPathContext};
1091        use crate::xpath::iterator::XmlItem;
1092
1093        let evaluator = XPath10Evaluator;
1094
1095        let names = NameTable::new();
1096        let static_ctx = XPathContext::new(&names);
1097        let mut dyn_ctx: DynamicContext<'_, RoXmlNavigator<'static>> =
1098            DynamicContext::new(&static_ctx, 0);
1099
1100        // Create a sequence of 3 items
1101        let items = vec![
1102            XmlItem::Atomic(XmlValue::integer(1.into())),
1103            XmlItem::Atomic(XmlValue::integer(2.into())),
1104            XmlItem::Atomic(XmlValue::integer(3.into())),
1105        ];
1106        let args = vec![XPathValue::from_sequence(items)];
1107        let handle = FunctionHandle::from(FunctionId::Count);
1108
1109        let result = evaluator.eval(handle, &mut dyn_ctx, args).unwrap();
1110        // In XPath 1.0, count() should return double, not integer
1111        assert_eq!(result.as_f64(), Some(3.0));
1112        // Should NOT be an integer
1113        assert!(result.as_integer().is_none());
1114    }
1115
1116    #[test]
1117    fn test_xpath10_evaluator_string_with_node() {
1118        let evaluator = XPath10Evaluator;
1119
1120        let doc = roxmltree::Document::parse("<r><a>first</a><b>second</b></r>").unwrap();
1121        let mut nav_a = RoXmlNavigator::new(&doc);
1122        nav_a.move_to_first_child(); // <r>
1123        nav_a.move_to_first_child(); // <a>
1124        let mut nav_b = nav_a.clone();
1125        nav_b.move_to_next_sibling(); // <b>
1126
1127        use crate::namespace::table::NameTable;
1128        use crate::xpath::context::{DynamicContext, XPathContext};
1129        use crate::xpath::iterator::XmlItem;
1130
1131        let names = NameTable::new();
1132        let static_ctx = XPathContext::new(&names);
1133        let mut dyn_ctx = DynamicContext::new(&static_ctx, 0);
1134
1135        // Sequence of two nodes — XPath 1.0 string() should use first node
1136        let node_seq = XPathValue::from_sequence(vec![XmlItem::Node(nav_a), XmlItem::Node(nav_b)]);
1137        let args = vec![node_seq];
1138        let handle = FunctionHandle::from(FunctionId::String);
1139
1140        let result = evaluator.eval(handle, &mut dyn_ctx, args).unwrap();
1141        assert_eq!(result.as_str(), Some("first".to_string()));
1142    }
1143}