This commit is contained in:
Alex Wied
2026-06-17 20:50:26 -04:00
parent 6df00eb5a3
commit f5ca8dd9ae
7 changed files with 352 additions and 160 deletions
+76 -19
View File
@@ -99,6 +99,11 @@ rdam:P30154 rdf:type owl:ObjectProperty .
rdam:uniformResourceLocator.en rdf:type owl:ObjectProperty .
### https://graphofliberty.org/2026/04/ont/associatedProperty
:associatedProperty rdf:type owl:ObjectProperty ;
rdfs:label "associated property"@en .
### https://graphofliberty.org/2026/04/ont/indexedByField
:indexedByField rdf:type owl:ObjectProperty ;
rdfs:domain owl:DatatypeProperty ;
@@ -163,6 +168,18 @@ ldp:contains rdf:type owl:DatatypeProperty ;
rdfs:label "catalog id" .
### https://graphofliberty.org/2026/04/ont/fieldLabel
:fieldLabel rdf:type owl:DatatypeProperty ;
rdfs:comment "The label of a field, which shall be displayed to the user."@en ;
rdfs:label "field label"@en .
### https://graphofliberty.org/2026/04/ont/fieldName
:fieldName rdf:type owl:DatatypeProperty ;
rdfs:comment "The name of the field, as defined in the full-text search document schema."@en ;
rdfs:label "field name"@en .
### https://graphofliberty.org/2026/04/ont/readOnly
:readOnly rdf:type owl:DatatypeProperty ;
rdfs:comment "Indicates that the property or class is read only (server managed) and should not be made editable in user-facing applications."@en ;
@@ -195,8 +212,7 @@ rdac:C10002 rdf:type owl:Class ;
### http://rdaregistry.info/Elements/c/C10004
rdac:C10004 rdf:type owl:Class ;
rdfs:subClassOf rdac:C10002 ;
rdfs:label "Person"@en .
rdfs:subClassOf rdac:C10002 .
### http://rdaregistry.info/Elements/c/C10007
@@ -282,11 +298,6 @@ ldp:Resource rdf:type owl:Class .
rdfs:subClassOf :Entity .
### https://graphofliberty.org/2026/04/ont/Person
:Person rdf:type owl:Class ;
rdfs:subClassOf :Entity .
### https://graphofliberty.org/2026/04/ont/Podcast
:Podcast rdf:type owl:Class ;
rdfs:subClassOf :Entity .
@@ -366,6 +377,13 @@ rdaa:identifierForPerson.en rdf:type owl:NamedIndividual .
rdaa:surname.en rdf:type owl:NamedIndividual .
### http://rdaregistry.info/Elements/c/C10004
rdac:C10004 rdf:type owl:NamedIndividual ;
:associatedProperty rdaa:P50291 ,
rdaa:P50292 ;
:catalogId "8"^^xsd:nonNegativeInteger .
### http://rdaregistry.info/Elements/c/C10007
rdac:C10007 rdf:type owl:NamedIndividual ;
:template [ rdf:type rdac:C10007
@@ -385,14 +403,28 @@ rdam:uniformResourceLocator.en rdf:type owl:NamedIndividual .
### http://www.w3.org/1999/02/22-rdf-syntax-ns#Property
rdf:Property rdf:type owl:NamedIndividual ;
:associatedProperty rdfs:comment ,
rdfs:label ;
:catalogId "0"^^xsd:nonNegativeInteger .
### http://www.w3.org/2000/01/rdf-schema#Class
rdfs:Class rdf:type owl:NamedIndividual ;
:associatedProperty rdfs:comment ,
rdfs:label ;
:catalogId "1"^^xsd:nonNegativeInteger .
### http://www.w3.org/2000/01/rdf-schema#comment
rdfs:comment rdf:type owl:NamedIndividual ;
:indexedByField :comment .
### http://www.w3.org/2000/01/rdf-schema#label
rdfs:label rdf:type owl:NamedIndividual ;
:indexedByField :label .
### http://www.w3.org/ns/ldp#BasicContainer
ldp:BasicContainer rdf:type owl:NamedIndividual ;
:readOnly "true"^^xsd:boolean .
@@ -448,11 +480,6 @@ ldp:contains rdf:type owl:NamedIndividual ;
:catalogId "7"^^xsd:nonNegativeInteger .
### https://graphofliberty.org/2026/04/ont/Person
:Person rdf:type owl:NamedIndividual ;
:catalogId "8"^^xsd:nonNegativeInteger .
### https://graphofliberty.org/2026/04/ont/Podcast
:Podcast rdf:type owl:NamedIndividual ;
:catalogId "9"^^xsd:nonNegativeInteger .
@@ -463,18 +490,36 @@ ldp:contains rdf:type owl:NamedIndividual ;
:catalogId "10"^^xsd:nonNegativeInteger .
### https://graphofliberty.org/2026/04/ont/comment
:comment rdf:type owl:NamedIndividual ,
:IndexField ;
:fieldLabel "Comment"@en ;
:fieldName "comment" ;
rdfs:label "Comment Field"@en .
### https://graphofliberty.org/2026/04/ont/givenName
:givenName rdf:type owl:NamedIndividual ,
:IndexField ;
rdfs:label "Given Name"@en ;
rdfs:value "given name" .
:fieldLabel "Given Name"@en ;
:fieldName "givenName" ;
rdfs:label "Given Name Field"@en .
### https://graphofliberty.org/2026/04/ont/label
:label rdf:type owl:NamedIndividual ,
:IndexField ;
:fieldLabel "Label"@en ;
:fieldName "label" ;
rdfs:label "Label Field"@en .
### https://graphofliberty.org/2026/04/ont/surname
:surname rdf:type owl:NamedIndividual ,
:IndexField ;
rdfs:label "Surname"@en ;
rdfs:value "surname" .
:fieldLabel "Surname"@en ;
:fieldName "surname" ;
rdfs:label "Surname Field"@en .
#################################################################
@@ -490,12 +535,27 @@ rdaa:P50291 rdfs:label "has surname"@en .
rdaa:P50292 rdfs:label "has given name"@en .
rdac:C10004 rdfs:label "Person"@en .
rdac:C10007 rdfs:label "Manifestation"@en .
rdam:P30154 rdfs:label "has uniform resource locator"@en .
rdf:Property rdfs:label "Property"@en .
rdfs:Class rdfs:label "Class"@en .
rdfs:comment rdfs:label "Comment Property"@en .
rdfs:label rdfs:label "Label Property"@en .
:AudioBook rdfs:label "Audio Book"@en .
@@ -514,9 +574,6 @@ rdam:P30154 rdfs:label "has uniform resource locator"@en .
:Music rdfs:label "Music"@en .
:Person rdfs:label "Person"@en .
:Podcast rdfs:label "Podcast"@en .
+152 -61
View File
@@ -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>,
+7 -2
View File
@@ -3,16 +3,21 @@ pub mod gl {
pub const TEMPLATE: NamedNodeRef =
NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/template");
pub const INDEXED_BY_FIELD: NamedNodeRef =
NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/indexedByField");
pub const CATALOG_ID: NamedNodeRef =
NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/catalogId");
pub const ENTITY: NamedNodeRef =
NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/Entity");
pub const INDEX_FIELD: NamedNodeRef =
NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/IndexField");
pub const ASSOCIATED_PROPERTY: NamedNodeRef =
NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/associatedProperty");
/*pub const INDEX_FIELD: NamedNodeRef =
NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/IndexField");*/
}
pub mod owl {