This commit is contained in:
2026-08-26 19:44:18 -04:00
parent 35477a3658
commit 948b415d5a
6 changed files with 184 additions and 84 deletions
+2 -2
View File
@@ -8,14 +8,14 @@ pub enum Category {
impl Category {
pub fn to_named_node(&self) -> NamedNodeRef<'_> {
match self {
Category::AudioBook => vocab::gl::AUDIO_BOOK,
Category::AudioBook => vocab::glo::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),
vocab::glo::AUDIO_BOOK => Some(Category::AudioBook),
_ => None,
}
}
+39 -1
View File
@@ -1,7 +1,8 @@
use std::collections::HashMap;
use std::fmt::Display;
use oxigraph::model::{NamedNode, NamedNodeRef};
use oxigraph::model::{Graph, Literal, NamedNode, NamedNodeRef, NamedOrBlankNode, Term, Triple};
use oxilangtag::LanguageTag;
use url::Url;
use gl_search::Schema;
use crate::language::LanguageCondition;
use crate::vocab;
@@ -45,6 +46,14 @@ impl Class {
Class::Manifestation,
];
pub const CREATABLE: &[Class] = &[
Class::Work,
Class::Person,
Class::CorporateBody,
Class::Expression,
Class::Manifestation,
];
pub fn to_named_node(&self) -> NamedNodeRef<'_> {
match self {
Class::RdfProperty => vocab::rdf::PROPERTY,
@@ -105,4 +114,33 @@ impl Class {
],
}
}
pub fn next_url(&self) -> Option<Url> {
match self {
Class::Work => Some(Url::parse("http://fedora.quill.lan/rest/#Work").unwrap()),
_ => None,
}
}
pub fn template(&self, subject: NamedNode) -> Graph {
let empty_english_string = Term::Literal(Literal::new_language_tagged_literal_unchecked("", "en"));
let empty_node = Term::NamedNode(NamedNode::new_unchecked("#"));
match self {
Class::RdfProperty => Graph::new(),
Class::RdfsClass => Graph::new(),
Class::SkosConcept => Graph::new(),
Class::Work => Graph::from_iter([
Triple::new(NamedOrBlankNode::NamedNode(subject), vocab::rdawd::TITLE_OF_WORK, empty_english_string)
]),
Class::Person => Graph::from_iter([
Triple::new(NamedOrBlankNode::NamedNode(subject.clone()), vocab::rdaao::IDENTIFIER_FOR_PERSON, empty_node),
Triple::new(NamedOrBlankNode::NamedNode(subject.clone()), vocab::rdaad::GIVEN_NAME, empty_english_string.clone()),
Triple::new(NamedOrBlankNode::NamedNode(subject), vocab::rdaad::SURNAME, empty_english_string),
]),
Class::CorporateBody => Graph::new(),
Class::Expression => Graph::new(),
Class::Manifestation => Graph::new(),
}
}
}
+11 -1
View File
@@ -1,7 +1,7 @@
pub use oxigraph::model::vocab::rdf;
pub use oxigraph::model::vocab::rdfs;
pub mod gl {
pub mod glo {
use oxigraph::model::NamedNodeRef;
pub const AUDIO_BOOK: NamedNodeRef =
@@ -9,6 +9,9 @@ pub mod gl {
pub const READ_ONLY: NamedNodeRef =
NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/readOnly");
pub const NEXT_URL: NamedNodeRef =
NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/nextUrl");
}
pub mod skos {
@@ -57,6 +60,13 @@ pub mod rdaad {
NamedNodeRef::new_unchecked("http://rdaregistry.info/Elements/a/datatype/P50032");
}
pub mod rdaao {
use oxigraph::model::NamedNodeRef;
pub const IDENTIFIER_FOR_PERSON: NamedNodeRef =
NamedNodeRef::new_unchecked("http://rdaregistry.info/Elements/a/object/P50094");
}
pub mod rdawd {
use oxigraph::model::NamedNodeRef;
+16
View File
@@ -95,6 +95,12 @@ xsd:unsignedShort rdf:type rdfs:Datatype ;
rdfs:label "derived with"@en .
### https://graphofliberty.org/2026/04/ont/nextUrl
:nextUrl rdf:type owl:ObjectProperty ;
rdfs:comment "The next URL to use when creating a new document."@en ;
rdfs:label "Next URL"@en .
#################################################################
# Data properties
#################################################################
@@ -135,6 +141,11 @@ fedora:Container rdf:type owl:NamedIndividual ;
:readOnly "true"^^xsd:boolean .
### http://fedora.info/definitions/v4/repository#RepositoryRoot
fedora:RepositoryRoot rdf:type owl:NamedIndividual ;
:readOnly "true"^^xsd:boolean .
### http://fedora.info/definitions/v4/repository#Resource
fedora:Resource rdf:type owl:NamedIndividual ;
:readOnly "true"^^xsd:boolean .
@@ -160,6 +171,11 @@ fedora:hasParent rdf:type owl:NamedIndividual ;
:readOnly "true"^^xsd:boolean .
### http://fedora.info/definitions/v4/repository#hasTransactionProvider
fedora:hasTransactionProvider rdf:type owl:NamedIndividual ;
:readOnly "true"^^xsd:boolean .
### http://fedora.info/definitions/v4/repository#lastModified
fedora:lastModified rdf:type owl:NamedIndividual ;
:readOnly "true"^^xsd:boolean .
+36 -36
View File
@@ -45,12 +45,12 @@ pub(crate) enum Message {
NavigateTo(Url),
FetchDocument(Url),
LoadDocument(RdfSource<KeyedDataset<RowState>>),
ResourceNotFound,
ShowNewDocumentButtons,
HideNewDocumentButtons,
ShowError(String),
AddRow(Option<(QuadKey, RowState)>),
DeleteRow(QuadKey),
DeleteAllRows,
OpenQueryWindow(SearchResultClickAction),
HoverRow(QuadKey),
UnhoverRow(QuadKey),
@@ -68,7 +68,7 @@ pub(crate) enum Message {
ShowOverwriteConfirmationModal,
HideOverwriteConfirmationModal,
ConfirmOverwrite,
NewDocument(NamedNode),
NewDocument(Class, Option<Url>),
NavigateToPredicate(QuadKey),
NavigateToObject(QuadKey),
NavigateBack,
@@ -123,6 +123,7 @@ pub(crate) struct Publisher {
show_overwrite_confirmation: bool,
modified: bool,
show_new_document_buttons: bool,
resource_not_found: bool,
}
impl Publisher {
@@ -180,6 +181,7 @@ impl Publisher {
show_overwrite_confirmation: false,
modified: false,
show_new_document_buttons: false,
resource_not_found: false,
},
task.map(|_| Message::ConnectToOntologyService("http://[::1]:3000".to_string())),
)
@@ -259,8 +261,7 @@ impl Publisher {
}
}
Message::NavigateTo(url) => {
task = Task::done(Message::DeleteAllRows)
.chain(Task::done(Message::ResetState))
task = Task::done(Message::ResetState)
.chain(Task::done(Message::FetchDocument(url)));
}
Message::FetchDocument(url) => {
@@ -280,7 +281,7 @@ impl Publisher {
},
Err(ldp::Error::Reqwest(err)) => {
if err.status() == Some(StatusCode::NOT_FOUND) {
Message::ShowNewDocumentButtons
Message::ResourceNotFound
} else {
Message::ShowError(err.to_string())
}
@@ -289,6 +290,10 @@ impl Publisher {
}
});
}
Message::ResourceNotFound => {
self.resource_not_found = true;
task = Task::done(Message::ShowNewDocumentButtons);
}
Message::LoadDocument(document) => {
let nodes_to_look_up = document.dataset().quads.iter().map(|(_, quad)| {
let mut nodes = vec![ quad.predicate.clone() ];
@@ -342,10 +347,6 @@ impl Publisher {
self.document.dataset_mut().remove(key);
self.modified = true;
}
Message::DeleteAllRows => {
self.document.dataset_mut().clear();
self.modified = true;
}
Message::OpenQueryWindow(action) => {
let selected_entity_class = match action {
SearchResultClickAction::Predicate(_) => Some(vocab::rdf::PROPERTY),
@@ -538,21 +539,26 @@ impl Publisher {
Message::HideNewDocumentButtons => {
self.show_new_document_buttons = false;
}
Message::NewDocument(class) => {
let url = Url::parse(self.url_input.as_str()).expect("Invalid URL");
let mut document: RdfSource<KeyedDataset<RowState>> = RdfSource::new(url);
Message::NewDocument(class, None) => {
let client = self.http_client.clone();
task = tasks::navigate_to_next_url(client, class);
}
Message::NewDocument(class, Some(url)) => {
let mut document: RdfSource<KeyedDataset<RowState>> = RdfSource::new(url.clone());
let mut quad = document.new_quad();
quad.predicate = vocab::rdf::TYPE.into_owned();
quad.object = Term::NamedNode(class);
quad.object = Term::NamedNode(class.to_named_node().into_owned());
document.dataset_mut().quads.insert(quad);
task = Task::done(Message::HideNewDocumentButtons)
.chain(Task::done(Message::URLInputChanged(url.to_string())))
.chain(Task::done(Message::LoadDocument(document)));
}
Message::ResetState => {
self.modified = false;
self.show_new_document_buttons = false;
self.show_overwrite_confirmation = false;
self.resource_not_found = false;
}
Message::NavigateToPredicate(key) => {
if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) {
@@ -811,10 +817,7 @@ impl Publisher {
.into()
}
pub(crate) fn view(&self, window: window::Id) -> Element<'_, Message> {
if let Some(search_state) = &self.search_state
&& search_state.window_id == window
{
fn view_search_window<'a>(&'a self, search_state: &'a SearchState) -> Element<'a, Message> {
let search_input = text_input("Query", &search_state.query)
.on_input(Message::QueryUpdated)
.id("query");
@@ -871,12 +874,19 @@ impl Publisher {
Some(scrollable(table(columns, &search_state.results)))
};
return column![row![search_input, type_selector], results_table].into();
column![row![search_input, type_selector], results_table].into()
}
pub(crate) fn view(&self, window: window::Id) -> Element<'_, Message> {
if let Some(search_state) = &self.search_state
&& search_state.window_id == window {
return self.view_search_window(search_state);
}
let back_button = button("\u{1f870}").on_press(Message::NavigateBack);
let forward_button = button("\u{1f872}").on_press(Message::NavigateForward);
let add_row_button = button("Add row").on_press(Message::AddRow(None));
let new_document_button = button("New Document").on_press(Message::ShowNewDocumentButtons);
let add_row_button = button("Add Row").on_press(Message::AddRow(None));
let address_input = iri_input(&self.curie_helper, "URL", None, &self.url_input)
.on_input(Message::URLInputChanged)
@@ -895,25 +905,14 @@ impl Publisher {
.collect();
let body: Element<Message> = if self.show_new_document_buttons {
let buttons = Class::ALL.iter()
let buttons = Class::CREATABLE.iter()
.map(|class| {
let node = class.to_named_node().into_owned();
let abbreviation = self.curie_helper
.abbreviate(None, node.as_str())
.unwrap_or(node.to_string());
let url = if self.resource_not_found {
Url::parse(&self.url_input).ok()
} else { None };
let description = self.resource_descriptions
.get(&node);
let button_text = if let Some(description) = description &&
let Some(label) = &description.label {
format!("{label} ({abbreviation})")
} else {
abbreviation
};
button(text(button_text))
.on_press(Message::NewDocument(node))
button(text(class.to_string()))
.on_press(Message::NewDocument(class.clone(), url))
.into()
});
@@ -1004,6 +1003,7 @@ impl Publisher {
let content = navigation_area(column![
row![
new_document_button,
add_row_button,
back_button,
forward_button,
+37 -1
View File
@@ -1,5 +1,11 @@
use iced::Task;
use oxigraph::model::{Graph, NamedNode};
use ldp::reqwest_middleware::ClientWithMiddleware;
use ldp::ResourceRequestBuilder;
use oxigraph::io::RdfFormat;
use oxigraph::model::{Dataset, Graph, NamedNode, NamedNodeRef, NamedOrBlankNode, NamedOrBlankNodeRef, TermRef};
use tracing::debug;
use url::Url;
use gl_graph::class::Class;
use gl_graph::ontology::{ConnectedOntology, OntologyBuilder, ResourceSelector};
use gl_graph::vocab;
use crate::app::Message;
@@ -48,3 +54,33 @@ pub(crate) fn run_inference(mut ontology: ConnectedOntology, graph: Graph) -> Ta
}
})
}
pub(crate) fn navigate_to_next_url(client: ClientWithMiddleware, class: Class) -> Task<Message> {
if let Some(lookup_url) = class.next_url() {
Task::future(async move {
let request = ResourceRequestBuilder::with_client_and_url(client, lookup_url.clone())
.accept_rdf_format(RdfFormat::Turtle)
.follow_described_by(true)
.build();
match request.send().await {
Ok(resource) => match resource.into_rdf_source::<Dataset>().await {
Ok(document) => {
let node = NamedNodeRef::new_unchecked(lookup_url.as_str());
let result = document.dataset()
.quads_for_pattern(Some(NamedOrBlankNodeRef::NamedNode(node)), Some(vocab::glo::NEXT_URL), None, None)
.next();
if let Some(next_url_quad) = result {
if let TermRef::NamedNode(next_url) = next_url_quad.object {
let url = Url::parse(next_url.as_str()).unwrap();
Message::NewDocument(class, Some(url))
} else { Message::None }
} else { Message::None }
}
_ => Message::None
}
_ => Message::None
}
})
} else { Task::none() }
}