.
This commit is contained in:
Generated
+1
@@ -1636,6 +1636,7 @@ dependencies = [
|
|||||||
"spargebra",
|
"spargebra",
|
||||||
"thiserror 2.0.20",
|
"thiserror 2.0.20",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -13,3 +13,4 @@ rayon.workspace = true
|
|||||||
spargebra.workspace = true
|
spargebra.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
|
url = "2.5.8"
|
||||||
+9
-1
@@ -1,5 +1,6 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
const ONTOLOGY_PREFIX: &str = "https://graphofliberty.org/2026/04/ont/";
|
const ONTOLOGY_PREFIX: &str = "https://graphofliberty.org/2026/04/ont/";
|
||||||
|
|
||||||
@@ -19,7 +20,9 @@ pub static PREFIXES: LazyLock<BTreeMap<String, String>> = LazyLock::new(|| {
|
|||||||
),
|
),
|
||||||
("prov", "http://www.w3.org/ns/prov#"),
|
("prov", "http://www.w3.org/ns/prov#"),
|
||||||
("locid", "http://id.loc.gov/vocabulary/identifiers/"),
|
("locid", "http://id.loc.gov/vocabulary/identifiers/"),
|
||||||
|
("locname", "http://id.loc.gov/authorities/names/"),
|
||||||
("loclang", "http://id.loc.gov/vocabulary/languages/"),
|
("loclang", "http://id.loc.gov/vocabulary/languages/"),
|
||||||
|
("viaf", "http://viaf.org/viaf/"),
|
||||||
("premis", "http://www.loc.gov/premis/rdf/v1#"),
|
("premis", "http://www.loc.gov/premis/rdf/v1#"),
|
||||||
("premis3", "http://www.loc.gov/premis/rdf/v3/"),
|
("premis3", "http://www.loc.gov/premis/rdf/v3/"),
|
||||||
("dcterms", "http://purl.org/dc/terms/"),
|
("dcterms", "http://purl.org/dc/terms/"),
|
||||||
@@ -69,7 +72,12 @@ impl CurieHelper {
|
|||||||
return Some(format!("{name}:{local_name}"));
|
return Some(format!("{name}:{local_name}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
|
||||||
|
if let Some(base) = base &&
|
||||||
|
let Some(parsed_iri) = Url::parse(iri).ok() &&
|
||||||
|
let Some(parsed_base) = Url::parse(base).ok() {
|
||||||
|
parsed_base.make_relative(&parsed_iri)
|
||||||
|
} else { None }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn expand(&self, base: Option<&str>, abbreviated_iri: &str) -> Option<String> {
|
pub fn expand(&self, base: Option<&str>, abbreviated_iri: &str) -> Option<String> {
|
||||||
|
|||||||
@@ -118,15 +118,17 @@ impl<'a> Indexer<'a> {
|
|||||||
results
|
results
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ontology(&self, entities: HashMap<NamedNode, (NamedNode, ResourceDescription)>) -> Vec<HashMap<Field, OwnedValue>> {
|
pub fn ontology(&self, entities: HashMap<NamedNode, ResourceDescription>) -> Vec<HashMap<Field, OwnedValue>> {
|
||||||
let label_field = Schema::field("label", self.language.primary_language());
|
let label_field = Schema::field("label", self.language.primary_language());
|
||||||
let definition_field = Schema::field("definition", self.language.primary_language());
|
let definition_field = Schema::field("definition", self.language.primary_language());
|
||||||
|
|
||||||
entities.iter()
|
entities.iter()
|
||||||
.map(|(subject, (class, description))| {
|
.map(|(subject, description)| {
|
||||||
let mut document = HashMap::with_capacity(4);
|
let mut document = HashMap::with_capacity(4);
|
||||||
|
|
||||||
let discriminant = Class::try_from_named_node(class)
|
let discriminant = description.class
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|node| Class::try_from_named_node(node.as_ref()))
|
||||||
.map(|doc_type| doc_type as u64)
|
.map(|doc_type| doc_type as u64)
|
||||||
.map(OwnedValue::U64)
|
.map(OwnedValue::U64)
|
||||||
.unwrap_or(OwnedValue::Null);
|
.unwrap_or(OwnedValue::Null);
|
||||||
|
|||||||
+46
-119
@@ -5,18 +5,24 @@ use oxigraph::io::{RdfFormat, RdfParser, RdfSerializer};
|
|||||||
use oxigraph::model::{Graph, NamedNode, NamedNodeRef, Triple};
|
use oxigraph::model::{Graph, NamedNode, NamedNodeRef, Triple};
|
||||||
use oxigraph::sparql::results::{QueryResultsFormat, QueryResultsParser, SliceQueryResultsParserOutput};
|
use oxigraph::sparql::results::{QueryResultsFormat, QueryResultsParser, SliceQueryResultsParserOutput};
|
||||||
use spargebra::algebra::GraphPattern;
|
use spargebra::algebra::GraphPattern;
|
||||||
use spargebra::term::{GroundTerm, Variable};
|
use spargebra::term::{GroundTerm, NamedNodePattern, TermPattern, TriplePattern, Variable};
|
||||||
use gl_inference::proto::ontology_client::OntologyClient;
|
use gl_inference::proto::ontology_client::OntologyClient;
|
||||||
use gl_inference::proto::OntologyQueryRequest;
|
use gl_inference::proto::OntologyQueryRequest;
|
||||||
use gl_inference::tonic::transport::{Channel, Endpoint};
|
use gl_inference::tonic::transport::{Channel, Endpoint};
|
||||||
use crate::helpers::{term_as_str, term_to_named_node};
|
use crate::helpers::{term_as_str, term_to_named_node};
|
||||||
use crate::language::LanguageCondition;
|
use crate::language::LanguageCondition;
|
||||||
use crate::vocab::{rdf, rdfs};
|
use crate::vocab;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct ResourceDescription {
|
pub struct ResourceDescription {
|
||||||
pub label: Option<String>,
|
pub label: Option<String>,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
|
pub class: Option<NamedNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ResourceSelector {
|
||||||
|
Classes,
|
||||||
|
Subjects,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||||
@@ -127,8 +133,8 @@ SELECT ?subject ?class WHERE {{
|
|||||||
if let Some(subject) = subject &&
|
if let Some(subject) = subject &&
|
||||||
let Some(class) = class {
|
let Some(class) = class {
|
||||||
let entry = match class.as_ref() {
|
let entry = match class.as_ref() {
|
||||||
rdf::PROPERTY => ReadOnlyEntity::Property(subject),
|
vocab::rdf::PROPERTY => ReadOnlyEntity::Property(subject),
|
||||||
rdfs::CLASS => ReadOnlyEntity::Class(subject),
|
vocab::rdfs::CLASS => ReadOnlyEntity::Class(subject),
|
||||||
_ => continue,
|
_ => continue,
|
||||||
};
|
};
|
||||||
results.insert(entry);
|
results.insert(entry);
|
||||||
@@ -138,125 +144,44 @@ SELECT ?subject ?class WHERE {{
|
|||||||
Ok(results)
|
Ok(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn datatypes(&mut self) -> crate::Result<HashMap<NamedNode, ResourceDescription>> {
|
fn selector_to_sparql_pattern<'a>(selector: ResourceSelector, nodes: impl IntoIterator<Item = NamedNodeRef<'a>>) -> String {
|
||||||
let label_filter = self.language.filter("label");
|
|
||||||
let comment_filter = self.language.filter("comment");
|
|
||||||
|
|
||||||
let mut request = OntologyQueryRequest::default();
|
|
||||||
request.sparql_query = Some(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 ?subject ?label ?comment WHERE {{
|
|
||||||
OPTIONAL {{
|
|
||||||
?subject a rdfs:Datatype
|
|
||||||
}} OPTIONAL {{
|
|
||||||
?subject rdfs:label ?label
|
|
||||||
{label_filter}
|
|
||||||
}} OPTIONAL {{
|
|
||||||
?subject rdfs:comment ?comment
|
|
||||||
{comment_filter}
|
|
||||||
}}
|
|
||||||
}}"#));
|
|
||||||
|
|
||||||
let response = self.client.query(request).await?;
|
|
||||||
let parser_output = QueryResultsParser::from_format(QueryResultsFormat::Json)
|
|
||||||
.for_slice(&response.get_ref().results)?;
|
|
||||||
|
|
||||||
let mut results = HashMap::new();
|
|
||||||
if let SliceQueryResultsParserOutput::Solutions(solutions) = parser_output {
|
|
||||||
for solution in solutions.filter_map(Result::ok) {
|
|
||||||
let subject = solution
|
|
||||||
.get("subject")
|
|
||||||
.and_then(term_to_named_node)
|
|
||||||
.map(|node| node.clone());
|
|
||||||
|
|
||||||
if let Some(subject) = subject {
|
|
||||||
let label = solution
|
|
||||||
.get("label")
|
|
||||||
.and_then(term_as_str)
|
|
||||||
.map(String::from);
|
|
||||||
|
|
||||||
let description = solution
|
|
||||||
.get("description")
|
|
||||||
.and_then(term_as_str)
|
|
||||||
.map(String::from);
|
|
||||||
|
|
||||||
results.insert(subject, ResourceDescription { label, description });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(results)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn resource_description_from_subject(&mut self, subject: NamedNode) -> crate::Result<ResourceDescription> {
|
|
||||||
let label_filter = self.language.filter("label");
|
|
||||||
let comment_filter = self.language.filter("comment");
|
|
||||||
let definition_filter = self.language.filter("definition");
|
|
||||||
|
|
||||||
let mut request = OntologyQueryRequest::default();
|
|
||||||
request.sparql_query = Some(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 ?label ?description WHERE {{
|
|
||||||
OPTIONAL {{
|
|
||||||
{subject} rdfs:label ?label
|
|
||||||
{label_filter}
|
|
||||||
}} OPTIONAL {{
|
|
||||||
{subject} rdfs:comment ?comment
|
|
||||||
{comment_filter}
|
|
||||||
}} OPTIONAL {{
|
|
||||||
{subject} skos:definition ?definition
|
|
||||||
{definition_filter}
|
|
||||||
}}
|
|
||||||
BIND(COALESCE(?definition, ?comment) AS ?description)
|
|
||||||
}}"#));
|
|
||||||
|
|
||||||
let foo = request.sparql_query.clone().unwrap();
|
|
||||||
|
|
||||||
let response = self.client.query(request).await?;
|
|
||||||
let parser_output = QueryResultsParser::from_format(QueryResultsFormat::Json)
|
|
||||||
.for_slice(&response.get_ref().results)?;
|
|
||||||
|
|
||||||
if let SliceQueryResultsParserOutput::Solutions(solutions) = parser_output {
|
|
||||||
Ok(solutions.filter_map(Result::ok)
|
|
||||||
.next()
|
|
||||||
.map(|solution| {
|
|
||||||
let label = solution
|
|
||||||
.get("label")
|
|
||||||
.and_then(term_as_str)
|
|
||||||
.map(String::from);
|
|
||||||
|
|
||||||
let description = solution
|
|
||||||
.get("description")
|
|
||||||
.and_then(term_as_str)
|
|
||||||
.map(String::from);
|
|
||||||
|
|
||||||
ResourceDescription {
|
|
||||||
label,
|
|
||||||
description,
|
|
||||||
}
|
|
||||||
}).unwrap_or(ResourceDescription::default())
|
|
||||||
)
|
|
||||||
} else { unreachable!() }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn named_nodes_to_values_expression<'a>(variable: &str, nodes: impl IntoIterator<Item = NamedNodeRef<'a>>) -> String {
|
|
||||||
let bindings = nodes.into_iter()
|
let bindings = nodes.into_iter()
|
||||||
.map(|node| GroundTerm::NamedNode(node.into_owned()))
|
.map(|node| GroundTerm::NamedNode(node.into_owned()))
|
||||||
.map(Some)
|
.map(Some)
|
||||||
.map(|item| vec![item])
|
.map(|item| vec![item])
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
GraphPattern::Values {
|
let variable = match selector {
|
||||||
variables: vec![Variable::new_unchecked(variable)],
|
ResourceSelector::Classes => Variable::new_unchecked("class"),
|
||||||
|
ResourceSelector::Subjects => Variable::new_unchecked("subject"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let values_pattern = GraphPattern::Values {
|
||||||
|
variables: vec![variable.clone()],
|
||||||
bindings,
|
bindings,
|
||||||
|
};
|
||||||
|
|
||||||
|
let triple_pattern = match selector {
|
||||||
|
ResourceSelector::Classes => vec![TriplePattern {
|
||||||
|
subject: TermPattern::Variable(Variable::new_unchecked("subject")),
|
||||||
|
predicate: NamedNodePattern::NamedNode(vocab::rdf::TYPE.into_owned()),
|
||||||
|
object: TermPattern::Variable(variable),
|
||||||
|
}],
|
||||||
|
ResourceSelector::Subjects => vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let basic_pattern = GraphPattern::Bgp {
|
||||||
|
patterns: triple_pattern,
|
||||||
|
};
|
||||||
|
|
||||||
|
GraphPattern::Join {
|
||||||
|
left: Box::new(basic_pattern),
|
||||||
|
right: Box::new(values_pattern),
|
||||||
}.to_string()
|
}.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn resource_descriptions_from_classes<'a>(&mut self, classes: impl IntoIterator<Item = NamedNodeRef<'a>>) -> crate::Result<HashMap<NamedNode, (NamedNode, ResourceDescription)>> {
|
pub async fn resource_descriptions<'a>(&mut self, selector: ResourceSelector, nodes: impl IntoIterator<Item = NamedNodeRef<'a>>) -> crate::Result<HashMap<NamedNode, ResourceDescription>> {
|
||||||
let values = Self::named_nodes_to_values_expression("class", classes);
|
let pattern = Self::selector_to_sparql_pattern(selector, nodes);
|
||||||
let label_filter = self.language.filter("label");
|
let label_filter = self.language.filter("label");
|
||||||
let comment_filter = self.language.filter("comment");
|
let comment_filter = self.language.filter("comment");
|
||||||
let definition_filter = self.language.filter("definition");
|
let definition_filter = self.language.filter("definition");
|
||||||
@@ -266,9 +191,8 @@ SELECT ?label ?description WHERE {{
|
|||||||
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|
||||||
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
|
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
|
||||||
|
|
||||||
SELECT ?class ?subject ?label ?description WHERE {{
|
SELECT ?subject ?label ?description ?class WHERE {{
|
||||||
{values}
|
{pattern}
|
||||||
?subject a ?class .
|
|
||||||
OPTIONAL {{
|
OPTIONAL {{
|
||||||
?subject rdfs:label ?label .
|
?subject rdfs:label ?label .
|
||||||
{label_filter}
|
{label_filter}
|
||||||
@@ -298,8 +222,7 @@ SELECT ?class ?subject ?label ?description WHERE {{
|
|||||||
.and_then(term_to_named_node)
|
.and_then(term_to_named_node)
|
||||||
.map(|node| node.clone());
|
.map(|node| node.clone());
|
||||||
|
|
||||||
if let Some(subject) = subject &&
|
if let Some(subject) = subject {
|
||||||
let Some(class) = class {
|
|
||||||
let label = solution
|
let label = solution
|
||||||
.get("label")
|
.get("label")
|
||||||
.and_then(term_as_str)
|
.and_then(term_as_str)
|
||||||
@@ -310,7 +233,11 @@ SELECT ?class ?subject ?label ?description WHERE {{
|
|||||||
.and_then(term_as_str)
|
.and_then(term_as_str)
|
||||||
.map(String::from);
|
.map(String::from);
|
||||||
|
|
||||||
results.insert(subject, (class.clone(), ResourceDescription { label, description }));
|
results.insert(subject, ResourceDescription {
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
class: class.cloned(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-2
@@ -916,7 +916,7 @@ impl Publisher {
|
|||||||
let subject_column = table::column("Subject", |triple: TripleRef| {
|
let subject_column = table::column("Subject", |triple: TripleRef| {
|
||||||
let curie = if let NamedOrBlankNodeRef::NamedNode(subject) = triple.subject {
|
let curie = if let NamedOrBlankNodeRef::NamedNode(subject) = triple.subject {
|
||||||
Some(self.curie_helper
|
Some(self.curie_helper
|
||||||
.abbreviate(None, subject.as_str())
|
.abbreviate(Some(self.document.origin().as_str()), subject.as_str())
|
||||||
.unwrap_or_else(|| subject.as_str().to_string()))
|
.unwrap_or_else(|| subject.as_str().to_string()))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -942,7 +942,14 @@ impl Publisher {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let object_column = table::column("Object", |triple: TripleRef| {
|
let object_column = table::column("Object", |triple: TripleRef| {
|
||||||
text(triple.object.to_string())
|
let displayed_text = match triple.object {
|
||||||
|
TermRef::NamedNode(node) => {
|
||||||
|
self.curie_helper.abbreviate(Some(self.document.origin().as_str()), node.as_str())
|
||||||
|
}
|
||||||
|
_ => None
|
||||||
|
}.unwrap_or(triple.object.to_string());
|
||||||
|
|
||||||
|
text(displayed_text)
|
||||||
});
|
});
|
||||||
Some(table(
|
Some(table(
|
||||||
[subject_column, predicate_column, object_column],
|
[subject_column, predicate_column, object_column],
|
||||||
|
|||||||
+2
-2
@@ -27,7 +27,7 @@ use tracing_subscriber::{EnvFilter, fmt};
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
use gl_graph::indexer::Indexer;
|
use gl_graph::indexer::Indexer;
|
||||||
use gl_graph::{language, CurieHelper, vocab};
|
use gl_graph::{language, CurieHelper, vocab};
|
||||||
use gl_graph::ontology::OntologyBuilder;
|
use gl_graph::ontology::{OntologyBuilder, ResourceSelector};
|
||||||
use gl_inference::proto::ontology_client::OntologyClient;
|
use gl_inference::proto::ontology_client::OntologyClient;
|
||||||
use gl_inference::proto::OntologyQueryRequest;
|
use gl_inference::proto::OntologyQueryRequest;
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ fn main() -> color_eyre::Result<()> {
|
|||||||
let mut client = OntologyBuilder::from_string("http://[::1]:3000", language::ENGLISH_OR_UNTAGGED.clone())?
|
let mut client = OntologyBuilder::from_string("http://[::1]:3000", language::ENGLISH_OR_UNTAGGED.clone())?
|
||||||
.connect()
|
.connect()
|
||||||
.await?;
|
.await?;
|
||||||
let resource_descriptions = client.resource_descriptions_from_classes([
|
let resource_descriptions = client.resource_descriptions(ResourceSelector::Classes, [
|
||||||
vocab::rdf::PROPERTY,
|
vocab::rdf::PROPERTY,
|
||||||
vocab::rdfs::CLASS,
|
vocab::rdfs::CLASS,
|
||||||
vocab::skos::CONCEPT,
|
vocab::skos::CONCEPT,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use iced::Task;
|
use iced::Task;
|
||||||
use oxigraph::model::{Graph, NamedNode};
|
use oxigraph::model::{Graph, NamedNode};
|
||||||
use gl_graph::ontology::{ConnectedOntology, OntologyBuilder};
|
use gl_graph::ontology::{ConnectedOntology, OntologyBuilder, ResourceSelector};
|
||||||
|
use gl_graph::vocab;
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
|
|
||||||
pub(crate) fn connect_to_ontology_service(builder: OntologyBuilder) -> Task<Message> {
|
pub(crate) fn connect_to_ontology_service(builder: OntologyBuilder) -> Task<Message> {
|
||||||
@@ -14,16 +15,19 @@ pub(crate) fn connect_to_ontology_service(builder: OntologyBuilder) -> Task<Mess
|
|||||||
|
|
||||||
pub(crate) fn lookup_resource_description(mut ontology: ConnectedOntology, subject: NamedNode) -> Task<Message> {
|
pub(crate) fn lookup_resource_description(mut ontology: ConnectedOntology, subject: NamedNode) -> Task<Message> {
|
||||||
let subject_clone = subject.clone();
|
let subject_clone = subject.clone();
|
||||||
Task::perform(async move { ontology.resource_description_from_subject(subject_clone).await }, |result| {
|
Task::perform(async move { ontology.resource_descriptions(ResourceSelector::Subjects, vec![subject_clone.as_ref()]).await }, |result| {
|
||||||
match result {
|
match result {
|
||||||
Ok(description) => Message::CacheResourceDescription(subject, description),
|
Ok(descriptions) => {
|
||||||
|
let description = descriptions[&subject].clone();
|
||||||
|
Message::CacheResourceDescription(subject, description)
|
||||||
|
},
|
||||||
Err(err) => Message::ShowError(err.to_string()),
|
Err(err) => Message::ShowError(err.to_string()),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn list_datatypes(mut ontology: ConnectedOntology) -> Task<Message> {
|
pub(crate) fn list_datatypes(mut ontology: ConnectedOntology) -> Task<Message> {
|
||||||
Task::perform(async move { ontology.datatypes().await }, |result| {
|
Task::perform(async move { ontology.resource_descriptions(ResourceSelector::Classes, vec![vocab::rdfs::DATATYPE]).await }, |result| {
|
||||||
match result {
|
match result {
|
||||||
Ok(datatypes) => Message::CacheDatatypes(datatypes),
|
Ok(datatypes) => Message::CacheDatatypes(datatypes),
|
||||||
Err(err) => Message::ShowError(err.to_string()),
|
Err(err) => Message::ShowError(err.to_string()),
|
||||||
|
|||||||
Reference in New Issue
Block a user