2026-06-08 19:33:49 -04:00
|
|
|
use crate::error;
|
2026-06-09 17:20:46 -04:00
|
|
|
use crate::rdf::vocab::{gl, owl};
|
2026-06-08 19:33:49 -04:00
|
|
|
use oxigraph::io::{RdfFormat, RdfParser};
|
|
|
|
|
use oxigraph::model::vocab::{rdf, rdfs, xsd};
|
|
|
|
|
use oxigraph::model::{
|
2026-06-09 19:25:47 -04:00
|
|
|
Dataset, GraphName, NamedNode, NamedNodeRef, NamedOrBlankNode, NamedOrBlankNodeRef, Quad, Term,
|
|
|
|
|
TermRef, Triple, TripleRef,
|
2026-06-08 19:33:49 -04:00
|
|
|
};
|
|
|
|
|
use oxigraph::sparql::{QueryResults, SparqlEvaluator};
|
2026-06-09 19:25:47 -04:00
|
|
|
use std::collections::HashMap;
|
|
|
|
|
use std::fmt::Display;
|
2026-06-08 19:33:49 -04:00
|
|
|
|
|
|
|
|
const PREFIXES: &[(&str, &str)] = &[
|
|
|
|
|
("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"),
|
|
|
|
|
("rdfs", "http://www.w3.org/2000/01/rdf-schema#"),
|
|
|
|
|
("owl", "http://www.w3.org/2002/07/owl#"),
|
|
|
|
|
("xsd", "http://www.w3.org/2001/XMLSchema#"),
|
|
|
|
|
("ldp", "http://www.w3.org/ns/ldp#"),
|
|
|
|
|
("dc", "http://purl.org/dc/elements/1.1/"),
|
|
|
|
|
("posix", "http://www.w3.org/ns/posix/stat#"),
|
|
|
|
|
(
|
|
|
|
|
"ebucore",
|
|
|
|
|
"http://www.ebu.ch/metadata/ontologies/ebucore/ebucore#",
|
|
|
|
|
),
|
|
|
|
|
("premis", "http://www.loc.gov/premis/rdf/v1#"),
|
|
|
|
|
("premis3", "http://www.loc.gov/premis/rdf/v3/"),
|
|
|
|
|
("dcterms", "http://purl.org/dc/terms/"),
|
|
|
|
|
("fedora", "http://fedora.info/definitions/v4/repository#"),
|
|
|
|
|
("rdaa", "http://rdaregistry.info/Elements/a/"),
|
|
|
|
|
("rdac", "http://rdaregistry.info/Elements/c/"),
|
|
|
|
|
("rdae", "http://rdaregistry.info/Elements/e/"),
|
|
|
|
|
("rdai", "http://rdaregistry.info/Elements/i/"),
|
|
|
|
|
("rdam", "http://rdaregistry.info/Elements/m/"),
|
|
|
|
|
("rdan", "http://rdaregistry.info/Elements/n/"),
|
|
|
|
|
("rdap", "http://rdaregistry.info/Elements/p/"),
|
|
|
|
|
("rdat", "http://rdaregistry.info/Elements/t/"),
|
|
|
|
|
("rdaw", "http://rdaregistry.info/Elements/w/"),
|
|
|
|
|
("rdax", "http://rdaregistry.info/Elements/x/"),
|
|
|
|
|
("schema", "https://schema.org/"),
|
|
|
|
|
("quill", "http://fedora.quill.lan/rest/"),
|
|
|
|
|
("gl", "https://graphofliberty.org/2026/04/ont/"),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const RDF_ONT: &[u8] = include_bytes!("ontologies/22-rdf-syntax-ns.ttl");
|
|
|
|
|
const RDFS_ONT: &[u8] = include_bytes!("ontologies/rdf-schema.ttl");
|
2026-06-09 15:40:28 -04:00
|
|
|
const OWL_ONT: &[u8] = include_bytes!("ontologies/owl.ttl");
|
2026-06-08 19:33:49 -04:00
|
|
|
const LDP_ONT: &[u8] = include_bytes!("ontologies/ldp.ttl");
|
|
|
|
|
const FEDORA_ONT: &[u8] = include_bytes!("ontologies/fedora.xml");
|
|
|
|
|
const GL_ONT: &[u8] = include_bytes!("ontologies/ontology.ttl");
|
|
|
|
|
|
|
|
|
|
pub struct OntologyBuilder<'a> {
|
|
|
|
|
ontologies: Vec<(RdfFormat, &'a [u8])>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<'a> OntologyBuilder<'a> {
|
|
|
|
|
pub fn with_ontology_bytes(mut self, format: RdfFormat, bytes: &'a [u8]) -> Self {
|
|
|
|
|
self.ontologies.push((format, bytes));
|
|
|
|
|
self
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn with_default_ontologies(self) -> Self {
|
|
|
|
|
self.with_ontology_bytes(RdfFormat::Turtle, RDF_ONT)
|
|
|
|
|
.with_ontology_bytes(RdfFormat::Turtle, RDFS_ONT)
|
2026-06-09 15:40:28 -04:00
|
|
|
.with_ontology_bytes(RdfFormat::Turtle, OWL_ONT)
|
2026-06-08 19:33:49 -04:00
|
|
|
.with_ontology_bytes(RdfFormat::Turtle, LDP_ONT)
|
|
|
|
|
.with_ontology_bytes(RdfFormat::RdfXml, FEDORA_ONT)
|
|
|
|
|
.with_ontology_bytes(RdfFormat::Turtle, GL_ONT)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn materialize_same_as(dataset: &mut Dataset) {
|
|
|
|
|
let additional_quads = dataset
|
2026-06-09 15:40:28 -04:00
|
|
|
.quads_for_pattern(None, Some(owl::SAME_AS), None, None)
|
2026-06-08 19:33:49 -04:00
|
|
|
.fold(Dataset::new(), |mut new_dataset, alias| {
|
|
|
|
|
if let NamedOrBlankNodeRef::NamedNode(x) = alias.subject
|
|
|
|
|
&& let TermRef::NamedNode(y) = alias.object
|
|
|
|
|
{
|
|
|
|
|
for mut quad in dataset.quads_for_pattern(
|
|
|
|
|
Some(NamedOrBlankNodeRef::NamedNode(x)),
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
) {
|
|
|
|
|
quad.subject = NamedOrBlankNodeRef::NamedNode(y);
|
|
|
|
|
new_dataset.insert(quad);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for mut quad in dataset.quads_for_pattern(
|
|
|
|
|
Some(NamedOrBlankNodeRef::NamedNode(y)),
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
) {
|
|
|
|
|
quad.subject = NamedOrBlankNodeRef::NamedNode(x);
|
|
|
|
|
new_dataset.insert(quad);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
new_dataset
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let old_size = dataset.len();
|
|
|
|
|
dataset.extend(&additional_quads);
|
|
|
|
|
let new_size = dataset.len();
|
|
|
|
|
|
|
|
|
|
if new_size > old_size {
|
|
|
|
|
Self::materialize_same_as(dataset)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-09 19:25:47 -04:00
|
|
|
fn materialize_super_classes(dataset: &mut Dataset) {
|
2026-06-09 15:40:28 -04:00
|
|
|
let query = SparqlEvaluator::new()
|
2026-06-09 17:20:46 -04:00
|
|
|
.parse_query(
|
2026-06-09 19:25:47 -04:00
|
|
|
r#"PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|
2026-06-09 15:40:28 -04:00
|
|
|
|
2026-06-09 19:25:47 -04:00
|
|
|
CONSTRUCT {
|
|
|
|
|
?item a ?parent
|
|
|
|
|
} WHERE {
|
|
|
|
|
?item a ?class .
|
|
|
|
|
?class rdfs:subClassOf ?parent .
|
|
|
|
|
}"#,
|
|
|
|
|
)
|
|
|
|
|
.expect("Unable to parse superclass query");
|
|
|
|
|
|
|
|
|
|
let mut additional_quads = Dataset::new();
|
|
|
|
|
if let QueryResults::Graph(graph) = query.on_queryable_dataset(&*dataset).execute().unwrap()
|
|
|
|
|
{
|
|
|
|
|
additional_quads.extend(graph.filter_map(Result::ok).map(|triple| {
|
|
|
|
|
Quad::new(
|
|
|
|
|
triple.subject,
|
|
|
|
|
triple.predicate,
|
|
|
|
|
triple.object,
|
|
|
|
|
GraphName::DefaultGraph,
|
|
|
|
|
)
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let old_size = dataset.len();
|
|
|
|
|
dataset.extend(&additional_quads);
|
|
|
|
|
let new_size = dataset.len();
|
|
|
|
|
|
|
|
|
|
if new_size > old_size {
|
|
|
|
|
Self::materialize_super_classes(dataset)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn iri_information(dataset: &Dataset) -> HashMap<NamedNode, IriInformation> {
|
|
|
|
|
let query = SparqlEvaluator::new()
|
|
|
|
|
.parse_query(
|
|
|
|
|
r#"PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
|
|
|
|
|
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|
|
|
|
|
PREFIX gl: <https://graphofliberty.org/2026/04/ont/>
|
|
|
|
|
|
|
|
|
|
SELECT DISTINCT ?class ?subject ?label ?comment ?read_only {
|
|
|
|
|
VALUES ?class { rdf:Property rdfs:Class }
|
2026-06-09 15:40:28 -04:00
|
|
|
?subject a ?class .
|
2026-06-09 19:25:47 -04:00
|
|
|
OPTIONAL {
|
2026-06-09 17:20:46 -04:00
|
|
|
?subject rdfs:label ?label
|
|
|
|
|
FILTER (LANG(?label) = 'en' || LANG(?label) = '')
|
2026-06-09 19:25:47 -04:00
|
|
|
}
|
|
|
|
|
OPTIONAL {
|
2026-06-09 17:20:46 -04:00
|
|
|
?subject rdfs:comment ?comment
|
|
|
|
|
FILTER (LANG(?comment) = 'en' || LANG(?comment) = '')
|
2026-06-09 19:25:47 -04:00
|
|
|
}
|
|
|
|
|
OPTIONAL { ?subject gl:readOnly ?read_only }
|
|
|
|
|
}"#,
|
2026-06-09 17:20:46 -04:00
|
|
|
)
|
|
|
|
|
.expect("Unable to parse property query");
|
2026-06-09 15:40:28 -04:00
|
|
|
|
|
|
|
|
let mut results = HashMap::new();
|
|
|
|
|
if let QueryResults::Solutions(solutions) =
|
|
|
|
|
query.on_queryable_dataset(dataset).execute().unwrap()
|
|
|
|
|
{
|
|
|
|
|
for solution in solutions.filter_map(Result::ok) {
|
2026-06-09 19:25:47 -04:00
|
|
|
let resource_type = solution.get("class").and_then(term_to_named_node).and_then(
|
|
|
|
|
|class| match class.as_ref() {
|
|
|
|
|
rdf::PROPERTY => Some(ResourceType::Property),
|
|
|
|
|
rdfs::CLASS => Some(ResourceType::Class),
|
|
|
|
|
_ => None,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
2026-06-09 15:40:28 -04:00
|
|
|
let subject = solution.get("subject").and_then(term_to_named_node);
|
|
|
|
|
let label = solution.get("label").and_then(term_to_string);
|
|
|
|
|
let comment = solution.get("comment").and_then(term_to_string);
|
2026-06-09 17:20:46 -04:00
|
|
|
let read_only = solution
|
|
|
|
|
.get("read_only")
|
|
|
|
|
.and_then(term_to_boolean)
|
|
|
|
|
.unwrap_or(false);
|
2026-06-09 15:40:28 -04:00
|
|
|
|
2026-06-09 19:25:47 -04:00
|
|
|
if let Some(subject) = subject
|
|
|
|
|
&& let Some(type_) = resource_type
|
|
|
|
|
{
|
2026-06-09 15:40:28 -04:00
|
|
|
let info = IriInformation {
|
2026-06-09 19:25:47 -04:00
|
|
|
type_,
|
2026-06-09 15:40:28 -04:00
|
|
|
label: label.map(String::from),
|
|
|
|
|
comment: comment.map(String::from),
|
|
|
|
|
read_only,
|
|
|
|
|
};
|
|
|
|
|
results.insert(subject.to_owned(), info);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
results
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-08 19:33:49 -04:00
|
|
|
pub fn build(&mut self) -> error::Result<Ontology> {
|
|
|
|
|
let prefixes = PREFIXES
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|(k, v)| (k.to_string(), v.to_string()))
|
|
|
|
|
.collect::<HashMap<String, String>>();
|
|
|
|
|
|
|
|
|
|
let mut dataset = Dataset::new();
|
|
|
|
|
for (format, bytes) in &self.ontologies {
|
|
|
|
|
let quads = RdfParser::from_format(*format)
|
|
|
|
|
.for_slice(bytes)
|
|
|
|
|
.filter_map(Result::ok);
|
|
|
|
|
dataset.extend(quads);
|
|
|
|
|
}
|
|
|
|
|
Self::materialize_same_as(&mut dataset);
|
2026-06-09 19:25:47 -04:00
|
|
|
Self::materialize_super_classes(&mut dataset);
|
2026-06-08 19:33:49 -04:00
|
|
|
|
2026-06-09 19:25:47 -04:00
|
|
|
let iri_info = Self::iri_information(&mut dataset);
|
2026-06-09 15:40:28 -04:00
|
|
|
|
2026-06-09 17:20:46 -04:00
|
|
|
// Full-text search index field names
|
|
|
|
|
let mut field_map = HashMap::new();
|
|
|
|
|
for quad in dataset.quads_for_pattern(None, Some(gl::INDEXED_BY_FIELD), None, None) {
|
|
|
|
|
if let NamedOrBlankNodeRef::NamedNode(subject) = quad.subject
|
|
|
|
|
&& let TermRef::Literal(literal) = quad.object
|
|
|
|
|
{
|
|
|
|
|
field_map.insert(subject.into_owned(), literal.value().to_string());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Catalog IDs (used to quickly filter full-text search results)
|
|
|
|
|
let mut catalog_ids = HashMap::new();
|
|
|
|
|
for quad in dataset.quads_for_pattern(None, Some(gl::CATALOG_ID), None, None) {
|
|
|
|
|
if let NamedOrBlankNodeRef::NamedNode(subject) = quad.subject
|
|
|
|
|
&& let TermRef::Literal(literal) = quad.object
|
|
|
|
|
{
|
|
|
|
|
if literal.datatype() == xsd::NON_NEGATIVE_INTEGER {
|
|
|
|
|
let value: u64 = literal.value().parse().expect("Failed to parse catalog ID from ontology. It ought to be a non-negative integer.");
|
|
|
|
|
catalog_ids.insert(subject.into_owned(), value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(Ontology {
|
2026-06-08 19:33:49 -04:00
|
|
|
dataset,
|
|
|
|
|
prefixes,
|
2026-06-09 19:25:47 -04:00
|
|
|
iri_info,
|
2026-06-09 17:20:46 -04:00
|
|
|
field_map,
|
|
|
|
|
catalog_ids,
|
|
|
|
|
})
|
2026-06-08 19:33:49 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-09 16:29:04 -04:00
|
|
|
#[derive(Clone, Debug)]
|
2026-06-09 19:25:47 -04:00
|
|
|
pub enum ResourceType {
|
|
|
|
|
Property,
|
|
|
|
|
Class,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Display for ResourceType {
|
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
|
let str = match self {
|
|
|
|
|
ResourceType::Property => "Property".to_string(),
|
|
|
|
|
ResourceType::Class => "Class".to_string(),
|
|
|
|
|
};
|
|
|
|
|
write!(f, "{}", str)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
pub struct IriInformation {
|
|
|
|
|
pub type_: ResourceType,
|
|
|
|
|
pub label: Option<String>,
|
|
|
|
|
pub comment: Option<String>,
|
|
|
|
|
pub read_only: bool,
|
2026-06-09 15:40:28 -04:00
|
|
|
}
|
|
|
|
|
|
2026-06-08 19:33:49 -04:00
|
|
|
pub struct Ontology {
|
|
|
|
|
dataset: Dataset,
|
|
|
|
|
prefixes: HashMap<String, String>,
|
2026-06-09 19:25:47 -04:00
|
|
|
iri_info: HashMap<NamedNode, IriInformation>,
|
2026-06-08 19:33:49 -04:00
|
|
|
field_map: HashMap<NamedNode, String>,
|
|
|
|
|
catalog_ids: HashMap<NamedNode, u64>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-09 15:40:28 -04:00
|
|
|
fn term_to_named_node(term: &Term) -> Option<&NamedNode> {
|
|
|
|
|
if let Term::NamedNode(node) = term {
|
|
|
|
|
Some(node)
|
2026-06-09 17:20:46 -04:00
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
2026-06-09 15:40:28 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn term_to_string(term: &Term) -> Option<&str> {
|
|
|
|
|
if let Term::Literal(literal) = term {
|
|
|
|
|
match literal.datatype() {
|
2026-06-09 17:20:46 -04:00
|
|
|
xsd::STRING | xsd::NORMALIZED_STRING | rdf::LANG_STRING | rdf::DIR_LANG_STRING => {
|
|
|
|
|
Some(literal.value())
|
|
|
|
|
}
|
2026-06-09 15:40:28 -04:00
|
|
|
_ => None,
|
|
|
|
|
}
|
2026-06-09 17:20:46 -04:00
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
2026-06-09 15:40:28 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn term_to_boolean(term: &Term) -> Option<bool> {
|
|
|
|
|
if let Term::Literal(literal) = term {
|
|
|
|
|
if literal.datatype() == xsd::BOOLEAN {
|
|
|
|
|
literal.value().parse().ok()
|
2026-06-09 17:20:46 -04:00
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
2026-06-08 19:33:49 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Ontology {
|
|
|
|
|
pub fn builder<'a>() -> OntologyBuilder<'a> {
|
|
|
|
|
OntologyBuilder {
|
|
|
|
|
ontologies: Vec::new(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-09 19:25:47 -04:00
|
|
|
pub fn info(&self, node: NamedNodeRef<'_>) -> Option<&IriInformation> {
|
2026-06-09 17:20:46 -04:00
|
|
|
let node = node.into_owned();
|
2026-06-09 19:25:47 -04:00
|
|
|
self.iri_info.get(&node)
|
2026-06-08 19:33:49 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn abbreviate(&self, node: NamedNodeRef<'_>) -> String {
|
|
|
|
|
for (prefix_name, prefix_iri) in &self.prefixes {
|
|
|
|
|
if let Some(local_name) = node.as_str().strip_prefix(prefix_iri) {
|
|
|
|
|
return if local_name.is_empty() {
|
|
|
|
|
format!("{prefix_name}:")
|
|
|
|
|
} else {
|
|
|
|
|
format!("{prefix_name}:{local_name}")
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
node.as_str().to_string()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn expand(&self, prefixed_iri: &str) -> Option<NamedNode> {
|
|
|
|
|
let (prefix, name) = prefixed_iri.split_once(':')?;
|
|
|
|
|
self.prefixes
|
|
|
|
|
.get(prefix)
|
|
|
|
|
.map(|base| NamedNode::new_unchecked(format!("{base}{name}")))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn field_name(&self, property: &NamedNode) -> Option<&String> {
|
|
|
|
|
self.field_map.get(property)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn catalog_id(&self, class: &NamedNode) -> Option<u64> {
|
|
|
|
|
self.catalog_ids.get(class).copied()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn for_each_annotated_iri<F>(&self, f: F)
|
|
|
|
|
where
|
|
|
|
|
F: Fn(&str, Option<&str>, Option<&str>),
|
|
|
|
|
{
|
|
|
|
|
let query = SparqlEvaluator::new()
|
|
|
|
|
.parse_query(
|
|
|
|
|
r"PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|
|
|
|
|
|
|
|
|
|
SELECT ?subject ?label ?comment WHERE {
|
|
|
|
|
OPTIONAL {
|
|
|
|
|
?subject rdfs:label ?label .
|
|
|
|
|
FILTER (LANG(?label) = 'en' || LANG(?label) = '')
|
|
|
|
|
}
|
|
|
|
|
OPTIONAL {
|
|
|
|
|
?subject rdfs:comment ?comment .
|
|
|
|
|
FILTER (LANG(?comment) = 'en' || LANG(?comment) = '')
|
|
|
|
|
}
|
|
|
|
|
}",
|
|
|
|
|
)
|
|
|
|
|
.expect("Unable to parse annotation query");
|
|
|
|
|
|
|
|
|
|
if let QueryResults::Solutions(solutions) =
|
|
|
|
|
query.on_queryable_dataset(&self.dataset).execute().unwrap()
|
|
|
|
|
{
|
|
|
|
|
for solution in solutions.filter_map(Result::ok) {
|
|
|
|
|
let label = solution.get("label").and_then(term_to_string);
|
|
|
|
|
let comment = solution.get("comment").and_then(term_to_string);
|
|
|
|
|
if let Some(Term::NamedNode(subject)) = solution.get("subject") {
|
|
|
|
|
f(subject.as_str(), label.as_deref(), comment.as_deref());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn datatypes(&self) -> impl Iterator<Item = NamedNodeRef<'_>> {
|
|
|
|
|
self.dataset
|
|
|
|
|
.quads_for_pattern(
|
|
|
|
|
None,
|
|
|
|
|
Some(rdf::TYPE),
|
|
|
|
|
Some(TermRef::NamedNode(rdfs::DATATYPE)),
|
|
|
|
|
None,
|
|
|
|
|
)
|
|
|
|
|
.filter_map(|quad| match quad.subject {
|
|
|
|
|
NamedOrBlankNodeRef::NamedNode(subject) => Some(subject),
|
|
|
|
|
_ => None,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn is_read_only<'a>(&self, triple: impl Into<TripleRef<'a>>) -> bool {
|
|
|
|
|
let triple = triple.into();
|
2026-06-09 19:25:47 -04:00
|
|
|
self.iri_info
|
2026-06-09 16:29:04 -04:00
|
|
|
.get(&triple.predicate.into_owned())
|
|
|
|
|
.map(|info| info.read_only)
|
2026-06-09 19:25:47 -04:00
|
|
|
.unwrap_or(false)
|
2026-06-08 19:33:49 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn exclude_read_only_predicate(&self) -> impl Fn(TripleRef<'_>) -> bool + 'static {
|
2026-06-09 19:25:47 -04:00
|
|
|
let iri_info = self.iri_info.clone();
|
|
|
|
|
move |triple| match (triple.predicate, triple.object) {
|
|
|
|
|
(rdf::TYPE, TermRef::NamedNode(node)) => iri_info
|
|
|
|
|
.get(&node.into_owned())
|
2026-06-09 16:29:04 -04:00
|
|
|
.map(|info| info.read_only)
|
2026-06-09 19:25:47 -04:00
|
|
|
.unwrap_or(false),
|
|
|
|
|
(predicate, _) => iri_info
|
|
|
|
|
.get(&predicate.into_owned())
|
|
|
|
|
.map(|info| info.read_only)
|
|
|
|
|
.unwrap_or(false),
|
2026-06-08 19:33:49 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn subclass_of(&self, class: NamedNodeRef<'_>) -> impl Iterator<Item = NamedNode> {
|
|
|
|
|
let query = SparqlEvaluator::new()
|
|
|
|
|
.parse_query(
|
|
|
|
|
format!(
|
|
|
|
|
"PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|
|
|
|
|
SELECT ?class {{
|
|
|
|
|
?class (rdfs:subClassOf|^rdfs:subClassOf)* {class} .
|
|
|
|
|
}}"
|
|
|
|
|
)
|
|
|
|
|
.as_str(),
|
|
|
|
|
)
|
|
|
|
|
.expect("Unable to parse subclass_of query");
|
|
|
|
|
|
|
|
|
|
if let QueryResults::Solutions(solutions) =
|
|
|
|
|
query.on_queryable_dataset(&self.dataset).execute().unwrap()
|
|
|
|
|
{
|
|
|
|
|
solutions.filter_map(Result::ok).filter_map(|solution| {
|
|
|
|
|
if let Some(Term::NamedNode(class)) = solution.get("class") {
|
|
|
|
|
Some(class.clone())
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
unreachable!()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn template_triples<'a>(
|
|
|
|
|
&'a self,
|
|
|
|
|
class: NamedNodeRef<'a>,
|
|
|
|
|
subject: NamedNodeRef<'_>,
|
|
|
|
|
) -> Option<impl Iterator<Item = Triple>> {
|
|
|
|
|
if let Some(quad) = self
|
|
|
|
|
.dataset
|
|
|
|
|
.quads_for_pattern(
|
|
|
|
|
Some(NamedOrBlankNodeRef::NamedNode(class)),
|
2026-06-09 15:40:28 -04:00
|
|
|
Some(gl::TEMPLATE),
|
2026-06-08 19:33:49 -04:00
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
)
|
|
|
|
|
.next()
|
|
|
|
|
{
|
|
|
|
|
if let TermRef::BlankNode(blank_node) = quad.object {
|
|
|
|
|
let iter = self
|
|
|
|
|
.dataset
|
|
|
|
|
.quads_for_subject(blank_node)
|
|
|
|
|
.map(|quad| quad.into_owned())
|
|
|
|
|
.map(Triple::from)
|
|
|
|
|
.map(move |mut triple| {
|
|
|
|
|
triple.subject = NamedOrBlankNode::NamedNode(subject.into_owned());
|
|
|
|
|
triple
|
|
|
|
|
});
|
|
|
|
|
Some(iter)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|