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