use crate::algebra::{Term as AlgebraTerm, Variable};
use crate::executor::Dataset;
use anyhow::{anyhow, bail, Result};
use dashmap::DashMap;
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq)]
pub enum PropFuncArg {
Node(AlgebraTerm),
List(Vec<AlgebraTerm>),
}
impl PropFuncArg {
pub fn node(term: AlgebraTerm) -> Self {
Self::Node(term)
}
pub fn list(terms: Vec<AlgebraTerm>) -> Self {
Self::List(terms)
}
pub fn is_node(&self) -> bool {
matches!(self, Self::Node(_))
}
pub fn is_list(&self) -> bool {
matches!(self, Self::List(_))
}
pub fn as_node(&self) -> Option<&AlgebraTerm> {
match self {
Self::Node(term) => Some(term),
_ => None,
}
}
pub fn as_list(&self) -> Option<&[AlgebraTerm]> {
match self {
Self::List(terms) => Some(terms),
_ => None,
}
}
pub fn list_size(&self) -> isize {
match self {
Self::List(terms) => terms.len() as isize,
_ => -1,
}
}
pub fn has_variable(&self) -> bool {
match self {
Self::Node(term) => matches!(term, AlgebraTerm::Variable(_)),
Self::List(terms) => terms.iter().any(|t| matches!(t, AlgebraTerm::Variable(_))),
}
}
pub fn variables(&self) -> Vec<Variable> {
match self {
Self::Node(AlgebraTerm::Variable(v)) => vec![v.clone()],
Self::List(terms) => terms
.iter()
.filter_map(|t| match t {
AlgebraTerm::Variable(v) => Some(v.clone()),
_ => None,
})
.collect(),
_ => Vec::new(),
}
}
}
#[derive(Clone)]
pub struct PropertyFunctionContext {
pub bindings: HashMap<Variable, AlgebraTerm>,
pub dataset: Arc<dyn Dataset>,
}
impl PropertyFunctionContext {
pub fn new(dataset: Arc<dyn Dataset>) -> Self {
Self {
bindings: HashMap::new(),
dataset,
}
}
pub fn with_bindings(
dataset: Arc<dyn Dataset>,
bindings: HashMap<Variable, AlgebraTerm>,
) -> Self {
Self { bindings, dataset }
}
pub fn get_binding(&self, var: &Variable) -> Option<&AlgebraTerm> {
self.bindings.get(var)
}
pub fn substitute(&self, arg: &PropFuncArg) -> PropFuncArg {
match arg {
PropFuncArg::Node(term) => match term {
AlgebraTerm::Variable(v) => {
if let Some(bound) = self.get_binding(v) {
PropFuncArg::Node(bound.clone())
} else {
arg.clone()
}
}
_ => arg.clone(),
},
PropFuncArg::List(terms) => {
let substituted: Vec<AlgebraTerm> = terms
.iter()
.map(|term| match term {
AlgebraTerm::Variable(v) => {
self.get_binding(v).cloned().unwrap_or_else(|| term.clone())
}
_ => term.clone(),
})
.collect();
PropFuncArg::List(substituted)
}
}
}
}
#[derive(Debug, Clone)]
pub enum PropertyFunctionResult {
Single(HashMap<Variable, AlgebraTerm>),
Multiple(Vec<HashMap<Variable, AlgebraTerm>>),
Boolean(bool),
Empty,
}
impl PropertyFunctionResult {
pub fn into_solutions(self) -> Vec<HashMap<Variable, AlgebraTerm>> {
match self {
Self::Single(bindings) => vec![bindings],
Self::Multiple(solutions) => solutions,
Self::Boolean(true) => vec![HashMap::new()],
Self::Boolean(false) | Self::Empty => Vec::new(),
}
}
pub fn is_empty(&self) -> bool {
match self {
Self::Empty | Self::Boolean(false) => true,
Self::Multiple(v) => v.is_empty(),
_ => false,
}
}
}
pub trait PropertyFunction: Send + Sync {
fn uri(&self) -> &str;
#[allow(unused_variables)]
fn build(
&self,
subject: &PropFuncArg,
predicate: &str,
object: &PropFuncArg,
_context: &PropertyFunctionContext,
) -> Result<()> {
if predicate != self.uri() {
bail!(
"Predicate mismatch: expected {}, got {}",
self.uri(),
predicate
);
}
Ok(())
}
fn execute(
&self,
subject: &PropFuncArg,
predicate: &str,
object: &PropFuncArg,
context: &PropertyFunctionContext,
) -> Result<PropertyFunctionResult>;
}
pub struct PropertyFunctionRegistry {
functions: DashMap<String, Arc<dyn PropertyFunction>>,
}
impl PropertyFunctionRegistry {
pub fn new() -> Self {
Self {
functions: DashMap::new(),
}
}
pub fn register<F: PropertyFunction + 'static>(&self, function: F) -> Result<()> {
let uri = function.uri().to_string();
self.functions.insert(uri, Arc::new(function));
Ok(())
}
pub fn get(&self, uri: &str) -> Option<Arc<dyn PropertyFunction>> {
self.functions.get(uri).map(|entry| Arc::clone(&*entry))
}
pub fn is_property_function(&self, uri: &str) -> bool {
self.functions.contains_key(uri)
}
pub fn registered_uris(&self) -> Vec<String> {
self.functions
.iter()
.map(|entry| entry.key().clone())
.collect()
}
pub fn with_standard_functions() -> Result<Self> {
let registry = Self::new();
register_standard_functions(®istry)?;
Ok(registry)
}
}
impl Default for PropertyFunctionRegistry {
fn default() -> Self {
Self::new()
}
}
pub fn register_standard_functions(registry: &PropertyFunctionRegistry) -> Result<()> {
registry.register(ListMemberFunction)?;
registry.register(ListIndexFunction)?;
registry.register(ListLengthFunction)?;
Ok(())
}
fn traverse_rdf_list(
list_node: &AlgebraTerm,
context: &PropertyFunctionContext,
) -> Result<Vec<AlgebraTerm>> {
use crate::algebra::{Iri, TriplePattern};
let rdf_first_iri = Iri::new("http://www.w3.org/1999/02/22-rdf-syntax-ns#first")
.map_err(|e| anyhow!("Invalid RDF first URI: {}", e))?;
let rdf_rest_iri = Iri::new("http://www.w3.org/1999/02/22-rdf-syntax-ns#rest")
.map_err(|e| anyhow!("Invalid RDF rest URI: {}", e))?;
let rdf_nil_iri = Iri::new("http://www.w3.org/1999/02/22-rdf-syntax-ns#nil")
.map_err(|e| anyhow!("Invalid RDF nil URI: {}", e))?;
let rdf_first = AlgebraTerm::Iri(rdf_first_iri);
let rdf_rest = AlgebraTerm::Iri(rdf_rest_iri);
let rdf_nil = AlgebraTerm::Iri(rdf_nil_iri);
let mut members = Vec::new();
let mut current_node = list_node.clone();
const MAX_DEPTH: usize = 10000;
let mut depth = 0;
loop {
depth += 1;
if depth > MAX_DEPTH {
bail!("RDF list traversal exceeded maximum depth (possible cycle)");
}
if current_node == rdf_nil {
break;
}
let first_var = Variable::new("_first")?;
let first_pattern = TriplePattern::new(
current_node.clone(),
rdf_first.clone(),
AlgebraTerm::Variable(first_var.clone()),
);
let first_triples = context.dataset.find_triples(&first_pattern)?;
if let Some((_, _, first_value)) = first_triples.first() {
members.push(first_value.clone());
} else {
break;
}
let rest_var = Variable::new("_rest")?;
let rest_pattern = TriplePattern::new(
current_node.clone(),
rdf_rest.clone(),
AlgebraTerm::Variable(rest_var.clone()),
);
let rest_triples = context.dataset.find_triples(&rest_pattern)?;
if let Some((_, _, rest_value)) = rest_triples.first() {
current_node = rest_value.clone();
} else {
break;
}
}
Ok(members)
}
#[derive(Debug, Clone)]
struct ListMemberFunction;
impl PropertyFunction for ListMemberFunction {
fn uri(&self) -> &str {
"http://jena.apache.org/ARQ/list#member"
}
fn build(
&self,
_subject: &PropFuncArg,
predicate: &str,
object: &PropFuncArg,
_context: &PropertyFunctionContext,
) -> Result<()> {
if predicate != self.uri() {
bail!("Predicate mismatch");
}
if object.is_list() && object.list_size() > 0 {
bail!("list:member does not accept list arguments in object position");
}
Ok(())
}
fn execute(
&self,
subject: &PropFuncArg,
_predicate: &str,
object: &PropFuncArg,
context: &PropertyFunctionContext,
) -> Result<PropertyFunctionResult> {
let subject = context.substitute(subject);
let object = context.substitute(object);
let list_node = subject
.as_node()
.ok_or_else(|| anyhow!("list:member requires single node in subject position"))?;
let members = self.get_list_members(list_node, context)?;
if let Some(member_node) = object.as_node() {
if let AlgebraTerm::Variable(v) = member_node {
let solutions: Vec<HashMap<Variable, AlgebraTerm>> = members
.into_iter()
.map(|m| {
let mut bindings = HashMap::new();
bindings.insert(v.clone(), m);
bindings
})
.collect();
Ok(PropertyFunctionResult::Multiple(solutions))
} else {
let is_member = members.contains(member_node);
Ok(PropertyFunctionResult::Boolean(is_member))
}
} else {
bail!("list:member requires node in object position");
}
}
}
impl ListMemberFunction {
fn get_list_members(
&self,
list_node: &AlgebraTerm,
context: &PropertyFunctionContext,
) -> Result<Vec<AlgebraTerm>> {
traverse_rdf_list(list_node, context)
}
}
#[derive(Debug, Clone)]
struct ListIndexFunction;
impl PropertyFunction for ListIndexFunction {
fn uri(&self) -> &str {
"http://jena.apache.org/ARQ/list#index"
}
fn build(
&self,
_subject: &PropFuncArg,
predicate: &str,
object: &PropFuncArg,
_context: &PropertyFunctionContext,
) -> Result<()> {
if predicate != self.uri() {
bail!("Predicate mismatch");
}
if !object.is_list() || object.list_size() != 2 {
bail!("list:index requires exactly 2 arguments in object position: (index member)");
}
Ok(())
}
fn execute(
&self,
subject: &PropFuncArg,
_predicate: &str,
object: &PropFuncArg,
context: &PropertyFunctionContext,
) -> Result<PropertyFunctionResult> {
let subject = context.substitute(subject);
let object = context.substitute(object);
let list_node = subject
.as_node()
.ok_or_else(|| anyhow!("list:index requires single node in subject position"))?;
let obj_list = object
.as_list()
.ok_or_else(|| anyhow!("list:index requires list in object position"))?;
if obj_list.len() != 2 {
bail!("list:index requires exactly 2 arguments: (index member)");
}
let index_term = &obj_list[0];
let member_term = &obj_list[1];
let members = self.get_list_members(list_node, context)?;
if let AlgebraTerm::Literal(lit) = index_term {
let index: usize = lit
.value
.parse()
.map_err(|_| anyhow!("Invalid index: {}", lit.value))?;
if let Some(member) = members.get(index) {
if let AlgebraTerm::Variable(v) = member_term {
let mut bindings = HashMap::new();
bindings.insert(v.clone(), member.clone());
Ok(PropertyFunctionResult::Single(bindings))
} else {
Ok(PropertyFunctionResult::Boolean(member == member_term))
}
} else {
Ok(PropertyFunctionResult::Empty)
}
} else if let AlgebraTerm::Variable(v_idx) = index_term {
use oxirs_core::model::NamedNode;
let xsd_integer = NamedNode::new("http://www.w3.org/2001/XMLSchema#integer")
.map_err(|e| anyhow!("Failed to create xsd:integer IRI: {}", e))?;
let solutions: Vec<HashMap<Variable, AlgebraTerm>> = members
.into_iter()
.enumerate()
.map(|(idx, member)| {
let mut bindings = HashMap::new();
bindings.insert(
v_idx.clone(),
AlgebraTerm::Literal(crate::algebra::Literal {
value: idx.to_string(),
language: None,
datatype: Some(xsd_integer.clone()),
}),
);
if let AlgebraTerm::Variable(v_member) = member_term {
bindings.insert(v_member.clone(), member);
}
bindings
})
.collect();
Ok(PropertyFunctionResult::Multiple(solutions))
} else {
bail!("list:index requires index to be integer or variable");
}
}
}
impl ListIndexFunction {
fn get_list_members(
&self,
list_node: &AlgebraTerm,
context: &PropertyFunctionContext,
) -> Result<Vec<AlgebraTerm>> {
traverse_rdf_list(list_node, context)
}
}
#[derive(Debug, Clone)]
struct ListLengthFunction;
impl PropertyFunction for ListLengthFunction {
fn uri(&self) -> &str {
"http://jena.apache.org/ARQ/list#length"
}
fn execute(
&self,
subject: &PropFuncArg,
_predicate: &str,
object: &PropFuncArg,
context: &PropertyFunctionContext,
) -> Result<PropertyFunctionResult> {
let subject = context.substitute(subject);
let object = context.substitute(object);
let list_node = subject
.as_node()
.ok_or_else(|| anyhow!("list:length requires single node in subject position"))?;
let length_node = object
.as_node()
.ok_or_else(|| anyhow!("list:length requires single node in object position"))?;
let members = self.get_list_members(list_node, context)?;
let length = members.len();
if let AlgebraTerm::Variable(v) = length_node {
use oxirs_core::model::NamedNode;
let xsd_integer = NamedNode::new("http://www.w3.org/2001/XMLSchema#integer")
.map_err(|e| anyhow!("Failed to create xsd:integer IRI: {}", e))?;
let mut bindings = HashMap::new();
bindings.insert(
v.clone(),
AlgebraTerm::Literal(crate::algebra::Literal {
value: length.to_string(),
language: None,
datatype: Some(xsd_integer),
}),
);
Ok(PropertyFunctionResult::Single(bindings))
} else if let AlgebraTerm::Literal(lit) = length_node {
let expected: usize = lit
.value
.parse()
.map_err(|_| anyhow!("Invalid length: {}", lit.value))?;
Ok(PropertyFunctionResult::Boolean(length == expected))
} else {
bail!("list:length requires integer or variable in object position");
}
}
}
impl ListLengthFunction {
fn get_list_members(
&self,
list_node: &AlgebraTerm,
context: &PropertyFunctionContext,
) -> Result<Vec<AlgebraTerm>> {
traverse_rdf_list(list_node, context)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_prop_func_arg_node() {
use oxirs_core::model::Variable;
let var = Variable::new("x").unwrap();
let term = AlgebraTerm::Variable(var);
let arg = PropFuncArg::node(term.clone());
assert!(arg.is_node());
assert!(!arg.is_list());
assert_eq!(arg.as_node(), Some(&term));
assert_eq!(arg.list_size(), -1);
}
#[test]
fn test_prop_func_arg_list() {
use oxirs_core::model::Variable;
let var_x = Variable::new("x").unwrap();
let var_y = Variable::new("y").unwrap();
let terms = vec![AlgebraTerm::Variable(var_x), AlgebraTerm::Variable(var_y)];
let arg = PropFuncArg::list(terms.clone());
assert!(arg.is_list());
assert!(!arg.is_node());
assert_eq!(arg.as_list(), Some(terms.as_slice()));
assert_eq!(arg.list_size(), 2);
}
#[test]
fn test_prop_func_arg_has_variable() {
use oxirs_core::model::Variable;
let var = Variable::new("x").unwrap();
let arg_var = PropFuncArg::node(AlgebraTerm::Variable(var));
assert!(arg_var.has_variable());
let arg_iri = PropFuncArg::node(AlgebraTerm::Iri(
crate::algebra::Iri::new("http://example.org/").unwrap(),
));
assert!(!arg_iri.has_variable());
let var_x = Variable::new("x").unwrap();
let arg_list = PropFuncArg::list(vec![
AlgebraTerm::Variable(var_x),
AlgebraTerm::Iri(crate::algebra::Iri::new("http://example.org/").unwrap()),
]);
assert!(arg_list.has_variable());
}
#[test]
fn test_prop_func_arg_variables() {
use oxirs_core::model::Variable;
let var_x = Variable::new("x").unwrap();
let var_y = Variable::new("y").unwrap();
let arg = PropFuncArg::list(vec![
AlgebraTerm::Variable(var_x.clone()),
AlgebraTerm::Variable(var_y.clone()),
AlgebraTerm::Iri(crate::algebra::Iri::new("http://example.org/").unwrap()),
]);
let vars = arg.variables();
assert_eq!(vars.len(), 2);
assert!(vars.iter().any(|v| v.as_str() == "x"));
assert!(vars.iter().any(|v| v.as_str() == "y"));
}
#[test]
fn test_registry_registration() {
let registry = PropertyFunctionRegistry::new();
registry.register(ListMemberFunction).unwrap();
assert!(registry.is_property_function("http://jena.apache.org/ARQ/list#member"));
assert!(!registry.is_property_function("http://example.org/unknown"));
}
#[test]
fn test_registry_with_standard_functions() {
let registry = PropertyFunctionRegistry::with_standard_functions().unwrap();
assert!(registry.is_property_function("http://jena.apache.org/ARQ/list#member"));
assert!(registry.is_property_function("http://jena.apache.org/ARQ/list#index"));
assert!(registry.is_property_function("http://jena.apache.org/ARQ/list#length"));
let uris = registry.registered_uris();
assert_eq!(uris.len(), 3);
}
#[test]
fn test_property_function_result_into_solutions() {
use oxirs_core::model::Variable;
let var_x = Variable::new("x").unwrap();
let var_y = Variable::new("y").unwrap();
let single = PropertyFunctionResult::Single({
let mut map = HashMap::new();
map.insert(var_x.clone(), AlgebraTerm::Variable(var_y.clone()));
map
});
assert_eq!(single.into_solutions().len(), 1);
let multiple = PropertyFunctionResult::Multiple(vec![HashMap::new(), HashMap::new()]);
assert_eq!(multiple.into_solutions().len(), 2);
let boolean_true = PropertyFunctionResult::Boolean(true);
assert_eq!(boolean_true.into_solutions().len(), 1);
let boolean_false = PropertyFunctionResult::Boolean(false);
assert_eq!(boolean_false.into_solutions().len(), 0);
let empty = PropertyFunctionResult::Empty;
assert_eq!(empty.into_solutions().len(), 0);
}
#[test]
fn test_context_substitute() {
use crate::executor::InMemoryDataset;
use oxirs_core::model::Variable;
let dataset: Arc<dyn crate::executor::Dataset> = Arc::new(InMemoryDataset::new());
let mut bindings = HashMap::new();
let var_x = Variable::new("x").unwrap();
bindings.insert(
var_x.clone(),
AlgebraTerm::Iri(crate::algebra::Iri::new("http://example.org/bound").unwrap()),
);
let context = PropertyFunctionContext::with_bindings(dataset, bindings);
let arg = PropFuncArg::node(AlgebraTerm::Variable(var_x));
let substituted = context.substitute(&arg);
match substituted {
PropFuncArg::Node(AlgebraTerm::Iri(iri)) => {
assert_eq!(iri.as_str(), "http://example.org/bound");
}
_ => panic!("Expected IRI after substitution"),
}
}
}