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}