Skip to main content

oxilite_core/
functions.rs

1//! Host functions: user code callable from SPARQL, Cypher and Datalog.
2//!
3//! A host function maps RDF terms to an optional RDF term, the signature of spareval's custom
4//! functions: `None` is an evaluation error, which SPARQL turns into an unbound value. Functions
5//! are registered on a store under an IRI (plus an optional Cypher name) and are never
6//! persisted: they are code, not data. Each frontend decides where the call runs — SPARQL and
7//! Cypher through the fallback evaluator, Datalog in a Rust pass over a rule's rows — and every
8//! part of a query that does not call one still compiles to SQL.
9//!
10// @lat: [[architecture#Host functions]]
11
12use crate::error::{Error, Result};
13use oxrdf::{NamedNode, Term};
14use std::collections::BTreeMap;
15use std::sync::Arc;
16
17/// The callable of a host function.
18pub type HostFn = Arc<dyn Fn(&[Term]) -> Option<Term> + Send + Sync>;
19
20/// A function implemented by the application.
21#[derive(Clone)]
22pub struct HostFunction {
23    iri: String,
24    cypher_name: Option<String>,
25    min_arity: usize,
26    max_arity: Option<usize>,
27    description: String,
28    f: HostFn,
29}
30
31impl std::fmt::Debug for HostFunction {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        f.debug_struct("HostFunction")
34            .field("iri", &self.iri)
35            .field("cypher_name", &self.cypher_name)
36            .field("min_arity", &self.min_arity)
37            .field("max_arity", &self.max_arity)
38            .finish_non_exhaustive()
39    }
40}
41
42impl HostFunction {
43    /// A function named by `iri` (checked when it is registered).
44    pub fn new(
45        iri: impl Into<String>,
46        f: impl Fn(&[Term]) -> Option<Term> + Send + Sync + 'static,
47    ) -> Self {
48        Self {
49            iri: iri.into(),
50            cypher_name: None,
51            min_arity: 0,
52            max_arity: None,
53            description: String::new(),
54            f: Arc::new(f),
55        }
56    }
57
58    /// The name Cypher calls it by (default: the local name of the IRI).
59    pub fn cypher_name(mut self, name: impl Into<String>) -> Self {
60        self.cypher_name = Some(name.into());
61        self
62    }
63
64    /// How many arguments it takes; a call with another number is an evaluation error.
65    pub fn arity(mut self, min: usize, max: impl Into<Option<usize>>) -> Self {
66        self.min_arity = min;
67        self.max_arity = max.into();
68        self
69    }
70
71    /// A one-line description, shown by tools (completion, `.functions`).
72    pub fn description(mut self, text: impl Into<String>) -> Self {
73        self.description = text.into();
74        self
75    }
76
77    pub fn iri(&self) -> &str {
78        &self.iri
79    }
80
81    /// The name Cypher calls it by: the explicit one, else the IRI's local name.
82    pub fn cypher(&self) -> &str {
83        self.cypher_name
84            .as_deref()
85            .unwrap_or_else(|| local_name(&self.iri))
86    }
87
88    pub fn min_arity(&self) -> usize {
89        self.min_arity
90    }
91
92    pub fn max_arity(&self) -> Option<usize> {
93        self.max_arity
94    }
95
96    pub fn description_text(&self) -> &str {
97        &self.description
98    }
99
100    /// Calls the function; a wrong number of arguments is an evaluation error (`None`).
101    pub fn call(&self, args: &[Term]) -> Option<Term> {
102        if args.len() < self.min_arity || self.max_arity.is_some_and(|m| args.len() > m) {
103            return None;
104        }
105        (self.f)(args)
106    }
107
108    /// The callable, arity check included, as spareval takes it.
109    pub fn callable(&self) -> impl Fn(&[Term]) -> Option<Term> + Send + Sync + 'static {
110        let me = self.clone();
111        move |args| me.call(args)
112    }
113}
114
115/// The part of an IRI after its last `#` or `/`.
116pub fn local_name(iri: &str) -> &str {
117    iri.rsplit(['#', '/']).next().unwrap_or(iri)
118}
119
120/// IRIs the engine interprets itself, which a host function may not take over.
121fn reserved(iri: &str) -> Option<&'static str> {
122    if iri == crate::text::TEXT_MATCH || iri.starts_with(crate::registry::NS) {
123        Some("the oxilite namespace is reserved")
124    } else if iri.starts_with("http://www.w3.org/2001/XMLSchema#") {
125        Some("XSD casts are built in")
126    } else {
127        None
128    }
129}
130
131/// The host functions of a store, by IRI.
132#[derive(Clone, Default, Debug)]
133pub struct FunctionRegistry {
134    by_iri: BTreeMap<String, HostFunction>,
135}
136
137impl FunctionRegistry {
138    pub fn new() -> Self {
139        Self::default()
140    }
141
142    /// Adds a function, replacing one with the same IRI. Fails on an invalid or reserved IRI,
143    /// and on a Cypher name another function already uses.
144    pub fn register(&mut self, function: HostFunction) -> Result<()> {
145        NamedNode::new(function.iri.as_str()).map_err(|e| {
146            Error::Other(format!(
147                "host function IRI <{}> is invalid: {e}",
148                function.iri
149            ))
150        })?;
151        if let Some(why) = reserved(&function.iri) {
152            return Err(Error::Other(format!(
153                "cannot register <{}> as a host function: {why}",
154                function.iri
155            )));
156        }
157        let name = function.cypher().to_lowercase();
158        if name.is_empty() {
159            return Err(Error::Other(format!(
160                "host function <{}> needs a Cypher name (its IRI has no local name)",
161                function.iri
162            )));
163        }
164        if let Some(other) = self
165            .by_iri
166            .values()
167            .find(|f| f.iri != function.iri && f.cypher().to_lowercase() == name)
168        {
169            return Err(Error::Other(format!(
170                "Cypher name {} is already used by <{}>",
171                function.cypher(),
172                other.iri
173            )));
174        }
175        self.by_iri.insert(function.iri.clone(), function);
176        Ok(())
177    }
178
179    /// Removes a function; `false` when none has this IRI.
180    pub fn unregister(&mut self, iri: &str) -> bool {
181        self.by_iri.remove(iri).is_some()
182    }
183
184    pub fn get(&self, iri: &str) -> Option<&HostFunction> {
185        self.by_iri.get(iri)
186    }
187
188    /// The function Cypher calls `name` (case-insensitive).
189    pub fn by_cypher_name(&self, name: &str) -> Option<&HostFunction> {
190        self.by_iri
191            .values()
192            .find(|f| f.cypher().eq_ignore_ascii_case(name))
193    }
194
195    pub fn iter(&self) -> impl Iterator<Item = &HostFunction> {
196        self.by_iri.values()
197    }
198
199    pub fn is_empty(&self) -> bool {
200        self.by_iri.is_empty()
201    }
202
203    pub fn len(&self) -> usize {
204        self.by_iri.len()
205    }
206
207    /// Adds every function to a spareval evaluator.
208    pub fn install(&self, mut evaluator: spareval::QueryEvaluator) -> spareval::QueryEvaluator {
209        for f in self.by_iri.values() {
210            evaluator =
211                evaluator.with_custom_function(NamedNode::new_unchecked(f.iri()), f.callable());
212        }
213        evaluator
214    }
215}
216
217/// A shared, possibly absent registry: what stores hand to `QueryOptions` and the frontends'
218/// options. Two handles are equal when they share one registry.
219#[derive(Clone, Default, Debug)]
220pub struct Functions(pub Option<Arc<FunctionRegistry>>);
221
222impl Functions {
223    pub fn new(registry: FunctionRegistry) -> Self {
224        Self(Some(Arc::new(registry)))
225    }
226
227    pub fn registry(&self) -> Option<&FunctionRegistry> {
228        self.0.as_deref()
229    }
230
231    pub fn get(&self, iri: &str) -> Option<&HostFunction> {
232        self.registry()?.get(iri)
233    }
234
235    pub fn by_cypher_name(&self, name: &str) -> Option<&HostFunction> {
236        self.registry()?.by_cypher_name(name)
237    }
238
239    pub fn is_empty(&self) -> bool {
240        self.registry().is_none_or(FunctionRegistry::is_empty)
241    }
242
243    /// Adds every function to a spareval evaluator.
244    pub fn install(&self, evaluator: spareval::QueryEvaluator) -> spareval::QueryEvaluator {
245        match self.registry() {
246            Some(r) => r.install(evaluator),
247            None => evaluator,
248        }
249    }
250}
251
252impl PartialEq for Functions {
253    fn eq(&self, other: &Self) -> bool {
254        match (&self.0, &other.0) {
255            (Some(a), Some(b)) => Arc::ptr_eq(a, b),
256            (None, None) => true,
257            _ => false,
258        }
259    }
260}
261
262impl Eq for Functions {}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267    use oxrdf::Literal;
268
269    fn upper() -> HostFunction {
270        HostFunction::new("http://example.com/fn#upper", |args| {
271            match args.first()? {
272                Term::Literal(l) => {
273                    Some(Literal::new_simple_literal(l.value().to_uppercase()).into())
274                }
275                _ => None,
276            }
277        })
278        .arity(1, 1)
279    }
280
281    #[test]
282    fn registry_names_and_arity() {
283        let mut r = FunctionRegistry::new();
284        r.register(upper()).unwrap();
285        let f = r.by_cypher_name("UPPER").unwrap();
286        assert_eq!(f.iri(), "http://example.com/fn#upper");
287        assert_eq!(
288            f.call(&[Literal::new_simple_literal("a").into()]),
289            Some(Literal::new_simple_literal("A").into())
290        );
291        assert_eq!(f.call(&[]), None);
292        assert!(r
293            .register(HostFunction::new("http://other/upper", |_| None))
294            .is_err());
295        assert!(r
296            .register(HostFunction::new(crate::text::TEXT_MATCH, |_| None))
297            .is_err());
298        assert!(r
299            .register(HostFunction::new("not an iri", |_| None))
300            .is_err());
301        assert!(r.unregister("http://example.com/fn#upper"));
302        assert!(r.is_empty());
303    }
304}