use crate::four_sect_dict::IdKind;
use crate::hdt::Hdt;
use crate::triples::{Id, ObjectIter, PredicateIter, PredicateObjectIter, SubjectIter};
use log::debug;
use sophia::api::graph::Graph;
use sophia::api::term::matcher::TermMatcher;
use sophia::api::term::{BnodeId, IriRef, LanguageTag, Term};
use std::convert::Infallible;
use std::io::{self, Error, ErrorKind};
use std::iter;
use std::sync::Arc;
mod term;
pub use term::HdtTerm;
enum HdtMatcher {
Constant((HdtTerm, Id)),
Other,
}
fn id_term(hdt: &Hdt, id: Id, kind: IdKind) -> HdtTerm {
auto_term(&hdt.dict.id_to_string(id, kind).unwrap()).unwrap()
}
fn unpack_matcher<T: TermMatcher>(hdt: &Hdt, tm: &T, kind: IdKind) -> Option<HdtMatcher> {
match tm.constant() {
Some(t) => match HdtTerm::try_from(t.borrow_term()) {
Some(t) => {
let id = hdt.dict.string_to_id(&term_string(&t), kind);
if id == 0 {
return None;
}
Some(HdtMatcher::Constant((t, id)))
}
None => None,
},
None => Some(HdtMatcher::Other),
}
}
fn auto_term(s: &str) -> io::Result<HdtTerm> {
match s.chars().next() {
None => Err(Error::new(ErrorKind::InvalidData, "empty input")),
Some('"') => match s.rfind('"') {
None => Err(Error::new(
ErrorKind::InvalidData,
format!("missing right quotation mark in literal string {s}"),
)),
Some(index) => {
let lex = Arc::from(&s[1..index]);
let rest = &s[index + 1..];
if rest.is_empty() {
return Ok(HdtTerm::LiteralDatatype(lex, term::XSD_STRING.clone()));
}
if let Some(tag_index) = rest.find('@') {
let tag = LanguageTag::new_unchecked(Arc::from(&rest[tag_index + 1..]));
return Ok(HdtTerm::LiteralLanguage(lex, tag));
}
let mut dt_split = rest.split("^^");
dt_split.next(); dt_split.next().map_or_else(
|| Err(Error::new(ErrorKind::InvalidData, format!("empty datatype in {s}"))),
|dt| {
let unquoted = &dt[1..dt.len() - 1];
let dt = IriRef::new_unchecked(Arc::from(unquoted));
Ok(HdtTerm::LiteralDatatype(lex, dt))
},
)
}
},
Some('_') => Ok(HdtTerm::BlankNode(BnodeId::new_unchecked(Arc::from(&s[2..])))),
_ => Ok(HdtTerm::Iri(IriRef::new_unchecked(Arc::from(s)))),
}
}
fn term_string(t: &HdtTerm) -> String {
match t {
HdtTerm::BlankNode(b) => "_:".to_owned() + b.as_str(),
HdtTerm::Iri(i) => i.as_str().to_owned(),
HdtTerm::LiteralLanguage(l, lang) => {
format!("\"{l}\"@{}", lang.as_str())
}
HdtTerm::LiteralDatatype(l, dt) => {
let xsd_string: &str = "http://www.w3.org/2001/XMLSchema#string";
let dts = dt.as_str();
if dts == xsd_string { format!("\"{l}\"") } else { format!("\"{l}\"^^<{dts}>") }
}
}
}
impl Graph for Hdt {
type Triple<'a> = [HdtTerm; 3];
type Error = Infallible;
fn triples(&self) -> impl Iterator<Item = Result<Self::Triple<'_>, Self::Error>> {
debug!("Iterating through ALL triples in the HDT Graph. This can be inefficient for large graphs.");
self.triples_all().map(move |[s, p, o]| {
Ok([auto_term(&s).unwrap(), HdtTerm::Iri(IriRef::new_unchecked(p)), auto_term(&o).unwrap()])
})
}
fn triples_matching<'s, 't, S, P, O>(
&'s self, sm: S, pm: P, om: O,
) -> impl Iterator<Item = Result<Self::Triple<'s>, Self::Error>> + 't
where
's: 't,
S: TermMatcher + 't,
P: TermMatcher + 't,
O: TermMatcher + 't,
{
use HdtMatcher::{Constant, Other};
let Some(xso) = unpack_matcher(self, &sm, IdKind::Subject) else {
return Box::new(iter::empty()) as Box<dyn Iterator<Item = _>>;
};
let Some(xpo) = unpack_matcher(self, &pm, IdKind::Predicate) else { return Box::new(iter::empty()) };
let Some(xoo) = unpack_matcher(self, &om, IdKind::Object) else { return Box::new(iter::empty()) };
match (xso, xpo, xoo) {
(Constant(s), Constant(p), Constant(o)) => Box::new(iter::once(Ok([s.0, p.0, o.0]))),
(Constant(s), Constant(p), Other) => Box::new(
SubjectIter::with_pattern(&self.triples, [s.1, p.1, 0])
.map(|tid| auto_term(&self.dict.id_to_string(tid[2], IdKind::Object).unwrap()).unwrap())
.filter(move |term| om.matches(term))
.map(move |term| Ok([s.0.clone(), p.0.clone(), term])),
),
(Constant(s), Other, Constant(o)) => Box::new(
SubjectIter::with_pattern(&self.triples, [s.1, 0, o.1])
.map(|t| id_term(self, t[1], IdKind::Predicate))
.filter(move |term| pm.matches(term))
.map(move |term| Ok([s.0.clone(), term, o.0.clone()])),
),
(Constant(s), Other, Other) => Box::new(
SubjectIter::with_pattern(&self.triples, [s.1, 0, 0])
.map(move |t| [id_term(self, t[1], IdKind::Predicate), id_term(self, t[2], IdKind::Object)])
.filter(move |[pt, ot]| pm.matches(pt) && om.matches(ot))
.map(move |[pt, ot]| Ok([s.0.clone(), pt, ot])),
),
(Other, Constant(p), Constant(o)) => Box::new(
PredicateObjectIter::new(&self.triples, p.1, o.1)
.map(|sid| id_term(self, sid, IdKind::Subject))
.filter(move |term| sm.matches(term))
.map(move |term| Ok([term, p.0.clone(), o.0.clone()])),
),
(Other, Constant(p), Other) => Box::new(
PredicateIter::new(&self.triples, p.1)
.map(move |t| [id_term(self, t[0], IdKind::Subject), id_term(self, t[2], IdKind::Object)])
.filter(move |[st, ot]| sm.matches(st) && om.matches(ot))
.map(move |[st, ot]| Ok([st, p.0.clone(), ot])),
),
(Other, Other, Constant(o)) => Box::new(ObjectIter::new(&self.triples, o.1).map(move |t| {
Ok([
auto_term(&Arc::from(self.dict.id_to_string(t[0], IdKind::Subject).unwrap())).unwrap(),
id_term(self, t[1], IdKind::Predicate),
o.0.clone(),
])
})),
(Other, Other, Other) => Box::new(
self.triples_all()
.map(move |[s, p, o]| {
[auto_term(&s).unwrap(), HdtTerm::Iri(IriRef::new_unchecked(p)), auto_term(&o).unwrap()]
})
.filter(move |[st, pt, ot]| sm.matches(st) && pm.matches(pt) && om.matches(ot))
.map(Result::Ok),
),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::init;
use fs_err::File;
use sophia::api::prelude::Triple;
use sophia::api::term::matcher::Any;
#[test]
fn test_graph() -> color_eyre::Result<()> {
init();
let file = File::open("tests/resources/snikmeta.hdt")?;
let graph = Hdt::read(std::io::BufReader::new(file))?;
let triples: Vec<Result<[HdtTerm; 3], Infallible>> = graph.triples().collect();
assert_eq!(triples.len(), 328);
let meta_top = "http://www.snik.eu/ontology/meta/Top";
assert!(
graph
.triples_matching(
Some(HdtTerm::Iri(IriRef::new_unchecked(Arc::from("http://www.snik.eu/ontology/meta")))),
Any,
Any
)
.next()
.is_some()
);
for uri in [meta_top, "http://www.snik.eu/ontology/meta", "doesnotexist"] {
let term = HdtTerm::Iri(IriRef::new_unchecked(Arc::from(uri)));
let filtered: Vec<_> = triples
.iter()
.map(|triple| triple.as_ref().unwrap())
.filter(|triple| triple.s().iri().is_some() && triple.s().iri().unwrap().to_string() == uri)
.collect();
let with_s: Vec<_> = graph.triples_matching(Some(term), Any, Any).map(Result::unwrap).collect();
let filtered_string = format!("{filtered:?}");
let with_s_string = format!("{with_s:?}");
assert_eq!(
filtered_string, with_s_string,
"different results between triples() and triples_with_s() for {uri}"
);
}
let s = HdtTerm::Iri(IriRef::new_unchecked(meta_top.into()));
let label = HdtTerm::Iri(IriRef::new_unchecked("http://www.w3.org/2000/01/rdf-schema#label".into()));
let o = HdtTerm::LiteralLanguage("top class".into(), LanguageTag::new_unchecked("en".into()));
assert!(graph.triples_matching(Any, Any, [o.borrow_term()]).next().is_some());
let tvec = vec![[s.clone(), label.clone(), o.clone()]];
assert_eq!(
tvec,
graph
.triples_matching([s.borrow_term()], [label.borrow_term()], Any)
.map(Result::unwrap)
.collect::<Vec<_>>()
);
assert_eq!(
tvec,
graph
.triples_matching([s.borrow_term()], Any, [o.borrow_term()])
.map(Result::unwrap)
.collect::<Vec<_>>()
);
assert_eq!(
tvec,
graph
.triples_matching(Any, [label.borrow_term()], [o.borrow_term()])
.map(Result::unwrap)
.collect::<Vec<_>>()
);
assert_eq!(1, graph.triples_matching(Any, Any, ["22.10"]).count());
let date = HdtTerm::LiteralDatatype(
"2022-10-20".into(),
IriRef::new_unchecked("http://www.w3.org/2001/XMLSchema#date".into()),
);
assert_eq!(1, graph.triples_matching(Any, Any, Some(&date)).count());
let meta = HdtTerm::Iri(IriRef::new_unchecked("http://www.snik.eu/ontology/meta".into()));
let modified = HdtTerm::Iri(IriRef::new_unchecked("http://purl.org/dc/terms/modified".into()));
assert_eq!(2, graph.triples_matching([&meta, &s], [&label, &modified], [&date, &o]).count());
assert_eq!(3, graph.triples_matching([&meta, &s], [&label, &modified], Any).count());
assert_eq!(2, graph.triples_matching([&meta, &s], Any, [&date, &o]).count());
assert_eq!(
graph.triples_matching([&meta, &s], Any, Any).count(),
graph.triples_matching([&meta], Any, Any).count() + graph.triples_matching([&s], Any, Any).count(),
);
assert_eq!(2, graph.triples_matching(Any, Any, [&date, &o]).count());
assert_eq!(2, graph.triples_matching(Any, [&label, &modified], [&date, &o]).count());
assert_eq!(
graph.triples_matching(Any, [&label, &modified], Any).count(),
graph.triples_matching(Any, [&label], Any).count()
+ graph.triples_matching(Any, [&modified], Any).count()
);
let blank = HdtTerm::BlankNode(BnodeId::new_unchecked("b1".into()));
assert_eq!(3, graph.triples_matching(Some(&blank), Any, Any).count());
assert_eq!(1, graph.triples_matching(Any, Any, Some(&blank)).count());
let rdftype =
HdtTerm::Iri(IriRef::new_unchecked("http://www.w3.org/1999/02/22-rdf-syntax-ns#type".into()));
let owlrestriction =
HdtTerm::Iri(IriRef::new_unchecked("http://www.w3.org/2002/07/owl#Restriction".into()));
assert_eq!(1, graph.triples_matching(Any, Some(rdftype), Some(owlrestriction)).count());
let s = HdtTerm::Iri(IriRef::new_unchecked("http://www.snik.eu/ontology/meta/хобби-N-0".into()));
let o = HdtTerm::LiteralLanguage("ХОББИ".into(), LanguageTag::new_unchecked("ru".into()));
let tvec = vec![[s.clone(), label.clone(), o.clone()]];
assert_eq!(
tvec,
graph
.triples_matching([s.borrow_term()], [label.borrow_term()], Any)
.map(Result::unwrap)
.collect::<Vec<_>>()
);
Ok(())
}
}