use crate::error::{Error, Result};
use oxrdf::{NamedNode, Term};
use std::collections::BTreeMap;
use std::sync::Arc;
pub type HostFn = Arc<dyn Fn(&[Term]) -> Option<Term> + Send + Sync>;
#[derive(Clone)]
pub struct HostFunction {
iri: String,
cypher_name: Option<String>,
min_arity: usize,
max_arity: Option<usize>,
description: String,
f: HostFn,
}
impl std::fmt::Debug for HostFunction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HostFunction")
.field("iri", &self.iri)
.field("cypher_name", &self.cypher_name)
.field("min_arity", &self.min_arity)
.field("max_arity", &self.max_arity)
.finish_non_exhaustive()
}
}
impl HostFunction {
pub fn new(
iri: impl Into<String>,
f: impl Fn(&[Term]) -> Option<Term> + Send + Sync + 'static,
) -> Self {
Self {
iri: iri.into(),
cypher_name: None,
min_arity: 0,
max_arity: None,
description: String::new(),
f: Arc::new(f),
}
}
pub fn cypher_name(mut self, name: impl Into<String>) -> Self {
self.cypher_name = Some(name.into());
self
}
pub fn arity(mut self, min: usize, max: impl Into<Option<usize>>) -> Self {
self.min_arity = min;
self.max_arity = max.into();
self
}
pub fn description(mut self, text: impl Into<String>) -> Self {
self.description = text.into();
self
}
pub fn iri(&self) -> &str {
&self.iri
}
pub fn cypher(&self) -> &str {
self.cypher_name
.as_deref()
.unwrap_or_else(|| local_name(&self.iri))
}
pub fn min_arity(&self) -> usize {
self.min_arity
}
pub fn max_arity(&self) -> Option<usize> {
self.max_arity
}
pub fn description_text(&self) -> &str {
&self.description
}
pub fn call(&self, args: &[Term]) -> Option<Term> {
if args.len() < self.min_arity || self.max_arity.is_some_and(|m| args.len() > m) {
return None;
}
(self.f)(args)
}
pub fn callable(&self) -> impl Fn(&[Term]) -> Option<Term> + Send + Sync + 'static {
let me = self.clone();
move |args| me.call(args)
}
}
pub fn local_name(iri: &str) -> &str {
iri.rsplit(['#', '/']).next().unwrap_or(iri)
}
fn reserved(iri: &str) -> Option<&'static str> {
if iri == crate::text::TEXT_MATCH || iri.starts_with(crate::registry::NS) {
Some("the oxilite namespace is reserved")
} else if iri.starts_with("http://www.w3.org/2001/XMLSchema#") {
Some("XSD casts are built in")
} else {
None
}
}
#[derive(Clone, Default, Debug)]
pub struct FunctionRegistry {
by_iri: BTreeMap<String, HostFunction>,
}
impl FunctionRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, function: HostFunction) -> Result<()> {
NamedNode::new(function.iri.as_str()).map_err(|e| {
Error::Other(format!(
"host function IRI <{}> is invalid: {e}",
function.iri
))
})?;
if let Some(why) = reserved(&function.iri) {
return Err(Error::Other(format!(
"cannot register <{}> as a host function: {why}",
function.iri
)));
}
let name = function.cypher().to_lowercase();
if name.is_empty() {
return Err(Error::Other(format!(
"host function <{}> needs a Cypher name (its IRI has no local name)",
function.iri
)));
}
if let Some(other) = self
.by_iri
.values()
.find(|f| f.iri != function.iri && f.cypher().to_lowercase() == name)
{
return Err(Error::Other(format!(
"Cypher name {} is already used by <{}>",
function.cypher(),
other.iri
)));
}
self.by_iri.insert(function.iri.clone(), function);
Ok(())
}
pub fn unregister(&mut self, iri: &str) -> bool {
self.by_iri.remove(iri).is_some()
}
pub fn get(&self, iri: &str) -> Option<&HostFunction> {
self.by_iri.get(iri)
}
pub fn by_cypher_name(&self, name: &str) -> Option<&HostFunction> {
self.by_iri
.values()
.find(|f| f.cypher().eq_ignore_ascii_case(name))
}
pub fn iter(&self) -> impl Iterator<Item = &HostFunction> {
self.by_iri.values()
}
pub fn is_empty(&self) -> bool {
self.by_iri.is_empty()
}
pub fn len(&self) -> usize {
self.by_iri.len()
}
pub fn install(&self, mut evaluator: spareval::QueryEvaluator) -> spareval::QueryEvaluator {
for f in self.by_iri.values() {
evaluator =
evaluator.with_custom_function(NamedNode::new_unchecked(f.iri()), f.callable());
}
evaluator
}
}
#[derive(Clone, Default, Debug)]
pub struct Functions(pub Option<Arc<FunctionRegistry>>);
impl Functions {
pub fn new(registry: FunctionRegistry) -> Self {
Self(Some(Arc::new(registry)))
}
pub fn registry(&self) -> Option<&FunctionRegistry> {
self.0.as_deref()
}
pub fn get(&self, iri: &str) -> Option<&HostFunction> {
self.registry()?.get(iri)
}
pub fn by_cypher_name(&self, name: &str) -> Option<&HostFunction> {
self.registry()?.by_cypher_name(name)
}
pub fn is_empty(&self) -> bool {
self.registry().is_none_or(FunctionRegistry::is_empty)
}
pub fn install(&self, evaluator: spareval::QueryEvaluator) -> spareval::QueryEvaluator {
match self.registry() {
Some(r) => r.install(evaluator),
None => evaluator,
}
}
}
impl PartialEq for Functions {
fn eq(&self, other: &Self) -> bool {
match (&self.0, &other.0) {
(Some(a), Some(b)) => Arc::ptr_eq(a, b),
(None, None) => true,
_ => false,
}
}
}
impl Eq for Functions {}
#[cfg(test)]
mod tests {
use super::*;
use oxrdf::Literal;
fn upper() -> HostFunction {
HostFunction::new("http://example.com/fn#upper", |args| {
match args.first()? {
Term::Literal(l) => {
Some(Literal::new_simple_literal(l.value().to_uppercase()).into())
}
_ => None,
}
})
.arity(1, 1)
}
#[test]
fn registry_names_and_arity() {
let mut r = FunctionRegistry::new();
r.register(upper()).unwrap();
let f = r.by_cypher_name("UPPER").unwrap();
assert_eq!(f.iri(), "http://example.com/fn#upper");
assert_eq!(
f.call(&[Literal::new_simple_literal("a").into()]),
Some(Literal::new_simple_literal("A").into())
);
assert_eq!(f.call(&[]), None);
assert!(r
.register(HostFunction::new("http://other/upper", |_| None))
.is_err());
assert!(r
.register(HostFunction::new(crate::text::TEXT_MATCH, |_| None))
.is_err());
assert!(r
.register(HostFunction::new("not an iri", |_| None))
.is_err());
assert!(r.unregister("http://example.com/fn#upper"));
assert!(r.is_empty());
}
}