Skip to main content

sup_xml_xslt/
extensions.rs

1//! User-supplied extension functions for XSLT / XPath.
2//!
3//! Stylesheets can reference functions in non-XSLT namespaces (e.g.
4//! `<xsl:value-of select="my:lookup($id)"/>` where `xmlns:my="urn:app"`).
5//! The XPath engine consults the [`ExtensionFunctions`] trait when
6//! it encounters such a call; if the trait's `call` returns `Some`,
7//! the engine uses that result.  Returning `None` signals "I don't
8//! handle this function" — the engine continues its fallback chain
9//! (native EXSLT, then "unknown function" error).
10//!
11//! Two integration paths:
12//!
13//! 1. **`Extensions` builder** — register individual functions by
14//!    `(namespace, name)`.  Best for the common case of a few
15//!    short helpers:
16//!
17//!    ```no_run
18//!    # fn main() -> Result<(), Box<dyn std::error::Error>> {
19//!    use sup_xml_xslt::{Extensions, XPathValue, Stylesheet};
20//!
21//!    let mut exts = Extensions::new();
22//!    exts.register("urn:app", "double", |args| {
23//!        let n = match args.first() {
24//!            Some(XPathValue::Number(n)) => n.as_f64(),
25//!            Some(XPathValue::String(s)) => s.parse().unwrap_or(0.0),
26//!            _ => 0.0,
27//!        };
28//!        Ok(XPathValue::Number((n * 2.0).into()))
29//!    });
30//!    # let style = Stylesheet::compile_str(
31//!    #     r#"<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"/>"#)?;
32//!    # let source_doc =
33//!    #     sup_xml_core::parse_str("<r/>", &sup_xml_core::ParseOptions::default())?;
34//!    let result = style.apply_with_extensions(&source_doc, &exts)?;
35//!    # let _ = result;
36//!    # Ok(())
37//!    # }
38//!    ```
39//!
40//! 2. **Custom [`ExtensionFunctions`] impl** — for cases that need
41//!    state, dynamic dispatch over a wide function set, or
42//!    integration with an existing function table.  Implement the
43//!    trait directly on your own type.
44//!
45//! Both paths reach the same hook inside the XPath evaluator.
46
47use std::collections::HashMap;
48
49use sup_xml_core::error::XmlError;
50use sup_xml_core::xpath::XPathValue;
51
52/// Caller-supplied lookup for non-XSLT XPath functions.
53///
54/// Implement on your own type to integrate with an existing function
55/// registry, or use the [`Extensions`] builder for per-call closure
56/// registration.
57pub trait ExtensionFunctions {
58    /// Try to invoke `name` in `ns_uri` with `args`.  Return
59    /// `Some(Ok(value))` on success, `Some(Err(_))` to surface a
60    /// runtime error, or `None` if this implementation doesn't
61    /// handle the call (the engine will continue its fallback
62    /// chain — native EXSLT, then "unknown function" error).
63    fn call(
64        &self,
65        ns_uri: &str,
66        name:   &str,
67        args:   Vec<XPathValue>,
68    ) -> Option<Result<XPathValue, XmlError>>;
69}
70
71type ExtensionFn = Box<dyn Fn(Vec<XPathValue>) -> Result<XPathValue, XmlError>>;
72
73/// Ergonomic builder for the common case of registering a handful of
74/// closure-based extension functions keyed by `(namespace, name)`.
75///
76/// Lookups are O(1) and don't allocate per call (the &str argument is
77/// matched directly against the stored String keys via `Borrow`).
78#[derive(Default)]
79pub struct Extensions {
80    fns: HashMap<String, HashMap<String, ExtensionFn>>,
81}
82
83impl Extensions {
84    pub fn new() -> Self { Self::default() }
85
86    /// Register `f` to handle `ns_uri:name(...)` calls.  Re-registering
87    /// the same (ns, name) pair replaces the previous closure.
88    pub fn register<F>(
89        &mut self,
90        ns_uri: impl Into<String>,
91        name:   impl Into<String>,
92        f:      F,
93    ) -> &mut Self
94    where
95        F: Fn(Vec<XPathValue>) -> Result<XPathValue, XmlError> + 'static,
96    {
97        self.fns.entry(ns_uri.into())
98            .or_default()
99            .insert(name.into(), Box::new(f));
100        self
101    }
102}
103
104impl ExtensionFunctions for Extensions {
105    fn call(
106        &self,
107        ns_uri: &str,
108        name:   &str,
109        args:   Vec<XPathValue>,
110    ) -> Option<Result<XPathValue, XmlError>> {
111        let f = self.fns.get(ns_uri)?.get(name)?;
112        Some(f(args))
113    }
114}
115
116impl std::fmt::Debug for Extensions {
117    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118        let mut names: Vec<String> = Vec::new();
119        for (ns, m) in &self.fns {
120            for k in m.keys() {
121                names.push(format!("{{{ns}}}{k}"));
122            }
123        }
124        names.sort();
125        f.debug_struct("Extensions").field("registered", &names).finish()
126    }
127}