use crate::model::Term;
use crate::OxirsError;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq)]
pub enum PropertyFunctionArg {
Term(Term),
Variable(String),
List(Vec<PropertyFunctionArg>),
}
impl fmt::Display for PropertyFunctionArg {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PropertyFunctionArg::Term(t) => write!(f, "{t:?}"),
PropertyFunctionArg::Variable(v) => write!(f, "?{v}"),
PropertyFunctionArg::List(args) => {
write!(f, "(")?;
for (i, a) in args.iter().enumerate() {
if i > 0 {
write!(f, " ")?;
}
write!(f, "{a}")?;
}
write!(f, ")")
}
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PropertyFunctionBinding {
bindings: HashMap<String, Term>,
}
impl PropertyFunctionBinding {
pub fn new() -> Self {
Self {
bindings: HashMap::new(),
}
}
pub fn bind(mut self, var: impl Into<String>, term: Term) -> Self {
self.bindings.insert(var.into(), term);
self
}
pub fn get(&self, var: &str) -> Option<&Term> {
self.bindings.get(var)
}
pub fn bindings(&self) -> &HashMap<String, Term> {
&self.bindings
}
pub fn len(&self) -> usize {
self.bindings.len()
}
pub fn is_empty(&self) -> bool {
self.bindings.is_empty()
}
}
impl Default for PropertyFunctionBinding {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct PropertyFunctionResult {
rows: Vec<PropertyFunctionBinding>,
}
impl PropertyFunctionResult {
pub fn empty() -> Self {
Self { rows: Vec::new() }
}
pub fn from_rows(rows: Vec<PropertyFunctionBinding>) -> Self {
Self { rows }
}
pub fn single(binding: PropertyFunctionBinding) -> Self {
Self {
rows: vec![binding],
}
}
pub fn rows(&self) -> &[PropertyFunctionBinding] {
&self.rows
}
pub fn len(&self) -> usize {
self.rows.len()
}
pub fn is_empty(&self) -> bool {
self.rows.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &PropertyFunctionBinding> {
self.rows.iter()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PropertyFunctionMetadata {
pub iri: String,
pub name: String,
pub description: String,
pub subject_must_be_bound: bool,
pub object_must_be_bound: bool,
pub min_subject_args: usize,
pub max_subject_args: Option<usize>,
pub min_object_args: usize,
pub max_object_args: Option<usize>,
pub category: String,
}
pub trait PropertyFunction: Send + Sync + fmt::Debug {
fn metadata(&self) -> PropertyFunctionMetadata;
fn evaluate(
&self,
subject: &PropertyFunctionArg,
object: &PropertyFunctionArg,
) -> Result<PropertyFunctionResult, OxirsError>;
fn validate(
&self,
subject: &PropertyFunctionArg,
object: &PropertyFunctionArg,
) -> Result<(), OxirsError> {
let meta = self.metadata();
if let PropertyFunctionArg::List(args) = subject {
if args.len() < meta.min_subject_args {
return Err(OxirsError::Query(format!(
"Property function {} requires at least {} subject arguments, got {}",
meta.iri,
meta.min_subject_args,
args.len()
)));
}
if let Some(max) = meta.max_subject_args {
if args.len() > max {
return Err(OxirsError::Query(format!(
"Property function {} accepts at most {} subject arguments, got {}",
meta.iri,
max,
args.len()
)));
}
}
}
if let PropertyFunctionArg::List(args) = object {
if args.len() < meta.min_object_args {
return Err(OxirsError::Query(format!(
"Property function {} requires at least {} object arguments, got {}",
meta.iri,
meta.min_object_args,
args.len()
)));
}
if let Some(max) = meta.max_object_args {
if args.len() > max {
return Err(OxirsError::Query(format!(
"Property function {} accepts at most {} object arguments, got {}",
meta.iri,
max,
args.len()
)));
}
}
}
Ok(())
}
fn estimated_cardinality(
&self,
_subject: &PropertyFunctionArg,
_object: &PropertyFunctionArg,
) -> Option<u64> {
None
}
}
pub trait PropertyFunctionFactory: Send + Sync {
fn create(&self, iri: &str) -> Result<Box<dyn PropertyFunction>, OxirsError>;
}
pub struct PropertyFunctionRegistry {
functions: HashMap<String, Arc<dyn PropertyFunction>>,
factories: HashMap<String, Arc<dyn PropertyFunctionFactory>>,
stats: PropertyFunctionStats,
}
#[derive(Debug, Clone, Default)]
pub struct PropertyFunctionStats {
pub total_evaluations: u64,
pub total_rows_produced: u64,
pub total_errors: u64,
pub per_function_counts: HashMap<String, u64>,
}
impl Default for PropertyFunctionRegistry {
fn default() -> Self {
let mut registry = Self {
functions: HashMap::new(),
factories: HashMap::new(),
stats: PropertyFunctionStats::default(),
};
registry.register_builtins();
registry
}
}
impl PropertyFunctionRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn empty() -> Self {
Self {
functions: HashMap::new(),
factories: HashMap::new(),
stats: PropertyFunctionStats::default(),
}
}
pub fn register(&mut self, iri: impl Into<String>, func: Arc<dyn PropertyFunction>) {
self.functions.insert(iri.into(), func);
}
pub fn register_factory(
&mut self,
iri: impl Into<String>,
factory: Arc<dyn PropertyFunctionFactory>,
) {
self.factories.insert(iri.into(), factory);
}
pub fn unregister(&mut self, iri: &str) -> bool {
let removed_func = self.functions.remove(iri).is_some();
let removed_factory = self.factories.remove(iri).is_some();
removed_func || removed_factory
}
pub fn is_property_function(&self, iri: &str) -> bool {
self.functions.contains_key(iri) || self.factories.contains_key(iri)
}
pub fn lookup(&mut self, iri: &str) -> Option<Arc<dyn PropertyFunction>> {
if let Some(func) = self.functions.get(iri) {
return Some(Arc::clone(func));
}
if let Some(factory) = self.factories.get(iri) {
match factory.create(iri) {
Ok(func) => {
let func: Arc<dyn PropertyFunction> = Arc::from(func);
self.functions.insert(iri.to_string(), Arc::clone(&func));
return Some(func);
}
Err(_) => return None,
}
}
None
}
pub fn evaluate(
&mut self,
iri: &str,
subject: &PropertyFunctionArg,
object: &PropertyFunctionArg,
) -> Result<PropertyFunctionResult, OxirsError> {
let func = self
.lookup(iri)
.ok_or_else(|| OxirsError::Query(format!("Unknown property function: {iri}")))?;
func.validate(subject, object)?;
let result = func.evaluate(subject, object);
self.stats.total_evaluations += 1;
*self
.stats
.per_function_counts
.entry(iri.to_string())
.or_insert(0) += 1;
match &result {
Ok(r) => {
self.stats.total_rows_produced += r.len() as u64;
}
Err(_) => {
self.stats.total_errors += 1;
}
}
result
}
pub fn registered_iris(&self) -> Vec<String> {
let mut iris: Vec<String> = self.functions.keys().cloned().collect();
for iri in self.factories.keys() {
if !iris.contains(iri) {
iris.push(iri.clone());
}
}
iris.sort();
iris
}
pub fn all_metadata(&self) -> Vec<PropertyFunctionMetadata> {
self.functions.values().map(|f| f.metadata()).collect()
}
pub fn statistics(&self) -> &PropertyFunctionStats {
&self.stats
}
pub fn reset_statistics(&mut self) {
self.stats = PropertyFunctionStats::default();
}
pub fn len(&self) -> usize {
let mut count = self.functions.len();
for iri in self.factories.keys() {
if !self.functions.contains_key(iri) {
count += 1;
}
}
count
}
pub fn is_empty(&self) -> bool {
self.functions.is_empty() && self.factories.is_empty()
}
fn register_builtins(&mut self) {
self.register(
"http://jena.apache.org/ARQ/list#member",
Arc::new(ListMemberPF::new()),
);
self.register(
"http://jena.apache.org/ARQ/list#index",
Arc::new(ListIndexPF::new()),
);
self.register(
"http://jena.apache.org/ARQ/list#length",
Arc::new(ListLengthPF::new()),
);
self.register(
"http://jena.apache.org/ARQ/property#splitIRI",
Arc::new(SplitIriPF::new()),
);
self.register(
"http://jena.apache.org/ARQ/property#localname",
Arc::new(LocalNamePF::new()),
);
self.register(
"http://jena.apache.org/ARQ/property#namespace",
Arc::new(NamespacePF::new()),
);
self.register(
"http://jena.apache.org/text#search",
Arc::new(TextSearchPF::new()),
);
self.register(
"http://jena.apache.org/ARQ/property#concat",
Arc::new(ConcatPF::new()),
);
self.register(
"http://jena.apache.org/ARQ/property#strSplit",
Arc::new(StrSplitPF::new()),
);
}
}
impl fmt::Debug for PropertyFunctionRegistry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PropertyFunctionRegistry")
.field("registered_count", &self.len())
.field("stats", &self.stats)
.finish()
}
}
#[derive(Debug)]
pub struct ListMemberPF {
_private: (),
}
impl ListMemberPF {
pub fn new() -> Self {
Self { _private: () }
}
}
impl Default for ListMemberPF {
fn default() -> Self {
Self::new()
}
}
impl PropertyFunction for ListMemberPF {
fn metadata(&self) -> PropertyFunctionMetadata {
PropertyFunctionMetadata {
iri: "http://jena.apache.org/ARQ/list#member".to_string(),
name: "list:member".to_string(),
description: "Enumerates all members of an RDF list".to_string(),
subject_must_be_bound: false,
object_must_be_bound: false,
min_subject_args: 0,
max_subject_args: None,
min_object_args: 0,
max_object_args: None,
category: "list".to_string(),
}
}
fn evaluate(
&self,
subject: &PropertyFunctionArg,
_object: &PropertyFunctionArg,
) -> Result<PropertyFunctionResult, OxirsError> {
match subject {
PropertyFunctionArg::List(members) => {
let mut rows = Vec::new();
for (i, member) in members.iter().enumerate() {
if let PropertyFunctionArg::Term(term) = member {
let binding = PropertyFunctionBinding::new()
.bind("index", make_integer_term(i as i64))
.bind("member", term.clone());
rows.push(binding);
}
}
Ok(PropertyFunctionResult::from_rows(rows))
}
PropertyFunctionArg::Term(term) => {
let binding = PropertyFunctionBinding::new()
.bind("index", make_integer_term(0))
.bind("member", term.clone());
Ok(PropertyFunctionResult::single(binding))
}
PropertyFunctionArg::Variable(_) => {
Ok(PropertyFunctionResult::empty())
}
}
}
fn estimated_cardinality(
&self,
subject: &PropertyFunctionArg,
_object: &PropertyFunctionArg,
) -> Option<u64> {
match subject {
PropertyFunctionArg::List(members) => Some(members.len() as u64),
PropertyFunctionArg::Term(_) => Some(1),
PropertyFunctionArg::Variable(_) => None,
}
}
}
#[derive(Debug)]
pub struct ListIndexPF {
_private: (),
}
impl ListIndexPF {
pub fn new() -> Self {
Self { _private: () }
}
}
impl Default for ListIndexPF {
fn default() -> Self {
Self::new()
}
}
impl PropertyFunction for ListIndexPF {
fn metadata(&self) -> PropertyFunctionMetadata {
PropertyFunctionMetadata {
iri: "http://jena.apache.org/ARQ/list#index".to_string(),
name: "list:index".to_string(),
description: "Returns (index, member) pairs for an RDF list".to_string(),
subject_must_be_bound: false,
object_must_be_bound: false,
min_subject_args: 0,
max_subject_args: None,
min_object_args: 0,
max_object_args: Some(2),
category: "list".to_string(),
}
}
fn evaluate(
&self,
subject: &PropertyFunctionArg,
_object: &PropertyFunctionArg,
) -> Result<PropertyFunctionResult, OxirsError> {
match subject {
PropertyFunctionArg::List(members) => {
let mut rows = Vec::new();
for (i, member) in members.iter().enumerate() {
if let PropertyFunctionArg::Term(term) = member {
let binding = PropertyFunctionBinding::new()
.bind("index", make_integer_term(i as i64))
.bind("item", term.clone());
rows.push(binding);
}
}
Ok(PropertyFunctionResult::from_rows(rows))
}
_ => Ok(PropertyFunctionResult::empty()),
}
}
}
#[derive(Debug)]
pub struct ListLengthPF {
_private: (),
}
impl ListLengthPF {
pub fn new() -> Self {
Self { _private: () }
}
}
impl Default for ListLengthPF {
fn default() -> Self {
Self::new()
}
}
impl PropertyFunction for ListLengthPF {
fn metadata(&self) -> PropertyFunctionMetadata {
PropertyFunctionMetadata {
iri: "http://jena.apache.org/ARQ/list#length".to_string(),
name: "list:length".to_string(),
description: "Returns the length of an RDF list".to_string(),
subject_must_be_bound: true,
object_must_be_bound: false,
min_subject_args: 0,
max_subject_args: None,
min_object_args: 0,
max_object_args: Some(1),
category: "list".to_string(),
}
}
fn evaluate(
&self,
subject: &PropertyFunctionArg,
_object: &PropertyFunctionArg,
) -> Result<PropertyFunctionResult, OxirsError> {
let len = match subject {
PropertyFunctionArg::List(members) => members.len(),
PropertyFunctionArg::Term(_) => 1,
PropertyFunctionArg::Variable(_) => {
return Err(OxirsError::Query(
"list:length requires a bound subject".to_string(),
));
}
};
let binding = PropertyFunctionBinding::new().bind("length", make_integer_term(len as i64));
Ok(PropertyFunctionResult::single(binding))
}
fn estimated_cardinality(
&self,
_subject: &PropertyFunctionArg,
_object: &PropertyFunctionArg,
) -> Option<u64> {
Some(1) }
}
#[derive(Debug)]
pub struct SplitIriPF {
_private: (),
}
impl SplitIriPF {
pub fn new() -> Self {
Self { _private: () }
}
}
impl Default for SplitIriPF {
fn default() -> Self {
Self::new()
}
}
impl PropertyFunction for SplitIriPF {
fn metadata(&self) -> PropertyFunctionMetadata {
PropertyFunctionMetadata {
iri: "http://jena.apache.org/ARQ/property#splitIRI".to_string(),
name: "apf:splitIRI".to_string(),
description: "Splits an IRI into namespace and local name".to_string(),
subject_must_be_bound: true,
object_must_be_bound: false,
min_subject_args: 1,
max_subject_args: Some(1),
min_object_args: 0,
max_object_args: Some(2),
category: "string".to_string(),
}
}
fn evaluate(
&self,
subject: &PropertyFunctionArg,
_object: &PropertyFunctionArg,
) -> Result<PropertyFunctionResult, OxirsError> {
let iri_str = match subject {
PropertyFunctionArg::Term(term) => extract_iri_string(term)?,
PropertyFunctionArg::Variable(_) => {
return Err(OxirsError::Query(
"apf:splitIRI requires a bound IRI subject".to_string(),
));
}
PropertyFunctionArg::List(args) => {
if let Some(PropertyFunctionArg::Term(term)) = args.first() {
extract_iri_string(term)?
} else {
return Err(OxirsError::Query(
"apf:splitIRI requires an IRI argument".to_string(),
));
}
}
};
let (namespace, local_name) = split_iri(&iri_str);
let binding = PropertyFunctionBinding::new()
.bind("namespace", make_string_term(&namespace))
.bind("localname", make_string_term(&local_name));
Ok(PropertyFunctionResult::single(binding))
}
fn estimated_cardinality(
&self,
_subject: &PropertyFunctionArg,
_object: &PropertyFunctionArg,
) -> Option<u64> {
Some(1)
}
}
#[derive(Debug)]
pub struct LocalNamePF {
_private: (),
}
impl LocalNamePF {
pub fn new() -> Self {
Self { _private: () }
}
}
impl Default for LocalNamePF {
fn default() -> Self {
Self::new()
}
}
impl PropertyFunction for LocalNamePF {
fn metadata(&self) -> PropertyFunctionMetadata {
PropertyFunctionMetadata {
iri: "http://jena.apache.org/ARQ/property#localname".to_string(),
name: "apf:localname".to_string(),
description: "Extracts the local name from an IRI".to_string(),
subject_must_be_bound: true,
object_must_be_bound: false,
min_subject_args: 1,
max_subject_args: Some(1),
min_object_args: 0,
max_object_args: Some(1),
category: "string".to_string(),
}
}
fn evaluate(
&self,
subject: &PropertyFunctionArg,
_object: &PropertyFunctionArg,
) -> Result<PropertyFunctionResult, OxirsError> {
let iri_str = extract_arg_iri(subject)?;
let (_, local_name) = split_iri(&iri_str);
let binding =
PropertyFunctionBinding::new().bind("localname", make_string_term(&local_name));
Ok(PropertyFunctionResult::single(binding))
}
fn estimated_cardinality(
&self,
_subject: &PropertyFunctionArg,
_object: &PropertyFunctionArg,
) -> Option<u64> {
Some(1)
}
}
#[derive(Debug)]
pub struct NamespacePF {
_private: (),
}
impl NamespacePF {
pub fn new() -> Self {
Self { _private: () }
}
}
impl Default for NamespacePF {
fn default() -> Self {
Self::new()
}
}
impl PropertyFunction for NamespacePF {
fn metadata(&self) -> PropertyFunctionMetadata {
PropertyFunctionMetadata {
iri: "http://jena.apache.org/ARQ/property#namespace".to_string(),
name: "apf:namespace".to_string(),
description: "Extracts the namespace from an IRI".to_string(),
subject_must_be_bound: true,
object_must_be_bound: false,
min_subject_args: 1,
max_subject_args: Some(1),
min_object_args: 0,
max_object_args: Some(1),
category: "string".to_string(),
}
}
fn evaluate(
&self,
subject: &PropertyFunctionArg,
_object: &PropertyFunctionArg,
) -> Result<PropertyFunctionResult, OxirsError> {
let iri_str = extract_arg_iri(subject)?;
let (namespace, _) = split_iri(&iri_str);
let binding =
PropertyFunctionBinding::new().bind("namespace", make_string_term(&namespace));
Ok(PropertyFunctionResult::single(binding))
}
}
#[derive(Debug)]
pub struct TextSearchPF {
_private: (),
}
impl TextSearchPF {
pub fn new() -> Self {
Self { _private: () }
}
}
impl Default for TextSearchPF {
fn default() -> Self {
Self::new()
}
}
impl PropertyFunction for TextSearchPF {
fn metadata(&self) -> PropertyFunctionMetadata {
PropertyFunctionMetadata {
iri: "http://jena.apache.org/text#search".to_string(),
name: "text:search".to_string(),
description: "Full-text search across literal values".to_string(),
subject_must_be_bound: false,
object_must_be_bound: true,
min_subject_args: 0,
max_subject_args: None,
min_object_args: 1,
max_object_args: Some(3),
category: "text".to_string(),
}
}
fn evaluate(
&self,
_subject: &PropertyFunctionArg,
object: &PropertyFunctionArg,
) -> Result<PropertyFunctionResult, OxirsError> {
let query = match object {
PropertyFunctionArg::Term(term) => extract_string_value(term),
PropertyFunctionArg::List(args) => {
if let Some(PropertyFunctionArg::Term(term)) = args.first() {
extract_string_value(term)
} else {
return Err(OxirsError::Query(
"text:search requires a search query string".to_string(),
));
}
}
PropertyFunctionArg::Variable(_) => {
return Err(OxirsError::Query(
"text:search requires a bound search query".to_string(),
));
}
};
let binding = PropertyFunctionBinding::new()
.bind("query", make_string_term(&query))
.bind("score", make_double_term(1.0));
Ok(PropertyFunctionResult::single(binding))
}
}
#[derive(Debug)]
pub struct ConcatPF {
_private: (),
}
impl ConcatPF {
pub fn new() -> Self {
Self { _private: () }
}
}
impl Default for ConcatPF {
fn default() -> Self {
Self::new()
}
}
impl PropertyFunction for ConcatPF {
fn metadata(&self) -> PropertyFunctionMetadata {
PropertyFunctionMetadata {
iri: "http://jena.apache.org/ARQ/property#concat".to_string(),
name: "apf:concat".to_string(),
description: "Concatenates string arguments into a single string".to_string(),
subject_must_be_bound: true,
object_must_be_bound: false,
min_subject_args: 1,
max_subject_args: None,
min_object_args: 0,
max_object_args: Some(1),
category: "string".to_string(),
}
}
fn evaluate(
&self,
subject: &PropertyFunctionArg,
_object: &PropertyFunctionArg,
) -> Result<PropertyFunctionResult, OxirsError> {
let parts: Vec<String> = match subject {
PropertyFunctionArg::List(args) => args
.iter()
.filter_map(|a| {
if let PropertyFunctionArg::Term(t) = a {
Some(extract_string_value(t))
} else {
None
}
})
.collect(),
PropertyFunctionArg::Term(term) => vec![extract_string_value(term)],
PropertyFunctionArg::Variable(_) => {
return Err(OxirsError::Query(
"apf:concat requires bound string arguments".to_string(),
));
}
};
let concatenated = parts.join("");
let binding =
PropertyFunctionBinding::new().bind("result", make_string_term(&concatenated));
Ok(PropertyFunctionResult::single(binding))
}
fn estimated_cardinality(
&self,
_subject: &PropertyFunctionArg,
_object: &PropertyFunctionArg,
) -> Option<u64> {
Some(1)
}
}
#[derive(Debug)]
pub struct StrSplitPF {
_private: (),
}
impl StrSplitPF {
pub fn new() -> Self {
Self { _private: () }
}
}
impl Default for StrSplitPF {
fn default() -> Self {
Self::new()
}
}
impl PropertyFunction for StrSplitPF {
fn metadata(&self) -> PropertyFunctionMetadata {
PropertyFunctionMetadata {
iri: "http://jena.apache.org/ARQ/property#strSplit".to_string(),
name: "apf:strSplit".to_string(),
description: "Splits a string by a delimiter, producing multiple bindings".to_string(),
subject_must_be_bound: true,
object_must_be_bound: false,
min_subject_args: 1,
max_subject_args: Some(1),
min_object_args: 1,
max_object_args: Some(2),
category: "string".to_string(),
}
}
fn evaluate(
&self,
subject: &PropertyFunctionArg,
object: &PropertyFunctionArg,
) -> Result<PropertyFunctionResult, OxirsError> {
let input = extract_arg_string(subject)?;
let delimiter = match object {
PropertyFunctionArg::Term(term) => extract_string_value(term),
PropertyFunctionArg::List(args) => {
if let Some(PropertyFunctionArg::Term(term)) = args.first() {
extract_string_value(term)
} else {
",".to_string() }
}
PropertyFunctionArg::Variable(_) => ",".to_string(),
};
let parts: Vec<&str> = input.split(&delimiter).collect();
let mut rows = Vec::new();
for (i, part) in parts.iter().enumerate() {
let binding = PropertyFunctionBinding::new()
.bind("index", make_integer_term(i as i64))
.bind("part", make_string_term(part));
rows.push(binding);
}
Ok(PropertyFunctionResult::from_rows(rows))
}
}
fn make_integer_term(value: i64) -> Term {
Term::Literal(crate::model::Literal::new_typed(
value.to_string(),
crate::model::NamedNode::new_unchecked("http://www.w3.org/2001/XMLSchema#integer"),
))
}
fn make_double_term(value: f64) -> Term {
Term::Literal(crate::model::Literal::new_typed(
value.to_string(),
crate::model::NamedNode::new_unchecked("http://www.w3.org/2001/XMLSchema#double"),
))
}
fn make_string_term(value: &str) -> Term {
Term::Literal(crate::model::Literal::new(value))
}
fn extract_string_value(term: &Term) -> String {
match term {
Term::Literal(lit) => lit.value().to_string(),
Term::NamedNode(nn) => nn.as_str().to_string(),
Term::BlankNode(bn) => bn.as_str().to_string(),
_ => format!("{term:?}"),
}
}
fn extract_iri_string(term: &Term) -> Result<String, OxirsError> {
match term {
Term::NamedNode(nn) => Ok(nn.as_str().to_string()),
_ => Err(OxirsError::Query(format!("Expected IRI, got: {term:?}"))),
}
}
fn extract_arg_iri(arg: &PropertyFunctionArg) -> Result<String, OxirsError> {
match arg {
PropertyFunctionArg::Term(term) => extract_iri_string(term),
PropertyFunctionArg::List(args) => {
if let Some(PropertyFunctionArg::Term(term)) = args.first() {
extract_iri_string(term)
} else {
Err(OxirsError::Query(
"Expected IRI argument in list".to_string(),
))
}
}
PropertyFunctionArg::Variable(v) => Err(OxirsError::Query(format!(
"Expected bound IRI, got unbound variable ?{v}"
))),
}
}
fn extract_arg_string(arg: &PropertyFunctionArg) -> Result<String, OxirsError> {
match arg {
PropertyFunctionArg::Term(term) => Ok(extract_string_value(term)),
PropertyFunctionArg::List(args) => {
if let Some(PropertyFunctionArg::Term(term)) = args.first() {
Ok(extract_string_value(term))
} else {
Err(OxirsError::Query(
"Expected string argument in list".to_string(),
))
}
}
PropertyFunctionArg::Variable(v) => Err(OxirsError::Query(format!(
"Expected bound string, got unbound variable ?{v}"
))),
}
}
fn split_iri(iri: &str) -> (String, String) {
if let Some(pos) = iri.rfind('#') {
(iri[..=pos].to_string(), iri[pos + 1..].to_string())
} else if let Some(pos) = iri.rfind('/') {
(iri[..=pos].to_string(), iri[pos + 1..].to_string())
} else if let Some(pos) = iri.rfind(':') {
(iri[..=pos].to_string(), iri[pos + 1..].to_string())
} else {
(String::new(), iri.to_string())
}
}
#[cfg(test)]
#[path = "property_function_registry_tests.rs"]
mod tests;