This commit is contained in:
Alex Wied
2026-06-23 21:55:52 -04:00
parent f5ca8dd9ae
commit 42dea77976
14 changed files with 543 additions and 897 deletions
+113 -301
View File
@@ -2,15 +2,13 @@ use crate::error;
use crate::rdf::vocab::{gl, owl, rda};
use oxigraph::io::{RdfFormat, RdfParser};
use oxigraph::model::vocab::{rdf, rdfs, xsd};
use oxigraph::model::{
Dataset, GraphName, NamedNode, NamedNodeRef, NamedOrBlankNode, NamedOrBlankNodeRef, Quad, Term,
TermRef, Triple, TripleRef,
};
use oxigraph::model::{Dataset, GraphName, LiteralRef, NamedNode, NamedNodeRef, NamedOrBlankNode, NamedOrBlankNodeRef, Quad, Term, TermRef, Triple, TripleRef};
use oxigraph::sparql::{QueryResults, SparqlEvaluator};
use std::collections::{HashMap, HashSet};
use std::fmt::Display;
use iced::widget::sensor::Key;
use tracing::{debug, info};
use std::path::{Path, PathBuf};
use oxigraph::store::Store;
use crate::rdf::materialize;
const PREFIXES: &[(&str, &str)] = &[
("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"),
@@ -29,6 +27,7 @@ const PREFIXES: &[(&str, &str)] = &[
("dcterms", "http://purl.org/dc/terms/"),
("fedora", "http://fedora.info/definitions/v4/repository#"),
("rdaa", "http://rdaregistry.info/Elements/a/"),
("rdaad", "http://rdaregistry.info/Elements/a/datatype/"),
("rdac", "http://rdaregistry.info/Elements/c/"),
("rdae", "http://rdaregistry.info/Elements/e/"),
("rdai", "http://rdaregistry.info/Elements/i/"),
@@ -43,159 +42,93 @@ const PREFIXES: &[(&str, &str)] = &[
("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");
const OWL_ONT: &[u8] = include_bytes!("ontologies/owl.ttl");
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])>,
pub struct OntologyBuilder {
path: Option<PathBuf>,
}
impl<'a> OntologyBuilder<'a> {
pub fn with_ontology_bytes(mut self, format: RdfFormat, bytes: &'a [u8]) -> Self {
self.ontologies.push((format, bytes));
impl OntologyBuilder {
pub fn with_path(mut self, path: impl AsRef<Path>) -> Self {
let path = path.as_ref().to_owned();
self.path = Some(path);
self
}
pub fn with_default_ontologies(self) -> Self {
self.with_ontology_bytes(RdfFormat::Turtle, RDF_ONT)
.with_ontology_bytes(RdfFormat::Turtle, RDFS_ONT)
.with_ontology_bytes(RdfFormat::Turtle, OWL_ONT)
.with_ontology_bytes(RdfFormat::Turtle, LDP_ONT)
.with_ontology_bytes(RdfFormat::RdfXml, FEDORA_ONT)
.with_ontology_bytes(RdfFormat::Turtle, GL_ONT)
}
pub fn build(self) -> error::Result<Ontology> {
let store = if let Some(path) = self.path {
Store::open(path)
} else {
Store::new()
}?;
fn materialize_same_as(dataset: &mut Dataset) {
let additional_quads = dataset
.quads_for_pattern(None, Some(owl::SAME_AS), None, None)
.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);
}
let prefixes = PREFIXES
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect::<HashMap<String, String>>();
for mut quad in dataset.quads_for_pattern(
Some(NamedOrBlankNodeRef::NamedNode(y)),
None,
None,
None,
) {
quad.subject = NamedOrBlankNodeRef::NamedNode(x);
new_dataset.insert(quad);
let store = materialize::same_as(store)?;
//materialize::super_classes(&mut dataset);
// Full-text search index field names
//let fields = Self::fields(&dataset);
/*let mut entities: HashMap<NamedNode, Entity> = HashMap::new();
let entity_iris = Self::subclass_of(&dataset, gl::ENTITY)
.chain(Self::subclass_of(&dataset, rda::ENTITY))
.chain([
rdf::PROPERTY.into_owned(),
rdfs::CLASS.into_owned(),
]);
for iri in entity_iris {
let subject = iri.as_ref().into();
let label = dataset.quads_for_pattern(Some(subject), Some(rdfs::LABEL), None, None)
.filter_map(|quad| term_to_string(&quad.object.into_owned()).map(String::from))
.next();
let catalog_id = dataset.quads_for_pattern(Some(subject), Some(gl::CATALOG_ID), None, None)
.filter_map(|quad| term_to_u64(&quad.object.into_owned()))
.next();
if let Some(catalog_id) = catalog_id {
let mut properties = HashSet::new();
for quad in dataset.quads_for_pattern(Some(subject), Some(gl::ASSOCIATED_PROPERTY), None, None) {
if let NamedOrBlankNodeRef::NamedNode(subject) = quad.subject
&& let TermRef::NamedNode(property) = quad.object {
properties.insert(property.into_owned());
}
}
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)
}
}
fn materialize_super_classes(dataset: &mut Dataset) {
let query = SparqlEvaluator::new()
.parse_query(
r#"PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
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 }
?subject a ?class .
OPTIONAL {
?subject rdfs:label ?label
FILTER (langMATCHES(LANG(?label), "en") || !hasLANG(?label))
}
OPTIONAL {
?subject rdfs:comment ?comment
FILTER (langMATCHES(LANG(?label), "en") || !hasLANG(?comment))
}
OPTIONAL { ?subject gl:readOnly ?read_only }
}"#,
)
.expect("Unable to parse property query");
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) {
let type_ = solution.get("class").and_then(term_to_named_node);
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);
let read_only = solution
.get("read_only")
.and_then(term_to_boolean)
.unwrap_or(false);
if let Some(subject) = subject && let Some(type_) = type_ {
let info = IriInformation {
type_: type_.clone(),
label: label.map(String::from),
comment: comment.map(String::from),
read_only,
};
results.insert(subject.to_owned(), info);
}
entities.insert(iri, Entity {
label: label.unwrap_or(catalog_id.to_string()),
catalog_id,
properties,
});
}
}
results
let mut indexed_by = 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::NamedNode(field) = quad.object
{
indexed_by.insert(subject.into_owned(), field.into_owned());
}
}*/
Ok(Ontology {
store,
prefixes,
fields: HashMap::new(),
entities: HashMap::from_iter([(rdf::PROPERTY.into_owned(), Entity {
label: "property".to_string(),
catalog_id: 0,
properties: HashSet::new(),
})]),
indexed_by: HashMap::new(),
})
}
fn fields(dataset: &Dataset) -> HashMap<NamedNode, IndexField> {
/*fn fields(dataset: &Dataset) -> HashMap<NamedNode, IndexField> {
let query = SparqlEvaluator::new()
.parse_query(
r#"PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
@@ -257,82 +190,7 @@ SELECT ?class {{
} else {
unreachable!()
}
}
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);
Self::materialize_super_classes(&mut dataset);
let iri_info = Self::iri_information(&mut dataset);
// Full-text search index field names
let fields = Self::fields(&dataset);
let mut entities: HashMap<NamedNode, Entity> = HashMap::new();
let entity_iris = Self::subclass_of(&dataset, gl::ENTITY)
.chain(Self::subclass_of(&dataset, rda::ENTITY))
.chain([
rdf::PROPERTY.into_owned(),
rdfs::CLASS.into_owned(),
]);
for iri in entity_iris {
let subject = iri.as_ref().into();
let label = dataset.quads_for_pattern(Some(subject), Some(rdfs::LABEL), None, None)
.filter_map(|quad| term_to_string(&quad.object.into_owned()).map(String::from))
.next();
let catalog_id = dataset.quads_for_pattern(Some(subject), Some(gl::CATALOG_ID), None, None)
.filter_map(|quad| term_to_u64(&quad.object.into_owned()))
.next();
if let Some(catalog_id) = catalog_id {
let mut properties = HashSet::new();
for quad in dataset.quads_for_pattern(Some(subject), Some(gl::ASSOCIATED_PROPERTY), None, None) {
if let NamedOrBlankNodeRef::NamedNode(subject) = quad.subject
&& let TermRef::NamedNode(property) = quad.object {
properties.insert(property.into_owned());
}
}
entities.insert(iri, Entity {
label: label.unwrap_or(catalog_id.to_string()),
catalog_id,
properties,
});
}
}
let mut indexed_by = 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::NamedNode(field) = quad.object
{
indexed_by.insert(subject.into_owned(), field.into_owned());
}
}
Ok(Ontology {
dataset,
prefixes,
iri_info,
fields,
entities,
indexed_by,
})
}
}*/
}
#[derive(Clone, Debug)]
@@ -375,11 +233,11 @@ pub struct IndexField {
}
pub struct Ontology {
dataset: Dataset,
store: Store,
prefixes: HashMap<String, String>,
// Resource (Property or Class) -> Rust Type
iri_info: HashMap<NamedNode, IriInformation>,
//iri_info: HashMap<NamedNode, IriInformation>,
// NamedIndividual of class IndexField -> Rust Type
fields: HashMap<NamedNode, IndexField>,
@@ -391,58 +249,17 @@ pub struct Ontology {
indexed_by: HashMap<NamedNode, NamedNode>,
}
fn term_to_named_node(term: &Term) -> Option<&NamedNode> {
if let Term::NamedNode(node) = term {
Some(node)
} else {
None
}
}
fn term_to_string(term: &Term) -> Option<&str> {
if let Term::Literal(literal) = term {
match literal.datatype() {
xsd::STRING | xsd::NORMALIZED_STRING | rdf::LANG_STRING | rdf::DIR_LANG_STRING => {
Some(literal.value())
}
_ => None,
}
} else {
None
}
}
fn term_to_boolean(term: &Term) -> Option<bool> {
if let Term::Literal(literal) = term {
if literal.datatype() == xsd::BOOLEAN {
literal.value().parse().ok()
} else {
None
}
} else {
None
}
}
fn term_to_u64(term: &Term) -> Option<u64> {
if let Term::Literal(literal) = term &&
literal.datatype() == xsd::NON_NEGATIVE_INTEGER {
let value: u64 = literal.value().parse().expect("Failed to parse u64 from ontology. It ought to be a non-negative integer.");
Some(value)
} else { None }
}
impl Ontology {
pub fn builder<'a>() -> OntologyBuilder<'a> {
pub fn builder() -> OntologyBuilder {
OntologyBuilder {
ontologies: Vec::new(),
path: None,
}
}
pub fn info(&self, node: NamedNodeRef<'_>) -> Option<&IriInformation> {
/*pub fn info(&self, node: NamedNodeRef<'_>) -> Option<&IriInformation> {
let node = node.into_owned();
self.iri_info.get(&node)
}
}*/
pub fn abbreviate(&self, node: NamedNodeRef<'_>) -> String {
for (prefix_name, prefix_iri) in &self.prefixes {
@@ -501,67 +318,62 @@ impl Ontology {
})
}
pub fn iri_info(&self) -> &HashMap<NamedNode, IriInformation> {
&self.iri_info
}
pub fn datatypes(&self) -> impl Iterator<Item = NamedNodeRef<'_>> {
self.dataset
pub fn datatypes(&self) -> impl Iterator<Item = NamedNode> {
self.store
.quads_for_pattern(
None,
Some(rdf::TYPE),
Some(TermRef::NamedNode(rdfs::DATATYPE)),
None,
)
.filter_map(Result::ok)
.filter_map(|quad| match quad.subject {
NamedOrBlankNodeRef::NamedNode(subject) => Some(subject),
NamedOrBlankNode::NamedNode(subject) => Some(subject),
_ => None,
})
}
fn is_read_only_impl<'a>(iri_info: &HashMap<NamedNode, IriInformation>, triple: impl Into<TripleRef<'a>>) -> bool {
let triple = triple.into();
match (triple.predicate, triple.object) {
(rdf::TYPE, TermRef::NamedNode(node)) => iri_info
.get(&node.into_owned())
.map(|info| info.read_only)
.unwrap_or(false),
(predicate, _) => iri_info
.get(&predicate.into_owned())
.map(|info| info.read_only)
.unwrap_or(false),
}
}
pub fn exclude_read_only(&self) -> impl Fn(TripleRef<'_>) -> bool + 'static {
let iri_info = self.iri_info.clone();
move |triple| { Self::is_read_only_impl(&iri_info, triple) }
}
pub fn is_read_only<'a>(&self, triple: impl Into<TripleRef<'a>>) -> bool {
Self::is_read_only_impl(&self.iri_info, triple)
let triple = triple.into();
let true_term = TermRef::Literal(LiteralRef::new_typed_literal("true", xsd::BOOLEAN));
let subject = match (triple.predicate, triple.object) {
(rdf::TYPE, TermRef::NamedNode(class)) => class,
(predicate, _) => predicate,
};
self.store
.quads_for_pattern(Some(NamedOrBlankNodeRef::NamedNode(subject)), Some(gl::READ_ONLY), Some(true_term), None)
.filter_map(Result::ok)
.count() >= 1
}
/*pub fn for_each_annotated_(&self) -> impl Iterator<Item = TripleRef<'_>> {
self.store
.quads_for_pattern()
}*/
pub fn template_triples<'a>(
&'a self,
class: NamedNodeRef<'a>,
subject: NamedNodeRef<'_>,
) -> Option<impl Iterator<Item = Triple>> {
if let Some(quad) = self
.dataset
.store
.quads_for_pattern(
Some(NamedOrBlankNodeRef::NamedNode(class)),
Some(gl::TEMPLATE),
None,
None,
)
.filter_map(Result::ok)
.next()
{
if let TermRef::BlankNode(blank_node) = quad.object {
if let Term::BlankNode(blank_node) = quad.object {
let iter = self
.dataset
.quads_for_subject(blank_node)
.map(|quad| quad.into_owned())
.store
.quads_for_pattern(Some(NamedOrBlankNodeRef::BlankNode(blank_node.as_ref())), None, None, None)
.filter_map(Result::ok)
.map(Triple::from)
.map(move |mut triple| {
triple.subject = NamedOrBlankNode::NamedNode(subject.into_owned());