use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet, VecDeque};
use tracing::debug;
use crate::model::{StarTerm, StarTriple};
use crate::store::StarStore;
use crate::StarResult;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PropertyPath {
Predicate(String),
Inverse(Box<PropertyPath>),
Sequence(Box<PropertyPath>, Box<PropertyPath>),
Alternative(Box<PropertyPath>, Box<PropertyPath>),
ZeroOrMore(Box<PropertyPath>),
OneOrMore(Box<PropertyPath>),
ZeroOrOne(Box<PropertyPath>),
NegatedPropertySet(Vec<String>),
}
impl PropertyPath {
pub fn predicate(iri: impl Into<String>) -> Self {
Self::Predicate(iri.into())
}
pub fn inverse(path: PropertyPath) -> Self {
Self::Inverse(Box::new(path))
}
pub fn sequence(first: PropertyPath, second: PropertyPath) -> Self {
Self::Sequence(Box::new(first), Box::new(second))
}
pub fn alternative(first: PropertyPath, second: PropertyPath) -> Self {
Self::Alternative(Box::new(first), Box::new(second))
}
pub fn zero_or_more(iri: impl Into<String>) -> Self {
Self::ZeroOrMore(Box::new(Self::Predicate(iri.into())))
}
pub fn one_or_more(iri: impl Into<String>) -> Self {
Self::OneOrMore(Box::new(Self::Predicate(iri.into())))
}
pub fn zero_or_one(iri: impl Into<String>) -> Self {
Self::ZeroOrOne(Box::new(Self::Predicate(iri.into())))
}
}
pub struct PropertyPathExecutor {
max_path_length: usize,
#[allow(dead_code)]
cache: HashMap<String, Vec<PathResult>>,
}
#[derive(Debug, Clone)]
pub struct PathResult {
pub start: StarTerm,
pub end: StarTerm,
pub path: Vec<StarTerm>,
}
impl PropertyPathExecutor {
pub fn new() -> Self {
Self {
max_path_length: 100,
cache: HashMap::new(),
}
}
pub fn evaluate_path(
&self,
store: &StarStore,
path: &PropertyPath,
start: Option<&StarTerm>,
end: Option<&StarTerm>,
) -> StarResult<Vec<PathResult>> {
debug!("Evaluating property path: {:?}", path);
match path {
PropertyPath::Predicate(predicate) => {
self.evaluate_predicate(store, predicate, start, end)
}
PropertyPath::Inverse(inner_path) => {
self.evaluate_path(store, inner_path, end, start)
}
PropertyPath::Sequence(first, second) => {
self.evaluate_sequence(store, first, second, start, end)
}
PropertyPath::Alternative(first, second) => {
self.evaluate_alternative(store, first, second, start, end)
}
PropertyPath::ZeroOrMore(inner_path) => {
self.evaluate_zero_or_more(store, inner_path, start, end)
}
PropertyPath::OneOrMore(inner_path) => {
self.evaluate_one_or_more(store, inner_path, start, end)
}
PropertyPath::ZeroOrOne(inner_path) => {
self.evaluate_zero_or_one(store, inner_path, start, end)
}
PropertyPath::NegatedPropertySet(predicates) => {
self.evaluate_negated_set(store, predicates, start, end)
}
}
}
fn evaluate_predicate(
&self,
store: &StarStore,
predicate: &str,
start: Option<&StarTerm>,
end: Option<&StarTerm>,
) -> StarResult<Vec<PathResult>> {
let pred_term = StarTerm::iri(predicate)?;
let triples = store.query(start, Some(&pred_term), end)?;
Ok(triples
.into_iter()
.map(|triple| PathResult {
start: triple.subject.clone(),
end: triple.object.clone(),
path: vec![triple.predicate.clone()],
})
.collect())
}
fn evaluate_sequence(
&self,
store: &StarStore,
first: &PropertyPath,
second: &PropertyPath,
start: Option<&StarTerm>,
end: Option<&StarTerm>,
) -> StarResult<Vec<PathResult>> {
let first_results = self.evaluate_path(store, first, start, None)?;
let mut final_results = Vec::new();
for first_result in first_results {
let second_results = self.evaluate_path(store, second, Some(&first_result.end), end)?;
for second_result in second_results {
let mut combined_path = first_result.path.clone();
combined_path.extend(second_result.path);
final_results.push(PathResult {
start: first_result.start.clone(),
end: second_result.end,
path: combined_path,
});
}
}
Ok(final_results)
}
fn evaluate_alternative(
&self,
store: &StarStore,
first: &PropertyPath,
second: &PropertyPath,
start: Option<&StarTerm>,
end: Option<&StarTerm>,
) -> StarResult<Vec<PathResult>> {
let mut results = self.evaluate_path(store, first, start, end)?;
let mut second_results = self.evaluate_path(store, second, start, end)?;
results.append(&mut second_results);
let mut seen = HashSet::new();
results.retain(|r| {
let key = format!("{}->{}", r.start, r.end);
seen.insert(key)
});
Ok(results)
}
fn evaluate_zero_or_more(
&self,
store: &StarStore,
path: &PropertyPath,
start: Option<&StarTerm>,
end: Option<&StarTerm>,
) -> StarResult<Vec<PathResult>> {
let mut results = Vec::new();
let mut visited = HashSet::new();
let mut queue = VecDeque::new();
let start_nodes = if let Some(s) = start {
vec![s.clone()]
} else {
let all_triples = store.query(None, None, None)?;
all_triples
.into_iter()
.map(|t| t.subject)
.collect::<HashSet<_>>()
.into_iter()
.collect()
};
for start_node in start_nodes {
queue.push_back((start_node.clone(), vec![]));
visited.insert(format!("{}", start_node));
if end.is_none() || end == Some(&start_node) {
results.push(PathResult {
start: start_node.clone(),
end: start_node.clone(),
path: vec![],
});
}
while let Some((current, current_path)) = queue.pop_front() {
if current_path.len() >= self.max_path_length {
continue;
}
let one_step = self.evaluate_path(store, path, Some(¤t), None)?;
for step_result in one_step {
let key = format!("{}", step_result.end);
if !visited.contains(&key) {
visited.insert(key);
let mut new_path = current_path.clone();
new_path.extend(step_result.path);
if end.is_none() || end == Some(&step_result.end) {
results.push(PathResult {
start: start_node.clone(),
end: step_result.end.clone(),
path: new_path.clone(),
});
}
queue.push_back((step_result.end, new_path));
}
}
}
}
Ok(results)
}
fn evaluate_one_or_more(
&self,
store: &StarStore,
path: &PropertyPath,
start: Option<&StarTerm>,
end: Option<&StarTerm>,
) -> StarResult<Vec<PathResult>> {
let mut results = self.evaluate_path(store, path, start, end)?;
let zero_or_more_path = PropertyPath::ZeroOrMore(Box::new(path.clone()));
for result in results.clone() {
let additional =
self.evaluate_path(store, &zero_or_more_path, Some(&result.end), end)?;
for add_result in additional {
if add_result.start != add_result.end {
let mut combined_path = result.path.clone();
combined_path.extend(add_result.path);
results.push(PathResult {
start: result.start.clone(),
end: add_result.end,
path: combined_path,
});
}
}
}
Ok(results)
}
fn evaluate_zero_or_one(
&self,
store: &StarStore,
path: &PropertyPath,
start: Option<&StarTerm>,
end: Option<&StarTerm>,
) -> StarResult<Vec<PathResult>> {
let mut results = Vec::new();
if let Some(s) = start {
if end.is_none() || end == Some(s) {
results.push(PathResult {
start: s.clone(),
end: s.clone(),
path: vec![],
});
}
}
let mut one_step = self.evaluate_path(store, path, start, end)?;
results.append(&mut one_step);
Ok(results)
}
fn evaluate_negated_set(
&self,
store: &StarStore,
predicates: &[String],
start: Option<&StarTerm>,
end: Option<&StarTerm>,
) -> StarResult<Vec<PathResult>> {
let all_triples = store.query(start, None, end)?;
let results = all_triples
.into_iter()
.filter(|triple| {
if let StarTerm::NamedNode(nn) = &triple.predicate {
!predicates.contains(&nn.iri)
} else {
true
}
})
.map(|triple| PathResult {
start: triple.subject.clone(),
end: triple.object.clone(),
path: vec![triple.predicate.clone()],
})
.collect();
Ok(results)
}
}
impl Default for PropertyPathExecutor {
fn default() -> Self {
Self::new()
}
}
pub struct FederatedQueryExecutor {
endpoints: HashMap<String, String>,
}
impl FederatedQueryExecutor {
pub fn new() -> Self {
Self {
endpoints: HashMap::new(),
}
}
pub fn register_endpoint(&mut self, name: impl Into<String>, url: impl Into<String>) {
self.endpoints.insert(name.into(), url.into());
}
pub fn execute_federated(
&self,
_endpoint: &str,
_query: &str,
) -> StarResult<Vec<HashMap<String, StarTerm>>> {
Ok(Vec::new())
}
}
impl Default for FederatedQueryExecutor {
fn default() -> Self {
Self::new()
}
}
pub struct FullTextSearch {
index: HashMap<String, Vec<StarTriple>>,
}
impl FullTextSearch {
pub fn new() -> Self {
Self {
index: HashMap::new(),
}
}
pub fn index_store(&mut self, store: &StarStore) -> StarResult<()> {
let triples = store.query(None, None, None)?;
for triple in triples {
if let StarTerm::Literal(lit) = &triple.object {
let words: Vec<String> = lit
.value
.split_whitespace()
.map(|s| s.to_lowercase())
.collect();
for word in words {
self.index.entry(word).or_default().push(triple.clone());
}
}
}
Ok(())
}
pub fn search(&self, term: &str) -> Vec<StarTriple> {
let normalized = term.to_lowercase();
self.index.get(&normalized).cloned().unwrap_or_default()
}
pub fn search_wildcard(&self, pattern: &str) -> Vec<StarTriple> {
let pattern_lower = pattern.to_lowercase();
let mut results = Vec::new();
for (word, triples) in &self.index {
if Self::matches_wildcard(word, &pattern_lower) {
results.extend(triples.clone());
}
}
let mut seen = HashSet::new();
results.retain(|t| seen.insert(format!("{}", t)));
results
}
fn matches_wildcard(text: &str, pattern: &str) -> bool {
if pattern.contains('*') {
let parts: Vec<&str> = pattern.split('*').collect();
if parts.is_empty() {
return true;
}
let mut pos = 0;
for (i, part) in parts.iter().enumerate() {
if part.is_empty() {
continue;
}
if i == 0 {
if !text.starts_with(part) {
return false;
}
pos = part.len();
} else if i == parts.len() - 1 {
return text.ends_with(part);
} else {
if let Some(found_pos) = text[pos..].find(part) {
pos += found_pos + part.len();
} else {
return false;
}
}
}
true
} else {
text == pattern
}
}
}
impl Default for FullTextSearch {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{StarTerm, StarTriple};
#[test]
fn test_property_path_predicate() -> StarResult<()> {
let store = StarStore::new();
let triple = StarTriple::new(
StarTerm::iri("http://example.org/alice")?,
StarTerm::iri("http://xmlns.com/foaf/0.1/knows")?,
StarTerm::iri("http://example.org/bob")?,
);
store.insert(&triple)?;
let executor = PropertyPathExecutor::new();
let path = PropertyPath::predicate("http://xmlns.com/foaf/0.1/knows");
let results = executor.evaluate_path(&store, &path, None, None)?;
assert_eq!(results.len(), 1);
assert_eq!(
format!("{}", results[0].start),
"<http://example.org/alice>"
);
Ok(())
}
#[test]
fn test_property_path_alternative() -> StarResult<()> {
let store = StarStore::new();
store.insert(&StarTriple::new(
StarTerm::iri("http://example.org/alice")?,
StarTerm::iri("http://xmlns.com/foaf/0.1/knows")?,
StarTerm::iri("http://example.org/bob")?,
))?;
store.insert(&StarTriple::new(
StarTerm::iri("http://example.org/alice")?,
StarTerm::iri("http://xmlns.com/foaf/0.1/worksWith")?,
StarTerm::iri("http://example.org/charlie")?,
))?;
let executor = PropertyPathExecutor::new();
let path = PropertyPath::alternative(
PropertyPath::predicate("http://xmlns.com/foaf/0.1/knows"),
PropertyPath::predicate("http://xmlns.com/foaf/0.1/worksWith"),
);
let results = executor.evaluate_path(&store, &path, None, None)?;
assert_eq!(results.len(), 2);
Ok(())
}
#[test]
fn test_full_text_search() -> StarResult<()> {
let store = StarStore::new();
store.insert(&StarTriple::new(
StarTerm::iri("http://example.org/doc1")?,
StarTerm::iri("http://purl.org/dc/terms/title")?,
StarTerm::literal("The Quick Brown Fox")?,
))?;
store.insert(&StarTriple::new(
StarTerm::iri("http://example.org/doc2")?,
StarTerm::iri("http://purl.org/dc/terms/title")?,
StarTerm::literal("A Slow Brown Dog")?,
))?;
let mut fts = FullTextSearch::new();
fts.index_store(&store)?;
let results = fts.search("brown");
assert_eq!(results.len(), 2);
let results = fts.search("quick");
assert_eq!(results.len(), 1);
Ok(())
}
#[test]
fn test_wildcard_search() -> StarResult<()> {
let store = StarStore::new();
store.insert(&StarTriple::new(
StarTerm::iri("http://example.org/doc1")?,
StarTerm::iri("http://purl.org/dc/terms/title")?,
StarTerm::literal("testing wildcards")?,
))?;
let mut fts = FullTextSearch::new();
fts.index_store(&store)?;
let results = fts.search_wildcard("test*");
assert_eq!(results.len(), 1);
Ok(())
}
}