.
This commit is contained in:
+152
-61
@@ -1,5 +1,5 @@
|
||||
use crate::error;
|
||||
use crate::rdf::vocab::{gl, owl};
|
||||
use crate::rdf::vocab::{gl, owl, rda};
|
||||
use oxigraph::io::{RdfFormat, RdfParser};
|
||||
use oxigraph::model::vocab::{rdf, rdfs, xsd};
|
||||
use oxigraph::model::{
|
||||
@@ -7,9 +7,10 @@ use oxigraph::model::{
|
||||
TermRef, Triple, TripleRef,
|
||||
};
|
||||
use oxigraph::sparql::{QueryResults, SparqlEvaluator};
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt::Display;
|
||||
use iced::widget::sensor::Key;
|
||||
use tracing::debug;
|
||||
use tracing::{debug, info};
|
||||
|
||||
const PREFIXES: &[(&str, &str)] = &[
|
||||
("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"),
|
||||
@@ -155,11 +156,11 @@ SELECT DISTINCT ?class ?subject ?label ?comment ?read_only {
|
||||
?subject a ?class .
|
||||
OPTIONAL {
|
||||
?subject rdfs:label ?label
|
||||
FILTER (LANG(?label) = 'en' || LANG(?label) = '')
|
||||
FILTER (langMATCHES(LANG(?label), "en") || !hasLANG(?label))
|
||||
}
|
||||
OPTIONAL {
|
||||
?subject rdfs:comment ?comment
|
||||
FILTER (LANG(?comment) = 'en' || LANG(?comment) = '')
|
||||
FILTER (langMATCHES(LANG(?label), "en") || !hasLANG(?comment))
|
||||
}
|
||||
OPTIONAL { ?subject gl:readOnly ?read_only }
|
||||
}"#,
|
||||
@@ -200,11 +201,11 @@ SELECT DISTINCT ?class ?subject ?label ?comment ?read_only {
|
||||
r#"PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|
||||
PREFIX gl: <https://graphofliberty.org/2026/04/ont/>
|
||||
|
||||
SELECT DISTINCT ?subject ?key ?label {
|
||||
SELECT DISTINCT ?subject ?name ?label {
|
||||
?subject a gl:IndexField ;
|
||||
rdfs:value ?key ;
|
||||
rdfs:label ?label .
|
||||
FILTER(langMATCHES(LANG(?label), "en") || !hasLANG(?label))
|
||||
gl:fieldName ?name ;
|
||||
gl:fieldLabel ?label .
|
||||
FILTER (langMATCHES(LANG(?label), "en") || !hasLANG(?label))
|
||||
}"#,
|
||||
)
|
||||
.expect("Unable to parse field query");
|
||||
@@ -215,13 +216,13 @@ SELECT DISTINCT ?subject ?key ?label {
|
||||
{
|
||||
for solution in solutions.filter_map(Result::ok) {
|
||||
let subject = solution.get("subject").and_then(term_to_named_node);
|
||||
let key = solution.get("key").and_then(term_to_string);
|
||||
let name = solution.get("name").and_then(term_to_string);
|
||||
let label = solution.get("label").and_then(term_to_string);
|
||||
|
||||
if let Some(subject) = subject && let Some(key) = key {
|
||||
if let Some(subject) = subject && let Some(name) = name {
|
||||
let field = IndexField {
|
||||
key: key.to_owned(),
|
||||
label: label.map(String::from),
|
||||
name: name.to_string(),
|
||||
label: label.map(|l| l.to_string()),
|
||||
};
|
||||
results.insert(subject.to_owned(), field);
|
||||
}
|
||||
@@ -230,6 +231,34 @@ SELECT DISTINCT ?subject ?key ?label {
|
||||
results
|
||||
}
|
||||
|
||||
fn subclass_of(dataset: &Dataset, 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(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 build(&mut self) -> error::Result<Ontology> {
|
||||
let prefixes = PREFIXES
|
||||
.iter()
|
||||
@@ -251,6 +280,41 @@ SELECT DISTINCT ?subject ?key ?label {
|
||||
// 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
|
||||
@@ -260,30 +324,35 @@ SELECT DISTINCT ?subject ?key ?label {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
dataset,
|
||||
prefixes,
|
||||
iri_info,
|
||||
fields,
|
||||
entities,
|
||||
indexed_by,
|
||||
catalog_ids,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LabeledIri {
|
||||
pub iri: NamedNode,
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
impl PartialEq for LabeledIri {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.iri == other.iri
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for LabeledIri {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.label.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct IriInformation {
|
||||
pub type_: NamedNode,
|
||||
@@ -292,19 +361,34 @@ pub struct IriInformation {
|
||||
pub read_only: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Entity {
|
||||
pub label: String,
|
||||
pub catalog_id: u64,
|
||||
pub properties: HashSet<NamedNode>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct IndexField {
|
||||
pub key: String,
|
||||
pub name: String,
|
||||
pub label: Option<String>,
|
||||
}
|
||||
|
||||
pub struct Ontology {
|
||||
dataset: Dataset,
|
||||
prefixes: HashMap<String, String>,
|
||||
|
||||
// Resource (Property or Class) -> Rust Type
|
||||
iri_info: HashMap<NamedNode, IriInformation>,
|
||||
|
||||
// NamedIndividual of class IndexField -> Rust Type
|
||||
fields: HashMap<NamedNode, IndexField>,
|
||||
|
||||
// NamedIndividual of class Entity -> Rust Type
|
||||
entities: HashMap<NamedNode, Entity>,
|
||||
|
||||
// Property -> NamedIndividual of class IndexField
|
||||
indexed_by: HashMap<NamedNode, NamedNode>,
|
||||
catalog_ids: HashMap<NamedNode, u64>,
|
||||
}
|
||||
|
||||
fn term_to_named_node(term: &Term) -> Option<&NamedNode> {
|
||||
@@ -340,6 +424,14 @@ fn term_to_boolean(term: &Term) -> Option<bool> {
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
OntologyBuilder {
|
||||
@@ -377,12 +469,39 @@ impl Ontology {
|
||||
.and_then(|node| self.fields.get(node))
|
||||
}
|
||||
|
||||
pub fn catalog_id(&self, class: &NamedNode) -> Option<u64> {
|
||||
self.catalog_ids.get(class).copied()
|
||||
pub fn fields_for_class(&self, class: &NamedNode) -> Vec<&IndexField> {
|
||||
self.entities.get(class)
|
||||
.and_then(|entity| {
|
||||
entity.properties.iter()
|
||||
.map(|property| self.field_for_property(property))
|
||||
.filter(Option::is_some)
|
||||
.collect()
|
||||
}).unwrap_or(Vec::new())
|
||||
}
|
||||
|
||||
pub fn iri_info(&self) -> &HashMap<NamedNode, IriInformation>
|
||||
{
|
||||
pub fn catalog_id(&self, class: &NamedNode) -> Option<u64> {
|
||||
self.entities
|
||||
.get(class)
|
||||
.map(|entity| entity.catalog_id)
|
||||
}
|
||||
|
||||
pub fn labeled_entity(&self, class: &NamedNode) -> Option<LabeledIri> {
|
||||
self.entities.get(class)
|
||||
.map(|entity| LabeledIri {
|
||||
iri: class.to_owned(),
|
||||
label: entity.label.to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn labeled_entities(&self) -> impl Iterator<Item = LabeledIri> {
|
||||
self.entities.iter()
|
||||
.map(|(iri, entity)| LabeledIri {
|
||||
iri: iri.to_owned(),
|
||||
label: entity.label.to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iri_info(&self) -> &HashMap<NamedNode, IriInformation> {
|
||||
&self.iri_info
|
||||
}
|
||||
|
||||
@@ -423,34 +542,6 @@ impl Ontology {
|
||||
Self::is_read_only_impl(&self.iri_info, triple)
|
||||
}
|
||||
|
||||
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>,
|
||||
|
||||
Reference in New Issue
Block a user