.
This commit is contained in:
@@ -4,6 +4,10 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
gl-inference.workspace = true
|
||||
gl-search.workspace = true
|
||||
|
||||
oxigraph.workspace = true
|
||||
oxilangtag.workspace = true
|
||||
thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
@@ -0,0 +1,22 @@
|
||||
use oxigraph::model::NamedNodeRef;
|
||||
use crate::vocab;
|
||||
|
||||
pub enum Category {
|
||||
AudioBook = 0,
|
||||
}
|
||||
|
||||
impl Category {
|
||||
pub fn to_named_node(&self) -> NamedNodeRef<'_> {
|
||||
match self {
|
||||
Category::AudioBook => vocab::gl::AUDIO_BOOK,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_from_named_node<'a>(node: impl Into<NamedNodeRef<'a>>) -> Option<Self> {
|
||||
let node = node.into();
|
||||
match node {
|
||||
vocab::gl::AUDIO_BOOK => Some(Category::AudioBook),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
use oxigraph::model::NamedNodeRef;
|
||||
use crate::vocab;
|
||||
|
||||
pub enum Class {
|
||||
RdfProperty = 0,
|
||||
RdfsClass = 1,
|
||||
SkosConcept = 2,
|
||||
Work = 10001,
|
||||
Person = 10004,
|
||||
CorporateBody = 10005,
|
||||
Expression = 10006,
|
||||
Manifestation = 10007,
|
||||
}
|
||||
|
||||
impl Class {
|
||||
pub fn to_named_node(&self) -> NamedNodeRef<'_> {
|
||||
match self {
|
||||
Class::RdfProperty => vocab::rdf::PROPERTY,
|
||||
Class::RdfsClass => vocab::rdfs::CLASS,
|
||||
Class::SkosConcept => vocab::skos::CONCEPT,
|
||||
Class::Work => vocab::rdac::WORK,
|
||||
Class::Person => vocab::rdac::PERSON,
|
||||
Class::CorporateBody => vocab::rdac::CORPORATE_BODY,
|
||||
Class::Expression => vocab::rdac::EXPRESSION,
|
||||
Class::Manifestation => vocab::rdac::MANIFESTATION,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_from_named_node<'a>(node: impl Into<NamedNodeRef<'a>>) -> Option<Self> {
|
||||
let node = node.into();
|
||||
match node {
|
||||
vocab::rdf::PROPERTY => Some(Class::RdfProperty),
|
||||
vocab::rdfs::CLASS => Some(Class::RdfsClass),
|
||||
vocab::skos::CONCEPT => Some(Class::SkosConcept),
|
||||
vocab::rdac::WORK => Some(Class::Work),
|
||||
vocab::rdac::PERSON => Some(Class::Person),
|
||||
vocab::rdac::CORPORATE_BODY => Some(Class::CorporateBody),
|
||||
vocab::rdac::EXPRESSION => Some(Class::Expression),
|
||||
vocab::rdac::MANIFESTATION => Some(Class::Manifestation),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use oxigraph::model::{NamedNode, NamedNodeRef, Term, TermRef};
|
||||
use oxigraph::model::vocab::xsd;
|
||||
use crate::vocab::rdf;
|
||||
|
||||
pub(crate) fn term_to_named_node(term: &Term) -> Option<&NamedNode> {
|
||||
if let Term::NamedNode(node) = term {
|
||||
Some(node)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn term_ref_as_named_node(term: TermRef<'_>) -> Option<NamedNodeRef<'_>> {
|
||||
if let TermRef::NamedNode(node) = term {
|
||||
Some(node)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn term_as_str(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
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn term_ref_as_str(term: TermRef<'_>) -> Option<&str> {
|
||||
if let TermRef::Literal(literal) = term {
|
||||
match literal.datatype() {
|
||||
xsd::STRING | xsd::NORMALIZED_STRING | rdf::LANG_STRING | rdf::DIR_LANG_STRING => {
|
||||
Some(literal.value())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
use std::collections::HashMap;
|
||||
use oxigraph::model::NamedNode;
|
||||
use oxigraph::sparql::results::{QueryResultsFormat, QueryResultsParser, SliceQueryResultsParserOutput};
|
||||
use gl_inference::Channel;
|
||||
use gl_inference::proto::ontology_client::OntologyClient;
|
||||
use gl_inference::proto::OntologyQueryRequest;
|
||||
use gl_search::{Field, OwnedValue, Schema};
|
||||
use crate::class::Class;
|
||||
use crate::{curie, CurieHelper};
|
||||
use crate::helpers::{term_as_str, term_to_named_node};
|
||||
use crate::language::LanguageCondition;
|
||||
|
||||
pub async fn index_ontology(
|
||||
client: &mut OntologyClient<Channel>,
|
||||
language: &LanguageCondition,
|
||||
) -> crate::Result<Vec<HashMap<Field, OwnedValue>>> {
|
||||
let curie_helper = CurieHelper::new((&*curie::PREFIXES).clone());
|
||||
|
||||
let label_filter = language.to_filter_expression("label");
|
||||
let description_filter = language.to_filter_expression("description");
|
||||
|
||||
let mut request = OntologyQueryRequest::default();
|
||||
request.sparql_query = format!(r#"PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
|
||||
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|
||||
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
|
||||
|
||||
SELECT ?class ?subject ?label ?description WHERE {{
|
||||
VALUES ?class {{ rdf:Property rdfs:Class skos:Concept }}
|
||||
?subject a ?class ;
|
||||
rdfs:label ?label .
|
||||
{label_filter}
|
||||
|
||||
OPTIONAL {{ ?subject rdfs:comment ?comment }}
|
||||
OPTIONAL {{ ?subject skos:definition ?definition }}
|
||||
BIND(COALESCE(?definition, ?comment) AS ?description)
|
||||
{description_filter}
|
||||
}}"#);
|
||||
|
||||
let response = client.query(request).await.unwrap();
|
||||
let parser_output = QueryResultsParser::from_format(QueryResultsFormat::Json)
|
||||
.for_slice(&response.get_ref().results)
|
||||
.unwrap();
|
||||
|
||||
let mut results = Vec::new();
|
||||
if let SliceQueryResultsParserOutput::Solutions(solutions) = parser_output {
|
||||
let primary_language = language.primary_language();
|
||||
let label_field = Schema::field("label", primary_language);
|
||||
let definition_field = Schema::field("definition", primary_language);
|
||||
for solution in solutions.filter_map(Result::ok) {
|
||||
let mut document = HashMap::with_capacity(4);
|
||||
|
||||
let discriminant = solution
|
||||
.get("class")
|
||||
.and_then(term_to_named_node)
|
||||
.and_then(Class::try_from_named_node)
|
||||
.map(|doc_type| doc_type as u64)
|
||||
.map(OwnedValue::U64)
|
||||
.unwrap_or(OwnedValue::Null);
|
||||
document.insert(Schema::discriminant_field(), discriminant);
|
||||
|
||||
let subject_str = solution
|
||||
.get("subject")
|
||||
.and_then(term_to_named_node)
|
||||
.map(NamedNode::as_str);
|
||||
|
||||
let curie = subject_str
|
||||
.and_then(|subject| curie_helper.abbreviate(None, subject))
|
||||
.map(OwnedValue::Str)
|
||||
.unwrap_or(OwnedValue::Null);
|
||||
document.insert(Schema::curie_field(), curie);
|
||||
|
||||
let subject = subject_str
|
||||
.map(OwnedValue::from)
|
||||
.unwrap_or(OwnedValue::Null);
|
||||
document.insert(Schema::iri_field(), subject);
|
||||
|
||||
let label = solution
|
||||
.get("label")
|
||||
.and_then(term_as_str)
|
||||
.map(OwnedValue::from)
|
||||
.unwrap_or(OwnedValue::Null);
|
||||
document.insert(label_field, label);
|
||||
|
||||
let definition = solution
|
||||
.get("description")
|
||||
.and_then(term_as_str)
|
||||
.map(OwnedValue::from)
|
||||
.unwrap_or(OwnedValue::Null);
|
||||
document.insert(definition_field, definition);
|
||||
|
||||
results.push(document);
|
||||
}
|
||||
}
|
||||
Ok(results)
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
use oxigraph::model::{Dataset, GraphNameRef, NamedNodeRef, Quad, QuadRef};
|
||||
use oxigraph::sparql::{QueryResults, SparqlEvaluator};
|
||||
use oxigraph::store::Store;
|
||||
use tracing::{debug_span, field};
|
||||
|
||||
const RDF_SCHEMA_PREFIX: &str = "http://www.w3.org/2000/01/rdf-schema#";
|
||||
const GL_PREFIX: &str = "https://graphofliberty.org/";
|
||||
|
||||
const INFERENCE_GRAPH: GraphNameRef = GraphNameRef::NamedNode(NamedNodeRef::new_unchecked(
|
||||
"https://graphofliberty.org/inference",
|
||||
));
|
||||
|
||||
/// `prp-spo1`
|
||||
const SUB_PROPERTY_OF_QUERY: &str = r#"CONSTRUCT {
|
||||
?x ?p2 ?y .
|
||||
} WHERE {
|
||||
?p1 rdfs:subPropertyOf+ ?p2 .
|
||||
GRAPH gl:inference { ?x ?p1 ?y . }
|
||||
}"#;
|
||||
|
||||
/// `cax-sco`
|
||||
const SUB_CLASS_OF_QUERY: &str = r#"CONSTRUCT {
|
||||
?x a ?c2 .
|
||||
} WHERE {
|
||||
?c1 rdfs:subClassOf+ ?c2 .
|
||||
GRAPH gl:inference { ?x a ?c1 . }
|
||||
}"#;
|
||||
|
||||
/// `prp-dom`
|
||||
const DOMAIN_QUERY: &str = r#"CONSTRUCT {
|
||||
?x a ?c .
|
||||
} WHERE {
|
||||
?p rdfs:domain ?c .
|
||||
GRAPH gl:inference { ?x ?p ?y . }
|
||||
}"#;
|
||||
|
||||
/// https://www.w3.org/TR/owl2-profiles/#Reasoning_in_OWL_2_RL_and_RDF_Graphs_using_Rules
|
||||
#[derive(Clone)]
|
||||
pub struct InferenceEngine {
|
||||
ontology: Dataset,
|
||||
}
|
||||
|
||||
fn run_query(name: &str, query: &str, dataset: &Dataset) -> crate::Result<Dataset> {
|
||||
let span = debug_span!("Query", name).entered();
|
||||
|
||||
let mut query = SparqlEvaluator::new()
|
||||
.with_prefix("rdfs", RDF_SCHEMA_PREFIX)?
|
||||
.with_prefix("gl", GL_PREFIX)?
|
||||
.parse_query(query)?;
|
||||
|
||||
query.dataset_mut().set_default_graph_as_union();
|
||||
|
||||
let inferences =
|
||||
if let QueryResults::Graph(result) = query.on_queryable_dataset(dataset).execute()? {
|
||||
result
|
||||
.filter_map(Result::ok)
|
||||
.map(|triple| {
|
||||
Quad::new(
|
||||
triple.subject,
|
||||
triple.predicate,
|
||||
triple.object,
|
||||
INFERENCE_GRAPH,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Dataset::new()
|
||||
};
|
||||
|
||||
Ok(inferences)
|
||||
}
|
||||
|
||||
fn run_recursively(mut dataset: Dataset) -> crate::Result<Dataset> {
|
||||
let sub_property_of_result = run_query("rdfs:subPropertyOf", SUB_PROPERTY_OF_QUERY, &dataset)?;
|
||||
let sub_class_of_result = run_query("rdfs:subClassOf", SUB_CLASS_OF_QUERY, &dataset)?;
|
||||
let domain_result = run_query("rdfs:domain", DOMAIN_QUERY, &dataset)?;
|
||||
|
||||
let old_count = dataset.len();
|
||||
dataset.extend(&sub_property_of_result);
|
||||
dataset.extend(&sub_class_of_result);
|
||||
dataset.extend(&domain_result);
|
||||
let new_count = dataset.len();
|
||||
|
||||
if new_count > old_count {
|
||||
run_recursively(dataset)
|
||||
} else {
|
||||
Ok(dataset)
|
||||
}
|
||||
}
|
||||
|
||||
impl InferenceEngine {
|
||||
pub fn new(store: Store) -> Self {
|
||||
let mut ontology = Dataset::new();
|
||||
debug_span!("Load Ontology").in_scope(|| {
|
||||
ontology.extend(store.iter().filter_map(Result::ok));
|
||||
});
|
||||
|
||||
Self { ontology }
|
||||
}
|
||||
|
||||
pub fn run(&self, dataset: &Dataset) -> crate::Result<Dataset> {
|
||||
let span = debug_span!("Inference", inferences = field::Empty).entered();
|
||||
|
||||
let input = dataset.iter().map(|triple| {
|
||||
QuadRef::new(
|
||||
triple.subject,
|
||||
triple.predicate,
|
||||
triple.object,
|
||||
INFERENCE_GRAPH,
|
||||
)
|
||||
});
|
||||
let mut ontology = self.ontology.clone();
|
||||
ontology.extend(input);
|
||||
|
||||
let result: Dataset = run_recursively(ontology)?
|
||||
.quads_for_graph_name(INFERENCE_GRAPH)
|
||||
.collect();
|
||||
|
||||
span.record("inferences", result.len());
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
use oxigraph::model::TermRef;
|
||||
use oxilangtag::LanguageTag;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
pub const ENGLISH_PRIMARY: &str = "en";
|
||||
|
||||
pub static ENGLISH_OR_UNTAGGED: LazyLock<LanguageCondition> = LazyLock::new(|| {
|
||||
LanguageCondition::ExactMatchOrUntagged(
|
||||
LanguageTag::parse(ENGLISH_PRIMARY.to_string()).unwrap(),
|
||||
)
|
||||
});
|
||||
|
||||
pub enum LanguageCondition {
|
||||
ExactMatchOnly(LanguageTag<String>),
|
||||
ExactMatchOrUntagged(LanguageTag<String>),
|
||||
UntaggedOnly,
|
||||
AnyOrNone,
|
||||
}
|
||||
|
||||
impl LanguageCondition {
|
||||
pub fn primary_matches_term<'a>(&self, term: impl Into<TermRef<'a>>) -> bool {
|
||||
if let TermRef::Literal(literal) = term.into() {
|
||||
let tag = literal
|
||||
.language()
|
||||
.map(LanguageTag::parse_and_normalize)
|
||||
.and_then(Result::ok);
|
||||
|
||||
match (tag, self) {
|
||||
(Some(language), LanguageCondition::ExactMatchOnly(expectation))
|
||||
| (Some(language), LanguageCondition::ExactMatchOrUntagged(expectation)) => {
|
||||
language.primary_language() == expectation.primary_language()
|
||||
}
|
||||
(None, LanguageCondition::ExactMatchOrUntagged(_)) => true,
|
||||
(None, LanguageCondition::UntaggedOnly) => true,
|
||||
(_, LanguageCondition::AnyOrNone) => true,
|
||||
_ => false,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn primary_language(&self) -> Option<&str> {
|
||||
match self {
|
||||
LanguageCondition::ExactMatchOnly(tag) => Some(tag.primary_language()),
|
||||
LanguageCondition::ExactMatchOrUntagged(tag) => Some(tag.primary_language()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_filter_expression(&self, var: &str) -> String {
|
||||
match self {
|
||||
LanguageCondition::ExactMatchOnly(language) => {
|
||||
format!(r#"FILTER(langMATCHES(LANG(?{var}), "{language}"))"#)
|
||||
}
|
||||
LanguageCondition::ExactMatchOrUntagged(language) => {
|
||||
format!(r#"FILTER(langMATCHES(LANG(?{var}), "{language}") || !hasLANG(?{var}))"#)
|
||||
}
|
||||
LanguageCondition::UntaggedOnly => format!("FILTER(!hasLANG(?{var}))"),
|
||||
LanguageCondition::AnyOrNone => "".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -2,8 +2,12 @@ mod curie;
|
||||
//mod materialize;
|
||||
mod error;
|
||||
|
||||
pub mod inference;
|
||||
pub mod vocab;
|
||||
pub mod class;
|
||||
pub mod category;
|
||||
pub mod language;
|
||||
mod helpers;
|
||||
pub mod index;
|
||||
|
||||
pub use curie::{CurieHelper, PREFIXES};
|
||||
pub use error::{Error, Result};
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
use crate::vocab;
|
||||
use oxigraph::model::{Dataset, GraphName, NamedNodeRef, NamedOrBlankNode, NamedOrBlankNodeRef, Term};
|
||||
use oxigraph::sparql::SparqlEvaluator;
|
||||
use oxigraph::store::Store;
|
||||
use tracing::{debug, debug_span};
|
||||
|
||||
|
||||
const RDF_SCHEMA: NamedNodeRef = NamedNodeRef::new_unchecked("http://www.w3.org/2000/01/rdf-schema#");
|
||||
|
||||
pub fn same_as(store: &mut Store) -> crate::Result<()> {
|
||||
let _span = debug_span!("Materialize owl:sameAs").entered();
|
||||
|
||||
let additional_quads = store
|
||||
.quads_for_pattern(None, Some(vocab::owl::SAME_AS), None, None)
|
||||
.filter_map(Result::ok)
|
||||
.fold(Dataset::new(), |mut new_dataset, alias| {
|
||||
if let NamedOrBlankNode::NamedNode(x) = alias.subject
|
||||
&& let Term::NamedNode(y) = alias.object
|
||||
{
|
||||
for mut quad in store.quads_for_pattern(
|
||||
Some(NamedOrBlankNodeRef::NamedNode(x.as_ref())),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
).filter_map(Result::ok) {
|
||||
quad.subject = NamedOrBlankNode::NamedNode(y.clone());
|
||||
quad.graph_name = GraphName::NamedNode(INFERENCE_GRAPH.into_owned());
|
||||
new_dataset.insert(quad.as_ref());
|
||||
}
|
||||
|
||||
for mut quad in store.quads_for_pattern(
|
||||
Some(NamedOrBlankNodeRef::NamedNode(y.as_ref())),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
).filter_map(Result::ok) {
|
||||
quad.subject = NamedOrBlankNode::NamedNode(x.clone());
|
||||
quad.graph_name = GraphName::NamedNode(INFERENCE_GRAPH.into_owned());
|
||||
new_dataset.insert(quad.as_ref());
|
||||
}
|
||||
}
|
||||
new_dataset
|
||||
});
|
||||
|
||||
let old_size = store.len()?;
|
||||
store.extend(&additional_quads)?;
|
||||
let new_size = store.len()?;
|
||||
|
||||
if new_size > old_size {
|
||||
let difference = new_size - old_size;
|
||||
debug!(?old_size, ?new_size, ?difference, "same_as");
|
||||
same_as(store)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn super_properties(store: &mut Store) -> crate::Result<()> {
|
||||
let _span = debug_span!("Materialize rdfs:subPropertyOf").entered();
|
||||
|
||||
let update = SparqlEvaluator::new()
|
||||
.with_prefix("rdfs", RDF_SCHEMA.as_str())?
|
||||
.with_prefix("inference", INFERENCE_GRAPH.as_str())?
|
||||
.parse_update(r#"INSERT {
|
||||
GRAPH inference: {
|
||||
?property rdfs:subPropertyOf ?parent .
|
||||
?subject ?parent ?object .
|
||||
}
|
||||
} WHERE {
|
||||
GRAPH ?g1 { ?property rdfs:subPropertyOf+ ?parent }
|
||||
OPTIONAL { GRAPH ?g2 { ?subject ?property ?object } }
|
||||
}"#)?;
|
||||
|
||||
let old_size = store.len()?;
|
||||
update.on_store(&store).execute()?;
|
||||
let new_size = store.len()?;
|
||||
|
||||
if new_size > old_size {
|
||||
let difference = new_size - old_size;
|
||||
debug!(?old_size, ?new_size, ?difference, "super_properties");
|
||||
super_properties(store)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn super_classes(store: &mut Store) -> crate::Result<()> {
|
||||
let _span = debug_span!("Materialize rdfs:subClassOf").entered();
|
||||
|
||||
let update = SparqlEvaluator::new()
|
||||
.with_prefix("rdfs", RDF_SCHEMA.as_str())?
|
||||
.with_prefix("inference", INFERENCE_GRAPH.as_str())?
|
||||
.parse_update(r#"INSERT {
|
||||
GRAPH inference: {
|
||||
?class rdfs:subClassOf ?parent .
|
||||
?item a ?parent .
|
||||
}
|
||||
} WHERE {
|
||||
GRAPH ?g1 { ?class rdfs:subClassOf+ ?parent }
|
||||
OPTIONAL { GRAPH ?g2 { ?item a ?class } }
|
||||
}"#)?;
|
||||
|
||||
let old_size = store.len()?;
|
||||
update.on_store(&store).execute()?;
|
||||
let new_size = store.len()?;
|
||||
|
||||
if new_size > old_size {
|
||||
let difference = new_size - old_size;
|
||||
debug!(?old_size, ?new_size, ?difference, "super_classes");
|
||||
super_classes(store)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+2
-17
@@ -4,23 +4,8 @@ pub use oxigraph::model::vocab::rdfs;
|
||||
pub mod gl {
|
||||
use oxigraph::model::NamedNodeRef;
|
||||
|
||||
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 CATEGORY_ID: NamedNodeRef =
|
||||
NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/categoryId");
|
||||
|
||||
pub const SEARCHABLE_CLASS: NamedNodeRef =
|
||||
NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/SearchableClass");
|
||||
|
||||
pub const ENTITY: NamedNodeRef =
|
||||
NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/Entity");
|
||||
|
||||
pub const ASSOCIATED_PROPERTY: NamedNodeRef =
|
||||
NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/associatedProperty");
|
||||
pub const AUDIO_BOOK: NamedNodeRef =
|
||||
NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/AudioBook");
|
||||
|
||||
pub const READ_ONLY: NamedNodeRef =
|
||||
NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/readOnly");
|
||||
|
||||
Reference in New Issue
Block a user