1use crate::model::Term;
38use crate::OxirsError;
39use serde::{Deserialize, Serialize};
40use std::collections::HashMap;
41use std::fmt;
42use std::sync::Arc;
43
44#[derive(Debug, Clone, PartialEq)]
47pub enum PropertyFunctionArg {
48 Term(Term),
50 Variable(String),
52 List(Vec<PropertyFunctionArg>),
54}
55
56impl fmt::Display for PropertyFunctionArg {
57 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58 match self {
59 PropertyFunctionArg::Term(t) => write!(f, "{t:?}"),
60 PropertyFunctionArg::Variable(v) => write!(f, "?{v}"),
61 PropertyFunctionArg::List(args) => {
62 write!(f, "(")?;
63 for (i, a) in args.iter().enumerate() {
64 if i > 0 {
65 write!(f, " ")?;
66 }
67 write!(f, "{a}")?;
68 }
69 write!(f, ")")
70 }
71 }
72 }
73}
74
75#[derive(Debug, Clone, PartialEq)]
77pub struct PropertyFunctionBinding {
78 bindings: HashMap<String, Term>,
80}
81
82impl PropertyFunctionBinding {
83 pub fn new() -> Self {
85 Self {
86 bindings: HashMap::new(),
87 }
88 }
89
90 pub fn bind(mut self, var: impl Into<String>, term: Term) -> Self {
92 self.bindings.insert(var.into(), term);
93 self
94 }
95
96 pub fn get(&self, var: &str) -> Option<&Term> {
98 self.bindings.get(var)
99 }
100
101 pub fn bindings(&self) -> &HashMap<String, Term> {
103 &self.bindings
104 }
105
106 pub fn len(&self) -> usize {
108 self.bindings.len()
109 }
110
111 pub fn is_empty(&self) -> bool {
113 self.bindings.is_empty()
114 }
115}
116
117impl Default for PropertyFunctionBinding {
118 fn default() -> Self {
119 Self::new()
120 }
121}
122
123#[derive(Debug, Clone)]
125pub struct PropertyFunctionResult {
126 rows: Vec<PropertyFunctionBinding>,
128}
129
130impl PropertyFunctionResult {
131 pub fn empty() -> Self {
133 Self { rows: Vec::new() }
134 }
135
136 pub fn from_rows(rows: Vec<PropertyFunctionBinding>) -> Self {
138 Self { rows }
139 }
140
141 pub fn single(binding: PropertyFunctionBinding) -> Self {
143 Self {
144 rows: vec![binding],
145 }
146 }
147
148 pub fn rows(&self) -> &[PropertyFunctionBinding] {
150 &self.rows
151 }
152
153 pub fn len(&self) -> usize {
155 self.rows.len()
156 }
157
158 pub fn is_empty(&self) -> bool {
160 self.rows.is_empty()
161 }
162
163 pub fn iter(&self) -> impl Iterator<Item = &PropertyFunctionBinding> {
165 self.rows.iter()
166 }
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct PropertyFunctionMetadata {
172 pub iri: String,
174 pub name: String,
176 pub description: String,
178 pub subject_must_be_bound: bool,
180 pub object_must_be_bound: bool,
182 pub min_subject_args: usize,
184 pub max_subject_args: Option<usize>,
186 pub min_object_args: usize,
188 pub max_object_args: Option<usize>,
190 pub category: String,
192}
193
194pub trait PropertyFunction: Send + Sync + fmt::Debug {
199 fn metadata(&self) -> PropertyFunctionMetadata;
201
202 fn evaluate(
211 &self,
212 subject: &PropertyFunctionArg,
213 object: &PropertyFunctionArg,
214 ) -> Result<PropertyFunctionResult, OxirsError>;
215
216 fn validate(
218 &self,
219 subject: &PropertyFunctionArg,
220 object: &PropertyFunctionArg,
221 ) -> Result<(), OxirsError> {
222 let meta = self.metadata();
223
224 if let PropertyFunctionArg::List(args) = subject {
226 if args.len() < meta.min_subject_args {
227 return Err(OxirsError::Query(format!(
228 "Property function {} requires at least {} subject arguments, got {}",
229 meta.iri,
230 meta.min_subject_args,
231 args.len()
232 )));
233 }
234 if let Some(max) = meta.max_subject_args {
235 if args.len() > max {
236 return Err(OxirsError::Query(format!(
237 "Property function {} accepts at most {} subject arguments, got {}",
238 meta.iri,
239 max,
240 args.len()
241 )));
242 }
243 }
244 }
245
246 if let PropertyFunctionArg::List(args) = object {
248 if args.len() < meta.min_object_args {
249 return Err(OxirsError::Query(format!(
250 "Property function {} requires at least {} object arguments, got {}",
251 meta.iri,
252 meta.min_object_args,
253 args.len()
254 )));
255 }
256 if let Some(max) = meta.max_object_args {
257 if args.len() > max {
258 return Err(OxirsError::Query(format!(
259 "Property function {} accepts at most {} object arguments, got {}",
260 meta.iri,
261 max,
262 args.len()
263 )));
264 }
265 }
266 }
267
268 Ok(())
269 }
270
271 fn estimated_cardinality(
274 &self,
275 _subject: &PropertyFunctionArg,
276 _object: &PropertyFunctionArg,
277 ) -> Option<u64> {
278 None
279 }
280}
281
282pub trait PropertyFunctionFactory: Send + Sync {
284 fn create(&self, iri: &str) -> Result<Box<dyn PropertyFunction>, OxirsError>;
286}
287
288pub struct PropertyFunctionRegistry {
290 functions: HashMap<String, Arc<dyn PropertyFunction>>,
292 factories: HashMap<String, Arc<dyn PropertyFunctionFactory>>,
294 stats: PropertyFunctionStats,
296}
297
298#[derive(Debug, Clone, Default)]
300pub struct PropertyFunctionStats {
301 pub total_evaluations: u64,
303 pub total_rows_produced: u64,
305 pub total_errors: u64,
307 pub per_function_counts: HashMap<String, u64>,
309}
310
311impl Default for PropertyFunctionRegistry {
312 fn default() -> Self {
313 let mut registry = Self {
314 functions: HashMap::new(),
315 factories: HashMap::new(),
316 stats: PropertyFunctionStats::default(),
317 };
318 registry.register_builtins();
319 registry
320 }
321}
322
323impl PropertyFunctionRegistry {
324 pub fn new() -> Self {
326 Self::default()
327 }
328
329 pub fn empty() -> Self {
331 Self {
332 functions: HashMap::new(),
333 factories: HashMap::new(),
334 stats: PropertyFunctionStats::default(),
335 }
336 }
337
338 pub fn register(&mut self, iri: impl Into<String>, func: Arc<dyn PropertyFunction>) {
340 self.functions.insert(iri.into(), func);
341 }
342
343 pub fn register_factory(
345 &mut self,
346 iri: impl Into<String>,
347 factory: Arc<dyn PropertyFunctionFactory>,
348 ) {
349 self.factories.insert(iri.into(), factory);
350 }
351
352 pub fn unregister(&mut self, iri: &str) -> bool {
354 let removed_func = self.functions.remove(iri).is_some();
355 let removed_factory = self.factories.remove(iri).is_some();
356 removed_func || removed_factory
357 }
358
359 pub fn is_property_function(&self, iri: &str) -> bool {
361 self.functions.contains_key(iri) || self.factories.contains_key(iri)
362 }
363
364 pub fn lookup(&mut self, iri: &str) -> Option<Arc<dyn PropertyFunction>> {
366 if let Some(func) = self.functions.get(iri) {
368 return Some(Arc::clone(func));
369 }
370
371 if let Some(factory) = self.factories.get(iri) {
373 match factory.create(iri) {
374 Ok(func) => {
375 let func: Arc<dyn PropertyFunction> = Arc::from(func);
376 self.functions.insert(iri.to_string(), Arc::clone(&func));
377 return Some(func);
378 }
379 Err(_) => return None,
380 }
381 }
382
383 None
384 }
385
386 pub fn evaluate(
388 &mut self,
389 iri: &str,
390 subject: &PropertyFunctionArg,
391 object: &PropertyFunctionArg,
392 ) -> Result<PropertyFunctionResult, OxirsError> {
393 let func = self
394 .lookup(iri)
395 .ok_or_else(|| OxirsError::Query(format!("Unknown property function: {iri}")))?;
396
397 func.validate(subject, object)?;
399
400 let result = func.evaluate(subject, object);
402
403 self.stats.total_evaluations += 1;
405 *self
406 .stats
407 .per_function_counts
408 .entry(iri.to_string())
409 .or_insert(0) += 1;
410
411 match &result {
412 Ok(r) => {
413 self.stats.total_rows_produced += r.len() as u64;
414 }
415 Err(_) => {
416 self.stats.total_errors += 1;
417 }
418 }
419
420 result
421 }
422
423 pub fn registered_iris(&self) -> Vec<String> {
425 let mut iris: Vec<String> = self.functions.keys().cloned().collect();
426 for iri in self.factories.keys() {
427 if !iris.contains(iri) {
428 iris.push(iri.clone());
429 }
430 }
431 iris.sort();
432 iris
433 }
434
435 pub fn all_metadata(&self) -> Vec<PropertyFunctionMetadata> {
437 self.functions.values().map(|f| f.metadata()).collect()
438 }
439
440 pub fn statistics(&self) -> &PropertyFunctionStats {
442 &self.stats
443 }
444
445 pub fn reset_statistics(&mut self) {
447 self.stats = PropertyFunctionStats::default();
448 }
449
450 pub fn len(&self) -> usize {
452 let mut count = self.functions.len();
453 for iri in self.factories.keys() {
454 if !self.functions.contains_key(iri) {
455 count += 1;
456 }
457 }
458 count
459 }
460
461 pub fn is_empty(&self) -> bool {
463 self.functions.is_empty() && self.factories.is_empty()
464 }
465
466 fn register_builtins(&mut self) {
468 self.register(
470 "http://jena.apache.org/ARQ/list#member",
471 Arc::new(ListMemberPF::new()),
472 );
473 self.register(
474 "http://jena.apache.org/ARQ/list#index",
475 Arc::new(ListIndexPF::new()),
476 );
477 self.register(
478 "http://jena.apache.org/ARQ/list#length",
479 Arc::new(ListLengthPF::new()),
480 );
481
482 self.register(
484 "http://jena.apache.org/ARQ/property#splitIRI",
485 Arc::new(SplitIriPF::new()),
486 );
487 self.register(
488 "http://jena.apache.org/ARQ/property#localname",
489 Arc::new(LocalNamePF::new()),
490 );
491 self.register(
492 "http://jena.apache.org/ARQ/property#namespace",
493 Arc::new(NamespacePF::new()),
494 );
495
496 self.register(
498 "http://jena.apache.org/text#search",
499 Arc::new(TextSearchPF::new()),
500 );
501
502 self.register(
504 "http://jena.apache.org/ARQ/property#concat",
505 Arc::new(ConcatPF::new()),
506 );
507
508 self.register(
510 "http://jena.apache.org/ARQ/property#strSplit",
511 Arc::new(StrSplitPF::new()),
512 );
513 }
514}
515
516impl fmt::Debug for PropertyFunctionRegistry {
517 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
518 f.debug_struct("PropertyFunctionRegistry")
519 .field("registered_count", &self.len())
520 .field("stats", &self.stats)
521 .finish()
522 }
523}
524
525#[derive(Debug)]
535pub struct ListMemberPF {
536 _private: (),
537}
538
539impl ListMemberPF {
540 pub fn new() -> Self {
541 Self { _private: () }
542 }
543}
544
545impl Default for ListMemberPF {
546 fn default() -> Self {
547 Self::new()
548 }
549}
550
551impl PropertyFunction for ListMemberPF {
552 fn metadata(&self) -> PropertyFunctionMetadata {
553 PropertyFunctionMetadata {
554 iri: "http://jena.apache.org/ARQ/list#member".to_string(),
555 name: "list:member".to_string(),
556 description: "Enumerates all members of an RDF list".to_string(),
557 subject_must_be_bound: false,
558 object_must_be_bound: false,
559 min_subject_args: 0,
560 max_subject_args: None,
561 min_object_args: 0,
562 max_object_args: None,
563 category: "list".to_string(),
564 }
565 }
566
567 fn evaluate(
568 &self,
569 subject: &PropertyFunctionArg,
570 _object: &PropertyFunctionArg,
571 ) -> Result<PropertyFunctionResult, OxirsError> {
572 match subject {
574 PropertyFunctionArg::List(members) => {
575 let mut rows = Vec::new();
576 for (i, member) in members.iter().enumerate() {
577 if let PropertyFunctionArg::Term(term) = member {
578 let binding = PropertyFunctionBinding::new()
579 .bind("index", make_integer_term(i as i64))
580 .bind("member", term.clone());
581 rows.push(binding);
582 }
583 }
584 Ok(PropertyFunctionResult::from_rows(rows))
585 }
586 PropertyFunctionArg::Term(term) => {
587 let binding = PropertyFunctionBinding::new()
589 .bind("index", make_integer_term(0))
590 .bind("member", term.clone());
591 Ok(PropertyFunctionResult::single(binding))
592 }
593 PropertyFunctionArg::Variable(_) => {
594 Ok(PropertyFunctionResult::empty())
596 }
597 }
598 }
599
600 fn estimated_cardinality(
601 &self,
602 subject: &PropertyFunctionArg,
603 _object: &PropertyFunctionArg,
604 ) -> Option<u64> {
605 match subject {
606 PropertyFunctionArg::List(members) => Some(members.len() as u64),
607 PropertyFunctionArg::Term(_) => Some(1),
608 PropertyFunctionArg::Variable(_) => None,
609 }
610 }
611}
612
613#[derive(Debug)]
617pub struct ListIndexPF {
618 _private: (),
619}
620
621impl ListIndexPF {
622 pub fn new() -> Self {
623 Self { _private: () }
624 }
625}
626
627impl Default for ListIndexPF {
628 fn default() -> Self {
629 Self::new()
630 }
631}
632
633impl PropertyFunction for ListIndexPF {
634 fn metadata(&self) -> PropertyFunctionMetadata {
635 PropertyFunctionMetadata {
636 iri: "http://jena.apache.org/ARQ/list#index".to_string(),
637 name: "list:index".to_string(),
638 description: "Returns (index, member) pairs for an RDF list".to_string(),
639 subject_must_be_bound: false,
640 object_must_be_bound: false,
641 min_subject_args: 0,
642 max_subject_args: None,
643 min_object_args: 0,
644 max_object_args: Some(2),
645 category: "list".to_string(),
646 }
647 }
648
649 fn evaluate(
650 &self,
651 subject: &PropertyFunctionArg,
652 _object: &PropertyFunctionArg,
653 ) -> Result<PropertyFunctionResult, OxirsError> {
654 match subject {
655 PropertyFunctionArg::List(members) => {
656 let mut rows = Vec::new();
657 for (i, member) in members.iter().enumerate() {
658 if let PropertyFunctionArg::Term(term) = member {
659 let binding = PropertyFunctionBinding::new()
660 .bind("index", make_integer_term(i as i64))
661 .bind("item", term.clone());
662 rows.push(binding);
663 }
664 }
665 Ok(PropertyFunctionResult::from_rows(rows))
666 }
667 _ => Ok(PropertyFunctionResult::empty()),
668 }
669 }
670}
671
672#[derive(Debug)]
676pub struct ListLengthPF {
677 _private: (),
678}
679
680impl ListLengthPF {
681 pub fn new() -> Self {
682 Self { _private: () }
683 }
684}
685
686impl Default for ListLengthPF {
687 fn default() -> Self {
688 Self::new()
689 }
690}
691
692impl PropertyFunction for ListLengthPF {
693 fn metadata(&self) -> PropertyFunctionMetadata {
694 PropertyFunctionMetadata {
695 iri: "http://jena.apache.org/ARQ/list#length".to_string(),
696 name: "list:length".to_string(),
697 description: "Returns the length of an RDF list".to_string(),
698 subject_must_be_bound: true,
699 object_must_be_bound: false,
700 min_subject_args: 0,
701 max_subject_args: None,
702 min_object_args: 0,
703 max_object_args: Some(1),
704 category: "list".to_string(),
705 }
706 }
707
708 fn evaluate(
709 &self,
710 subject: &PropertyFunctionArg,
711 _object: &PropertyFunctionArg,
712 ) -> Result<PropertyFunctionResult, OxirsError> {
713 let len = match subject {
714 PropertyFunctionArg::List(members) => members.len(),
715 PropertyFunctionArg::Term(_) => 1,
716 PropertyFunctionArg::Variable(_) => {
717 return Err(OxirsError::Query(
718 "list:length requires a bound subject".to_string(),
719 ));
720 }
721 };
722
723 let binding = PropertyFunctionBinding::new().bind("length", make_integer_term(len as i64));
724 Ok(PropertyFunctionResult::single(binding))
725 }
726
727 fn estimated_cardinality(
728 &self,
729 _subject: &PropertyFunctionArg,
730 _object: &PropertyFunctionArg,
731 ) -> Option<u64> {
732 Some(1) }
734}
735
736#[derive(Debug)]
740pub struct SplitIriPF {
741 _private: (),
742}
743
744impl SplitIriPF {
745 pub fn new() -> Self {
746 Self { _private: () }
747 }
748}
749
750impl Default for SplitIriPF {
751 fn default() -> Self {
752 Self::new()
753 }
754}
755
756impl PropertyFunction for SplitIriPF {
757 fn metadata(&self) -> PropertyFunctionMetadata {
758 PropertyFunctionMetadata {
759 iri: "http://jena.apache.org/ARQ/property#splitIRI".to_string(),
760 name: "apf:splitIRI".to_string(),
761 description: "Splits an IRI into namespace and local name".to_string(),
762 subject_must_be_bound: true,
763 object_must_be_bound: false,
764 min_subject_args: 1,
765 max_subject_args: Some(1),
766 min_object_args: 0,
767 max_object_args: Some(2),
768 category: "string".to_string(),
769 }
770 }
771
772 fn evaluate(
773 &self,
774 subject: &PropertyFunctionArg,
775 _object: &PropertyFunctionArg,
776 ) -> Result<PropertyFunctionResult, OxirsError> {
777 let iri_str = match subject {
778 PropertyFunctionArg::Term(term) => extract_iri_string(term)?,
779 PropertyFunctionArg::Variable(_) => {
780 return Err(OxirsError::Query(
781 "apf:splitIRI requires a bound IRI subject".to_string(),
782 ));
783 }
784 PropertyFunctionArg::List(args) => {
785 if let Some(PropertyFunctionArg::Term(term)) = args.first() {
786 extract_iri_string(term)?
787 } else {
788 return Err(OxirsError::Query(
789 "apf:splitIRI requires an IRI argument".to_string(),
790 ));
791 }
792 }
793 };
794
795 let (namespace, local_name) = split_iri(&iri_str);
797
798 let binding = PropertyFunctionBinding::new()
799 .bind("namespace", make_string_term(&namespace))
800 .bind("localname", make_string_term(&local_name));
801
802 Ok(PropertyFunctionResult::single(binding))
803 }
804
805 fn estimated_cardinality(
806 &self,
807 _subject: &PropertyFunctionArg,
808 _object: &PropertyFunctionArg,
809 ) -> Option<u64> {
810 Some(1)
811 }
812}
813
814#[derive(Debug)]
816pub struct LocalNamePF {
817 _private: (),
818}
819
820impl LocalNamePF {
821 pub fn new() -> Self {
822 Self { _private: () }
823 }
824}
825
826impl Default for LocalNamePF {
827 fn default() -> Self {
828 Self::new()
829 }
830}
831
832impl PropertyFunction for LocalNamePF {
833 fn metadata(&self) -> PropertyFunctionMetadata {
834 PropertyFunctionMetadata {
835 iri: "http://jena.apache.org/ARQ/property#localname".to_string(),
836 name: "apf:localname".to_string(),
837 description: "Extracts the local name from an IRI".to_string(),
838 subject_must_be_bound: true,
839 object_must_be_bound: false,
840 min_subject_args: 1,
841 max_subject_args: Some(1),
842 min_object_args: 0,
843 max_object_args: Some(1),
844 category: "string".to_string(),
845 }
846 }
847
848 fn evaluate(
849 &self,
850 subject: &PropertyFunctionArg,
851 _object: &PropertyFunctionArg,
852 ) -> Result<PropertyFunctionResult, OxirsError> {
853 let iri_str = extract_arg_iri(subject)?;
854 let (_, local_name) = split_iri(&iri_str);
855
856 let binding =
857 PropertyFunctionBinding::new().bind("localname", make_string_term(&local_name));
858 Ok(PropertyFunctionResult::single(binding))
859 }
860
861 fn estimated_cardinality(
862 &self,
863 _subject: &PropertyFunctionArg,
864 _object: &PropertyFunctionArg,
865 ) -> Option<u64> {
866 Some(1)
867 }
868}
869
870#[derive(Debug)]
872pub struct NamespacePF {
873 _private: (),
874}
875
876impl NamespacePF {
877 pub fn new() -> Self {
878 Self { _private: () }
879 }
880}
881
882impl Default for NamespacePF {
883 fn default() -> Self {
884 Self::new()
885 }
886}
887
888impl PropertyFunction for NamespacePF {
889 fn metadata(&self) -> PropertyFunctionMetadata {
890 PropertyFunctionMetadata {
891 iri: "http://jena.apache.org/ARQ/property#namespace".to_string(),
892 name: "apf:namespace".to_string(),
893 description: "Extracts the namespace from an IRI".to_string(),
894 subject_must_be_bound: true,
895 object_must_be_bound: false,
896 min_subject_args: 1,
897 max_subject_args: Some(1),
898 min_object_args: 0,
899 max_object_args: Some(1),
900 category: "string".to_string(),
901 }
902 }
903
904 fn evaluate(
905 &self,
906 subject: &PropertyFunctionArg,
907 _object: &PropertyFunctionArg,
908 ) -> Result<PropertyFunctionResult, OxirsError> {
909 let iri_str = extract_arg_iri(subject)?;
910 let (namespace, _) = split_iri(&iri_str);
911
912 let binding =
913 PropertyFunctionBinding::new().bind("namespace", make_string_term(&namespace));
914 Ok(PropertyFunctionResult::single(binding))
915 }
916}
917
918#[derive(Debug)]
922pub struct TextSearchPF {
923 _private: (),
924}
925
926impl TextSearchPF {
927 pub fn new() -> Self {
928 Self { _private: () }
929 }
930}
931
932impl Default for TextSearchPF {
933 fn default() -> Self {
934 Self::new()
935 }
936}
937
938impl PropertyFunction for TextSearchPF {
939 fn metadata(&self) -> PropertyFunctionMetadata {
940 PropertyFunctionMetadata {
941 iri: "http://jena.apache.org/text#search".to_string(),
942 name: "text:search".to_string(),
943 description: "Full-text search across literal values".to_string(),
944 subject_must_be_bound: false,
945 object_must_be_bound: true,
946 min_subject_args: 0,
947 max_subject_args: None,
948 min_object_args: 1,
949 max_object_args: Some(3),
950 category: "text".to_string(),
951 }
952 }
953
954 fn evaluate(
955 &self,
956 _subject: &PropertyFunctionArg,
957 object: &PropertyFunctionArg,
958 ) -> Result<PropertyFunctionResult, OxirsError> {
959 let query = match object {
961 PropertyFunctionArg::Term(term) => extract_string_value(term),
962 PropertyFunctionArg::List(args) => {
963 if let Some(PropertyFunctionArg::Term(term)) = args.first() {
964 extract_string_value(term)
965 } else {
966 return Err(OxirsError::Query(
967 "text:search requires a search query string".to_string(),
968 ));
969 }
970 }
971 PropertyFunctionArg::Variable(_) => {
972 return Err(OxirsError::Query(
973 "text:search requires a bound search query".to_string(),
974 ));
975 }
976 };
977
978 let binding = PropertyFunctionBinding::new()
981 .bind("query", make_string_term(&query))
982 .bind("score", make_double_term(1.0));
983
984 Ok(PropertyFunctionResult::single(binding))
985 }
986}
987
988#[derive(Debug)]
992pub struct ConcatPF {
993 _private: (),
994}
995
996impl ConcatPF {
997 pub fn new() -> Self {
998 Self { _private: () }
999 }
1000}
1001
1002impl Default for ConcatPF {
1003 fn default() -> Self {
1004 Self::new()
1005 }
1006}
1007
1008impl PropertyFunction for ConcatPF {
1009 fn metadata(&self) -> PropertyFunctionMetadata {
1010 PropertyFunctionMetadata {
1011 iri: "http://jena.apache.org/ARQ/property#concat".to_string(),
1012 name: "apf:concat".to_string(),
1013 description: "Concatenates string arguments into a single string".to_string(),
1014 subject_must_be_bound: true,
1015 object_must_be_bound: false,
1016 min_subject_args: 1,
1017 max_subject_args: None,
1018 min_object_args: 0,
1019 max_object_args: Some(1),
1020 category: "string".to_string(),
1021 }
1022 }
1023
1024 fn evaluate(
1025 &self,
1026 subject: &PropertyFunctionArg,
1027 _object: &PropertyFunctionArg,
1028 ) -> Result<PropertyFunctionResult, OxirsError> {
1029 let parts: Vec<String> = match subject {
1030 PropertyFunctionArg::List(args) => args
1031 .iter()
1032 .filter_map(|a| {
1033 if let PropertyFunctionArg::Term(t) = a {
1034 Some(extract_string_value(t))
1035 } else {
1036 None
1037 }
1038 })
1039 .collect(),
1040 PropertyFunctionArg::Term(term) => vec![extract_string_value(term)],
1041 PropertyFunctionArg::Variable(_) => {
1042 return Err(OxirsError::Query(
1043 "apf:concat requires bound string arguments".to_string(),
1044 ));
1045 }
1046 };
1047
1048 let concatenated = parts.join("");
1049 let binding =
1050 PropertyFunctionBinding::new().bind("result", make_string_term(&concatenated));
1051 Ok(PropertyFunctionResult::single(binding))
1052 }
1053
1054 fn estimated_cardinality(
1055 &self,
1056 _subject: &PropertyFunctionArg,
1057 _object: &PropertyFunctionArg,
1058 ) -> Option<u64> {
1059 Some(1)
1060 }
1061}
1062
1063#[derive(Debug)]
1067pub struct StrSplitPF {
1068 _private: (),
1069}
1070
1071impl StrSplitPF {
1072 pub fn new() -> Self {
1073 Self { _private: () }
1074 }
1075}
1076
1077impl Default for StrSplitPF {
1078 fn default() -> Self {
1079 Self::new()
1080 }
1081}
1082
1083impl PropertyFunction for StrSplitPF {
1084 fn metadata(&self) -> PropertyFunctionMetadata {
1085 PropertyFunctionMetadata {
1086 iri: "http://jena.apache.org/ARQ/property#strSplit".to_string(),
1087 name: "apf:strSplit".to_string(),
1088 description: "Splits a string by a delimiter, producing multiple bindings".to_string(),
1089 subject_must_be_bound: true,
1090 object_must_be_bound: false,
1091 min_subject_args: 1,
1092 max_subject_args: Some(1),
1093 min_object_args: 1,
1094 max_object_args: Some(2),
1095 category: "string".to_string(),
1096 }
1097 }
1098
1099 fn evaluate(
1100 &self,
1101 subject: &PropertyFunctionArg,
1102 object: &PropertyFunctionArg,
1103 ) -> Result<PropertyFunctionResult, OxirsError> {
1104 let input = extract_arg_string(subject)?;
1105
1106 let delimiter = match object {
1108 PropertyFunctionArg::Term(term) => extract_string_value(term),
1109 PropertyFunctionArg::List(args) => {
1110 if let Some(PropertyFunctionArg::Term(term)) = args.first() {
1111 extract_string_value(term)
1112 } else {
1113 ",".to_string() }
1115 }
1116 PropertyFunctionArg::Variable(_) => ",".to_string(),
1117 };
1118
1119 let parts: Vec<&str> = input.split(&delimiter).collect();
1120 let mut rows = Vec::new();
1121 for (i, part) in parts.iter().enumerate() {
1122 let binding = PropertyFunctionBinding::new()
1123 .bind("index", make_integer_term(i as i64))
1124 .bind("part", make_string_term(part));
1125 rows.push(binding);
1126 }
1127
1128 Ok(PropertyFunctionResult::from_rows(rows))
1129 }
1130}
1131
1132fn make_integer_term(value: i64) -> Term {
1138 Term::Literal(crate::model::Literal::new_typed(
1139 value.to_string(),
1140 crate::model::NamedNode::new_unchecked("http://www.w3.org/2001/XMLSchema#integer"),
1141 ))
1142}
1143
1144fn make_double_term(value: f64) -> Term {
1146 Term::Literal(crate::model::Literal::new_typed(
1147 value.to_string(),
1148 crate::model::NamedNode::new_unchecked("http://www.w3.org/2001/XMLSchema#double"),
1149 ))
1150}
1151
1152fn make_string_term(value: &str) -> Term {
1154 Term::Literal(crate::model::Literal::new(value))
1155}
1156
1157fn extract_string_value(term: &Term) -> String {
1159 match term {
1160 Term::Literal(lit) => lit.value().to_string(),
1161 Term::NamedNode(nn) => nn.as_str().to_string(),
1162 Term::BlankNode(bn) => bn.as_str().to_string(),
1163 _ => format!("{term:?}"),
1164 }
1165}
1166
1167fn extract_iri_string(term: &Term) -> Result<String, OxirsError> {
1169 match term {
1170 Term::NamedNode(nn) => Ok(nn.as_str().to_string()),
1171 _ => Err(OxirsError::Query(format!("Expected IRI, got: {term:?}"))),
1172 }
1173}
1174
1175fn extract_arg_iri(arg: &PropertyFunctionArg) -> Result<String, OxirsError> {
1177 match arg {
1178 PropertyFunctionArg::Term(term) => extract_iri_string(term),
1179 PropertyFunctionArg::List(args) => {
1180 if let Some(PropertyFunctionArg::Term(term)) = args.first() {
1181 extract_iri_string(term)
1182 } else {
1183 Err(OxirsError::Query(
1184 "Expected IRI argument in list".to_string(),
1185 ))
1186 }
1187 }
1188 PropertyFunctionArg::Variable(v) => Err(OxirsError::Query(format!(
1189 "Expected bound IRI, got unbound variable ?{v}"
1190 ))),
1191 }
1192}
1193
1194fn extract_arg_string(arg: &PropertyFunctionArg) -> Result<String, OxirsError> {
1196 match arg {
1197 PropertyFunctionArg::Term(term) => Ok(extract_string_value(term)),
1198 PropertyFunctionArg::List(args) => {
1199 if let Some(PropertyFunctionArg::Term(term)) = args.first() {
1200 Ok(extract_string_value(term))
1201 } else {
1202 Err(OxirsError::Query(
1203 "Expected string argument in list".to_string(),
1204 ))
1205 }
1206 }
1207 PropertyFunctionArg::Variable(v) => Err(OxirsError::Query(format!(
1208 "Expected bound string, got unbound variable ?{v}"
1209 ))),
1210 }
1211}
1212
1213fn split_iri(iri: &str) -> (String, String) {
1215 if let Some(pos) = iri.rfind('#') {
1217 (iri[..=pos].to_string(), iri[pos + 1..].to_string())
1218 } else if let Some(pos) = iri.rfind('/') {
1219 (iri[..=pos].to_string(), iri[pos + 1..].to_string())
1220 } else if let Some(pos) = iri.rfind(':') {
1221 (iri[..=pos].to_string(), iri[pos + 1..].to_string())
1222 } else {
1223 (String::new(), iri.to_string())
1224 }
1225}
1226
1227#[cfg(test)]
1228#[path = "property_function_registry_tests.rs"]
1229mod tests;