2026-06-08 19:33:49 -04:00
|
|
|
use crate::error;
|
2026-06-30 19:25:15 -04:00
|
|
|
use crate::rdf::vocab::gl;
|
2026-06-08 19:33:49 -04:00
|
|
|
use oxigraph::model::vocab::{rdf, rdfs, xsd};
|
2026-07-07 22:06:39 -04:00
|
|
|
use oxigraph::model::{Dataset, LiteralRef, NamedNode, NamedNodeRef, NamedOrBlankNode, NamedOrBlankNodeRef, Term, TermRef, Triple, TripleRef};
|
2026-06-08 19:33:49 -04:00
|
|
|
use oxigraph::sparql::{QueryResults, SparqlEvaluator};
|
2026-07-07 22:06:39 -04:00
|
|
|
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
|
2026-06-17 20:50:26 -04:00
|
|
|
use std::fmt::Display;
|
2026-06-23 21:55:52 -04:00
|
|
|
use std::path::{Path, PathBuf};
|
2026-06-26 11:28:44 -04:00
|
|
|
use std::sync::LazyLock;
|
2026-06-23 21:55:52 -04:00
|
|
|
use oxigraph::store::Store;
|
2026-07-07 22:06:39 -04:00
|
|
|
use tracing::{debug_span, info};
|
2026-07-02 14:09:54 -04:00
|
|
|
use crate::rdf::{conversion, materialize};
|
2026-07-07 22:06:39 -04:00
|
|
|
use crate::rdf::conversion::{quad_into_term, term_into_named_node, term_into_string, term_to_named_node};
|
2026-07-01 17:42:23 -04:00
|
|
|
use crate::rdf::language::LanguageCondition;
|
2026-06-08 19:33:49 -04:00
|
|
|
|
2026-06-26 11:28:44 -04:00
|
|
|
static PREFIXES: LazyLock<BTreeMap<String, String>> = LazyLock::new(|| {
|
|
|
|
|
BTreeMap::from_iter([
|
|
|
|
|
("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/"),
|
2026-07-01 17:42:23 -04:00
|
|
|
("rdaf", "http://rdaregistry.info/Elements/rof/"),
|
2026-06-26 11:28:44 -04:00
|
|
|
("rdat", "http://rdaregistry.info/Elements/t/"),
|
2026-07-01 17:42:23 -04:00
|
|
|
("rdau", "http://rdaregistry.info/Elements/u/"),
|
2026-06-26 11:28:44 -04:00
|
|
|
("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/"),
|
|
|
|
|
].map(|(k, v)| (k.to_string(), v.to_string())))
|
|
|
|
|
});
|
2026-06-08 19:33:49 -04:00
|
|
|
|
2026-06-23 21:55:52 -04:00
|
|
|
pub struct OntologyBuilder {
|
|
|
|
|
path: Option<PathBuf>,
|
2026-06-08 19:33:49 -04:00
|
|
|
}
|
|
|
|
|
|
2026-06-23 21:55:52 -04:00
|
|
|
impl OntologyBuilder {
|
|
|
|
|
pub fn with_path(mut self, path: impl AsRef<Path>) -> Self {
|
|
|
|
|
let path = path.as_ref().to_owned();
|
|
|
|
|
self.path = Some(path);
|
2026-06-08 19:33:49 -04:00
|
|
|
self
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-23 21:55:52 -04:00
|
|
|
pub fn build(self) -> error::Result<Ontology> {
|
2026-06-29 15:20:02 -04:00
|
|
|
let mut store = if let Some(path) = self.path {
|
2026-07-02 14:09:54 -04:00
|
|
|
Store::open_read_only(path)
|
2026-06-23 21:55:52 -04:00
|
|
|
} else {
|
|
|
|
|
Store::new()
|
|
|
|
|
}?;
|
2026-06-08 19:33:49 -04:00
|
|
|
|
2026-06-23 21:55:52 -04:00
|
|
|
let prefixes = PREFIXES
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|(k, v)| (k.to_string(), v.to_string()))
|
|
|
|
|
.collect::<HashMap<String, String>>();
|
2026-06-08 19:33:49 -04:00
|
|
|
|
2026-07-01 17:42:23 -04:00
|
|
|
/*materialize::same_as(&mut store)?;
|
2026-06-30 19:25:15 -04:00
|
|
|
materialize::super_properties(&mut store)?;
|
2026-06-29 15:20:02 -04:00
|
|
|
materialize::super_classes(&mut store)?;
|
2026-07-01 17:42:23 -04:00
|
|
|
debug_span!("Optimize Ontology").in_scope(|| store.optimize())?;*/
|
2026-06-23 21:55:52 -04:00
|
|
|
|
|
|
|
|
let mut indexed_by = HashMap::new();
|
2026-06-29 15:20:02 -04:00
|
|
|
for quad in store.quads_for_pattern(None, Some(gl::INDEXED_BY_FIELD), None, None)
|
|
|
|
|
.filter_map(Result::ok) {
|
|
|
|
|
if let NamedOrBlankNode::NamedNode(subject) = quad.subject
|
2026-07-01 21:28:12 -04:00
|
|
|
&& let Term::NamedNode(field) = quad.object {
|
2026-06-29 15:20:02 -04:00
|
|
|
indexed_by.insert(subject, field);
|
2026-06-23 21:55:52 -04:00
|
|
|
}
|
2026-06-29 15:20:02 -04:00
|
|
|
}
|
2026-06-23 21:55:52 -04:00
|
|
|
|
|
|
|
|
Ok(Ontology {
|
|
|
|
|
store,
|
|
|
|
|
prefixes,
|
|
|
|
|
})
|
2026-06-09 15:40:28 -04:00
|
|
|
}
|
2026-07-01 17:42:23 -04:00
|
|
|
}
|
2026-06-09 15:40:28 -04:00
|
|
|
|
2026-07-01 17:42:23 -04:00
|
|
|
pub struct IndexEntry {
|
|
|
|
|
pub catalog_id: u64,
|
|
|
|
|
pub fields: HashMap<String, String>,
|
2026-06-08 19:33:49 -04:00
|
|
|
}
|
|
|
|
|
|
2026-06-17 20:50:26 -04:00
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
pub struct LabeledIri {
|
|
|
|
|
pub iri: NamedNode,
|
2026-07-01 17:42:23 -04:00
|
|
|
pub label: String,
|
2026-06-17 20:50:26 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 {
|
2026-07-01 17:42:23 -04:00
|
|
|
write!(f, "{}", self.label)
|
2026-06-17 20:50:26 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-15 19:10:55 -04:00
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
pub struct IndexField {
|
2026-06-17 20:50:26 -04:00
|
|
|
pub name: String,
|
2026-07-01 17:42:23 -04:00
|
|
|
pub label: String,
|
2026-06-15 19:10:55 -04:00
|
|
|
}
|
|
|
|
|
|
2026-06-08 19:33:49 -04:00
|
|
|
pub struct Ontology {
|
2026-06-23 21:55:52 -04:00
|
|
|
store: Store,
|
2026-06-08 19:33:49 -04:00
|
|
|
prefixes: HashMap<String, String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Ontology {
|
2026-06-23 21:55:52 -04:00
|
|
|
pub fn builder() -> OntologyBuilder {
|
2026-06-08 19:33:49 -04:00
|
|
|
OntologyBuilder {
|
2026-06-23 21:55:52 -04:00
|
|
|
path: None,
|
2026-06-08 19:33:49 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-26 11:28:44 -04:00
|
|
|
pub fn prefixes(&self) -> &BTreeMap<String, String> {
|
|
|
|
|
&*PREFIXES
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-01 17:42:23 -04:00
|
|
|
pub fn index(&self, language: &LanguageCondition) -> error::Result<HashMap<NamedNode, IndexEntry>> {
|
|
|
|
|
let language_filter = language.to_filter_expression("fieldValue");
|
|
|
|
|
let query = format!(r#"SELECT ?individual ?catalogId ?fieldName ?fieldValue
|
|
|
|
|
WHERE {{
|
2026-07-07 22:06:39 -04:00
|
|
|
?class a gl:SearchableClass ;
|
2026-07-01 17:42:23 -04:00
|
|
|
gl:catalogId ?catalogId ;
|
|
|
|
|
gl:associatedProperty ?property .
|
|
|
|
|
|
|
|
|
|
?property gl:indexedByField/gl:fieldName ?fieldName .
|
|
|
|
|
|
2026-07-07 22:06:39 -04:00
|
|
|
?individual a ?class ;
|
2026-07-01 17:42:23 -04:00
|
|
|
?property ?fieldValue .
|
|
|
|
|
|
|
|
|
|
{language_filter}
|
|
|
|
|
}}"#);
|
|
|
|
|
let mut sparql = SparqlEvaluator::new()
|
|
|
|
|
.with_prefix("gl", "https://graphofliberty.org/2026/04/ont/")?
|
|
|
|
|
.parse_query(&query)?;
|
|
|
|
|
sparql.dataset_mut().set_default_graph_as_union();
|
|
|
|
|
|
|
|
|
|
let mut results = HashMap::new();
|
|
|
|
|
if let QueryResults::Solutions(solutions) = sparql.on_store(&self.store).execute()? {
|
|
|
|
|
for solution in solutions.filter_map(Result::ok) {
|
|
|
|
|
let individual = solution.get("individual").and_then(conversion::term_to_named_node);
|
|
|
|
|
let catalog_id = solution.get("catalogId").and_then(conversion::term_to_u64);
|
|
|
|
|
let field_name = solution.get("fieldName").and_then(conversion::term_as_str);
|
|
|
|
|
let field_value = solution.get("fieldValue").and_then(conversion::term_as_str);
|
|
|
|
|
if let (Some(individual), Some(catalog_id), Some(field_name), Some(field_value)) = (individual, catalog_id, field_name, field_value) {
|
|
|
|
|
results.entry(individual.to_owned())
|
|
|
|
|
.and_modify(|entry: &mut IndexEntry| {
|
|
|
|
|
entry.fields.insert(field_name.to_owned(), field_value.to_owned());
|
|
|
|
|
}).or_insert(IndexEntry {
|
|
|
|
|
catalog_id,
|
|
|
|
|
fields: HashMap::from_iter([(field_name.to_owned(), field_value.to_owned())]),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(results)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-01 21:28:12 -04:00
|
|
|
pub fn catalog_id(&self, class: &NamedNode) -> Option<u64> {
|
|
|
|
|
self.store.quads_for_pattern(Some(class.into()), Some(gl::CATALOG_ID), None, None)
|
|
|
|
|
.filter_map(Result::ok)
|
|
|
|
|
.map(conversion::quad_into_term)
|
|
|
|
|
.filter_map(conversion::term_into_u64)
|
|
|
|
|
.next()
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-01 17:42:23 -04:00
|
|
|
pub fn fields_for_class(&self, class: &NamedNode, language: &LanguageCondition) -> error::Result<Vec<IndexField>> {
|
|
|
|
|
let language_filter = language.to_filter_expression("label");
|
|
|
|
|
let query = format!(r#"SELECT DISTINCT ?name ?label {{
|
|
|
|
|
{class} gl:associatedProperty/gl:indexedByField ?field .
|
|
|
|
|
|
|
|
|
|
?field gl:fieldName ?name ;
|
|
|
|
|
gl:fieldLabel ?label .
|
|
|
|
|
|
|
|
|
|
{language_filter}
|
|
|
|
|
}}"#);
|
|
|
|
|
let mut sparql = SparqlEvaluator::new()
|
|
|
|
|
.with_prefix("rdfs", "http://www.w3.org/2000/01/rdf-schema#")?
|
|
|
|
|
.with_prefix("gl", "https://graphofliberty.org/2026/04/ont/")?
|
|
|
|
|
.parse_query(&query)?;
|
|
|
|
|
sparql.dataset_mut().set_default_graph_as_union();
|
|
|
|
|
|
|
|
|
|
let mut results = Vec::new();
|
|
|
|
|
if let QueryResults::Solutions(solutions) = sparql.on_store(&self.store).execute()? {
|
|
|
|
|
for solution in solutions.filter_map(Result::ok) {
|
|
|
|
|
let name = solution.get("name").and_then(conversion::term_as_str);
|
|
|
|
|
let label = solution.get("label").and_then(conversion::term_as_str);
|
|
|
|
|
|
|
|
|
|
if let Some(name) = name && let Some(label) = label {
|
|
|
|
|
results.push(IndexField {
|
|
|
|
|
name: name.to_string(),
|
|
|
|
|
label: label.to_string(),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(results)
|
|
|
|
|
}
|
|
|
|
|
|
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}")))
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-30 19:25:15 -04:00
|
|
|
pub fn info(&self, iri: &NamedNode, language: &LanguageCondition) -> LabeledIri {
|
2026-06-29 23:10:22 -04:00
|
|
|
let label = self.store.quads_for_pattern(Some(iri.as_ref().into()), Some(rdfs::LABEL), None, None)
|
|
|
|
|
.filter_map(Result::ok)
|
2026-07-07 22:06:39 -04:00
|
|
|
.map(quad_into_term)
|
2026-07-01 17:42:23 -04:00
|
|
|
.filter(|term| language.primary_matches_term(term))
|
2026-07-07 22:06:39 -04:00
|
|
|
.filter_map(term_into_string)
|
2026-07-01 17:42:23 -04:00
|
|
|
.next()
|
|
|
|
|
.unwrap_or_default();
|
2026-06-29 23:10:22 -04:00
|
|
|
|
|
|
|
|
LabeledIri {
|
|
|
|
|
iri: iri.clone(),
|
|
|
|
|
label,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-07 22:06:39 -04:00
|
|
|
pub fn searchable_classes(&self, language: &LanguageCondition) -> impl Iterator<Item = LabeledIri> {
|
|
|
|
|
self.store.quads_for_pattern(None, Some(rdf::TYPE), Some(gl::SEARCHABLE.into()), None)
|
2026-07-01 17:42:23 -04:00
|
|
|
.filter_map(Result::ok)
|
|
|
|
|
.filter_map(|quad| {
|
|
|
|
|
let label = self.store.quads_for_pattern(Some(quad.subject.as_ref().into()), Some(rdfs::LABEL), None, None)
|
|
|
|
|
.filter_map(Result::ok)
|
2026-07-07 22:06:39 -04:00
|
|
|
.map(quad_into_term)
|
2026-07-01 17:42:23 -04:00
|
|
|
.filter(|term| language.primary_matches_term(term))
|
2026-07-07 22:06:39 -04:00
|
|
|
.filter_map(term_into_string)
|
2026-07-01 17:42:23 -04:00
|
|
|
.next();
|
|
|
|
|
if let Some(label) = label && let NamedOrBlankNode::NamedNode(subject) = quad.subject {
|
|
|
|
|
Some(LabeledIri {
|
|
|
|
|
iri: subject,
|
|
|
|
|
label,
|
|
|
|
|
})
|
|
|
|
|
} else { None }
|
2026-06-17 20:50:26 -04:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-07 22:06:39 -04:00
|
|
|
pub fn subclasses_of(&self, class: &NamedNode) -> BTreeSet<NamedNode> {
|
|
|
|
|
self.store.quads_for_pattern(None, Some(rdfs::SUB_CLASS_OF), Some(class.into()), None)
|
|
|
|
|
.filter_map(Result::ok)
|
|
|
|
|
.filter_map(|quad| if let NamedOrBlankNode::NamedNode(subject) = quad.subject {
|
|
|
|
|
Some(subject)
|
|
|
|
|
} else { None })
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-23 21:55:52 -04:00
|
|
|
pub fn datatypes(&self) -> impl Iterator<Item = NamedNode> {
|
|
|
|
|
self.store
|
2026-06-08 19:33:49 -04:00
|
|
|
.quads_for_pattern(
|
|
|
|
|
None,
|
|
|
|
|
Some(rdf::TYPE),
|
|
|
|
|
Some(TermRef::NamedNode(rdfs::DATATYPE)),
|
|
|
|
|
None,
|
|
|
|
|
)
|
2026-06-23 21:55:52 -04:00
|
|
|
.filter_map(Result::ok)
|
2026-06-08 19:33:49 -04:00
|
|
|
.filter_map(|quad| match quad.subject {
|
2026-06-23 21:55:52 -04:00
|
|
|
NamedOrBlankNode::NamedNode(subject) => Some(subject),
|
2026-06-08 19:33:49 -04:00
|
|
|
_ => None,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 14:16:25 -04:00
|
|
|
pub fn is_read_only<'a>(&self, triple: impl Into<TripleRef<'a>>) -> bool {
|
2026-06-23 21:55:52 -04:00
|
|
|
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
|
2026-06-10 14:16:25 -04:00
|
|
|
}
|
|
|
|
|
|
2026-07-07 22:06:39 -04:00
|
|
|
pub fn exclude_read_only(&self) -> impl Fn(TripleRef<'_>) -> bool + 'static {
|
|
|
|
|
let true_term = TermRef::Literal(LiteralRef::new_typed_literal("true", xsd::BOOLEAN));
|
|
|
|
|
let dataset = self.store
|
|
|
|
|
.quads_for_pattern(None, Some(gl::READ_ONLY), Some(true_term), None)
|
|
|
|
|
.filter_map(Result::ok)
|
|
|
|
|
.collect::<Dataset>();
|
|
|
|
|
move |triple| {
|
|
|
|
|
dataset.quads_for_subject(triple.subject).count() == 0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-08 19:33:49 -04:00
|
|
|
pub fn template_triples<'a>(
|
|
|
|
|
&'a self,
|
|
|
|
|
class: NamedNodeRef<'a>,
|
|
|
|
|
subject: NamedNodeRef<'_>,
|
|
|
|
|
) -> Option<impl Iterator<Item = Triple>> {
|
|
|
|
|
if let Some(quad) = self
|
2026-06-23 21:55:52 -04:00
|
|
|
.store
|
2026-06-08 19:33:49 -04:00
|
|
|
.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,
|
|
|
|
|
)
|
2026-06-23 21:55:52 -04:00
|
|
|
.filter_map(Result::ok)
|
2026-06-08 19:33:49 -04:00
|
|
|
.next()
|
|
|
|
|
{
|
2026-06-23 21:55:52 -04:00
|
|
|
if let Term::BlankNode(blank_node) = quad.object {
|
2026-06-08 19:33:49 -04:00
|
|
|
let iter = self
|
2026-06-23 21:55:52 -04:00
|
|
|
.store
|
|
|
|
|
.quads_for_pattern(Some(NamedOrBlankNodeRef::BlankNode(blank_node.as_ref())), None, None, None)
|
|
|
|
|
.filter_map(Result::ok)
|
2026-06-08 19:33:49 -04:00
|
|
|
.map(Triple::from)
|
|
|
|
|
.map(move |mut triple| {
|
|
|
|
|
triple.subject = NamedOrBlankNode::NamedNode(subject.into_owned());
|
|
|
|
|
triple
|
|
|
|
|
});
|
|
|
|
|
Some(iter)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-15 19:10:55 -04:00
|
|
|
}
|