oxilite_core/
functions.rs1use crate::error::{Error, Result};
13use oxrdf::{NamedNode, Term};
14use std::collections::BTreeMap;
15use std::sync::Arc;
16
17pub type HostFn = Arc<dyn Fn(&[Term]) -> Option<Term> + Send + Sync>;
19
20#[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 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 pub fn cypher_name(mut self, name: impl Into<String>) -> Self {
60 self.cypher_name = Some(name.into());
61 self
62 }
63
64 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 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 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 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 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
115pub fn local_name(iri: &str) -> &str {
117 iri.rsplit(['#', '/']).next().unwrap_or(iri)
118}
119
120fn 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#[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 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 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 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 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#[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 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}