diff --git a/Cargo.lock b/Cargo.lock index e6d6ed7..dab2846 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -913,6 +913,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + [[package]] name = "bytemuck" version = "1.25.0" @@ -4906,6 +4912,17 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "papergrid" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6978128c8b51d8f4080631ceb2302ab51e32cc6e8615f735ee2f83fd269ae3f1" +dependencies = [ + "bytecount", + "fnv", + "unicode-width", +] + [[package]] name = "parking" version = "2.2.1" @@ -6796,6 +6813,30 @@ dependencies = [ "libc", ] +[[package]] +name = "tabled" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e39a2ee1fbcd360805a771e1b300f78cc88fec7b8d3e2f71cd37bbf23e725c7d" +dependencies = [ + "papergrid", + "tabled_derive", + "testing_table", +] + +[[package]] +name = "tabled_derive" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea5d1b13ca6cff1f9231ffd62f15eefd72543dab5e468735f1a456728a02846" +dependencies = [ + "heck", + "proc-macro-error2", + "proc-macro2 1.0.106", + "quote 1.0.44", + "syn 2.0.117", +] + [[package]] name = "tachys" version = "0.2.13" @@ -6862,6 +6903,15 @@ dependencies = [ "syn 0.15.44", ] +[[package]] +name = "testing_table" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f8daae29995a24f65619e19d8d31dea5b389f3d853d8bf297bbf607cd0014cc" +dependencies = [ + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -7521,9 +7571,11 @@ dependencies = [ "env_logger", "futures", "horned-owl", + "leptos", "log", "rdf-fusion", "serde", + "strum 0.27.2", "tokio", "tokio-stream", "vowlr-util", @@ -7542,8 +7594,12 @@ dependencies = [ name = "vowlr-util" version = "0.1.0" dependencies = [ + "leptos", + "log", "rkyv", "serde", + "strum 0.27.2", + "tabled", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2e77a81..59d960c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,10 +45,12 @@ "webgl", ], git="https://github.com/WebVOWL/WasmGrapher"} horned-owl={git="https://github.com/phillord/horned-owl", rev="c4f7835ecae70b5e7281dc23b9b7fc6daa497f1d"} + leptos={version="0.8.16", features=["nightly", "multipart", "rkyv"]} log="0.4" rdf-fusion="0.1.0" rkyv="0.8.12" smallvec={version="1.15.1", features=["union"]} + strum={version="0.27", features=["derive"]} tokio="1.48.0" [dependencies] @@ -67,16 +69,16 @@ futures={workspace=true} getrandom={version="0.3", features=["wasm_js"]} gloo-timers="0.2" - grapher={workspace=true} + grapher.workspace=true http={version="1.3.1", optional=true} - leptos={version="0.8.16", features=["nightly", "multipart", "rkyv"]} + leptos.workspace=true leptos_actix={version="0.8.7", optional=true} leptos_meta={version="0.8.6"} leptos_router={version="0.8.12", features=["nightly"]} - log={workspace=true} + log.workspace=true rayon="1.10" reqwest={version="0.12.24", optional=true, features=["json", "stream"]} - rkyv={workspace=true} + rkyv.workspace=true tokio={workspace=true, optional=true} vowlr-database={path="crates/database", optional=true} vowlr-parser={path="crates/parser", optional=true} @@ -113,6 +115,7 @@ "HtmlInputElement", "HtmlCollection", "Blob", + "BlobPropertyBag", ] @@ -127,6 +130,7 @@ "dep:vowlr-database", "dep:vowlr-parser", "leptos-use/actix", + "vowlr-util/server", ] ssr=[ "dep:actix-files", diff --git a/crates/database/src/errors.rs b/crates/database/src/errors.rs new file mode 100644 index 0000000..61ce678 --- /dev/null +++ b/crates/database/src/errors.rs @@ -0,0 +1,73 @@ +use std::panic::Location; + +use crate::serializers::Triple; +use oxrdf::{BlankNodeIdParseError, IriParseError}; +use vowlr_util::prelude::{ErrorRecord, ErrorSeverity, ErrorType}; + +#[derive(Debug)] +pub enum SerializationErrorKind { + /// An error raised when the object of a triple is required but missing. + MissingObject(Triple, String), + /// An error raised when the subject of a triple is required but missing. + MissingSubject(Triple, String), + /// An error raised when the serializer encountered an unrecoverable problem. + SerializationFailed(Triple, String), + /// An error raised during Iri or IriRef validation. + IriParseError(String, IriParseError), + /// An error raised during BlankNode IDs validation + BlankNodeParseError(String, BlankNodeIdParseError), +} + +#[derive(Debug)] +pub struct SerializationError { + /// The contained error type. + inner: SerializationErrorKind, + /// The error's location in the source code. + location: &'static Location<'static>, +} +impl std::fmt::Display for SerializationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.inner) + } +} + +impl From for SerializationError { + #[track_caller] + fn from(error: SerializationErrorKind) -> Self { + SerializationError { + inner: error, + location: Location::caller(), + } + } +} + +impl From for ErrorRecord { + fn from(value: SerializationError) -> Self { + let (message, severity) = match value.inner { + SerializationErrorKind::MissingObject(triple, e) => { + (format!("{e}:\n{triple}"), ErrorSeverity::Warning) + } + SerializationErrorKind::MissingSubject(triple, e) => { + (format!("{e}:\n{triple}"), ErrorSeverity::Warning) + } + SerializationErrorKind::SerializationFailed(triple, e) => { + (format!("{e}:\n{triple}"), ErrorSeverity::Critical) + } + SerializationErrorKind::IriParseError(iri, iri_parse_error) => ( + format!("{iri_parse_error} (IRI: {iri})"), + ErrorSeverity::Error, + ), + SerializationErrorKind::BlankNodeParseError(id, blank_node_id_parse_error) => ( + format!("{blank_node_id_parse_error} (ID: {id})"), + ErrorSeverity::Error, + ), + }; + ErrorRecord::new( + severity, + ErrorType::Serializer, + message, + #[cfg(debug_assertions)] + Some(value.location.to_string()), + ) + } +} diff --git a/crates/database/src/lib.rs b/crates/database/src/lib.rs index b2b1b9c..3e7f2a9 100644 --- a/crates/database/src/lib.rs +++ b/crates/database/src/lib.rs @@ -1,11 +1,4 @@ -use std::fmt::{Display, Formatter}; - -use grapher::prelude::{ElementType, OwlEdge, OwlType, RdfEdge, RdfType}; -use oxrdf::{BlankNodeIdParseError, IriParseError}; -use vowlr_parser::errors::VOWLRStoreError; - -use crate::serializers::Triple; - +mod errors; pub mod serializers; pub mod store; pub mod vocab; @@ -16,93 +9,3 @@ pub mod prelude { pub use crate::store::VOWLRStore; } - -pub const SYMMETRIC_EDGE_TYPES: [ElementType; 1] = - [ElementType::Owl(OwlType::Edge(OwlEdge::DisjointWith))]; - -pub const PROPERTY_EDGE_TYPES: [ElementType; 7] = [ - ElementType::Owl(OwlType::Edge(OwlEdge::ObjectProperty)), - ElementType::Owl(OwlType::Edge(OwlEdge::DatatypeProperty)), - ElementType::Owl(OwlType::Edge(OwlEdge::DeprecatedProperty)), - ElementType::Owl(OwlType::Edge(OwlEdge::ExternalProperty)), - ElementType::Owl(OwlType::Edge(OwlEdge::ValuesFrom)), - ElementType::Owl(OwlType::Edge(OwlEdge::InverseOf)), - ElementType::Rdf(RdfType::Edge(RdfEdge::RdfProperty)), -]; - -pub trait SerializationErrorExt { - fn triple(&self) -> Option<&Triple>; -} - -macro_rules! ser_err { - ($variant:ident($triple:expr, $msg:expr)) => { - $crate::SerializationErrorKind::$variant(($triple).map(Box::new), $msg) - }; -} -pub(crate) use ser_err; - -#[derive(Debug)] -pub enum SerializationErrorKind { - MissingObject(Option>, String), - MissingSubject(Option>, String), - SerializationFailed(Option>, String), - IriParseError(Option>, Box), - BlankNodeParseError(Option>, Box), -} -impl SerializationErrorExt for SerializationErrorKind { - fn triple(&self) -> Option<&Triple> { - match &self { - SerializationErrorKind::MissingObject(triple, _) - | SerializationErrorKind::MissingSubject(triple, _) - | SerializationErrorKind::SerializationFailed(triple, _) - | SerializationErrorKind::IriParseError(triple, _) - | SerializationErrorKind::BlankNodeParseError(triple, _) => { - triple.as_ref().map(|t| &**t) - } - } - } -} - -#[derive(Debug)] -pub struct SerializationError { - inner: SerializationErrorKind, -} -impl Display for SerializationError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "SerializationError: {:?}", self.inner) - } -} - -impl SerializationErrorExt for SerializationError { - fn triple(&self) -> Option<&Triple> { - self.inner.triple() - } -} - -impl From for SerializationError { - fn from(error: SerializationErrorKind) -> Self { - SerializationError { inner: error } - } -} - -impl From for SerializationError { - fn from(error: IriParseError) -> Self { - SerializationError { - inner: SerializationErrorKind::IriParseError(None, Box::new(error)), - } - } -} - -impl From for VOWLRStoreError { - fn from(error: SerializationError) -> Self { - VOWLRStoreError::from(error.to_string()) - } -} - -impl From for SerializationError { - fn from(error: BlankNodeIdParseError) -> Self { - SerializationError { - inner: SerializationErrorKind::BlankNodeParseError(None, Box::new(error)), - } - } -} diff --git a/crates/database/src/serializers.rs b/crates/database/src/serializers.rs index 1fa49c4..a2aee08 100644 --- a/crates/database/src/serializers.rs +++ b/crates/database/src/serializers.rs @@ -7,8 +7,9 @@ use std::{ use grapher::prelude::{ElementType, GraphDisplayData, OwlEdge, OwlType}; use log::error; use oxrdf::Term; +use vowlr_util::prelude::ErrorRecord; -use crate::{PROPERTY_EDGE_TYPES, SYMMETRIC_EDGE_TYPES}; +use crate::serializers::util::{PROPERTY_EDGE_TYPES, SYMMETRIC_EDGE_TYPES}; pub mod frontend; pub mod util; @@ -221,15 +222,8 @@ pub struct SerializationDataBuffer { /// can be either the subject, object or both (in this case, subject is used) /// - Value = The unresolved triples. unknown_buffer: HashMap>, - /// Stores triples that are impossible to serialize. - /// - /// This could be caused by various reasons, such as - /// visualization of the triple is not supported. - /// - /// Each element is a tuple of: - /// - 0 = The triple (if any). - /// - 1 = The reason it failed to serialize (or the message if no triple is available). - failed_buffer: Vec<(Option, String)>, + /// Stores errors encountered during serialization. + failed_buffer: Vec, /// The base IRI of the document. /// /// For instance: `http://purl.obolibrary.org/obo/envo.owl` @@ -409,17 +403,9 @@ impl Display for SerializationDataBuffer { .join("\n") )?; } - writeln!(f, "\tfailed_buffer:")?; - for (triple, reason) in self.failed_buffer.iter() { - match triple { - Some(triple) => { - writeln!(f, "\t\t{} : {}", triple, reason)?; - } - None => { - writeln!(f, "\t\tNO TRIPLE : {}", reason)?; - } - } - } + // Not needed as it's displayed by the serializer + // writeln!(f, "\tfailed_buffer:")?; + // writeln!(f, "{}", ErrorRecord::format_records(&self.failed_buffer))?; writeln!(f, "}}") } } diff --git a/crates/database/src/serializers/frontend.rs b/crates/database/src/serializers/frontend.rs index ddd4a4b..83c628a 100644 --- a/crates/database/src/serializers/frontend.rs +++ b/crates/database/src/serializers/frontend.rs @@ -6,7 +6,7 @@ use std::{ use super::{Edge, SerializationDataBuffer, Triple}; use crate::{ - SerializationError, SerializationErrorExt, ser_err, + errors::{SerializationError, SerializationErrorKind}, serializers::util::{get_reserved_iris, trim_tag_circumfix}, vocab::owl, }; @@ -25,6 +25,7 @@ use rdf_fusion::{ }, }; use vowlr_parser::errors::VOWLRStoreError; +use vowlr_util::prelude::VOWLRError; pub struct GraphDisplayDataSolutionSerializer { pub resolvable_iris: HashSet, @@ -41,13 +42,20 @@ impl GraphDisplayDataSolutionSerializer { &self, data: &mut GraphDisplayData, mut solution_stream: QuerySolutionStream, - ) -> Result<(), VOWLRStoreError> { + ) -> Result<(), VOWLRError> { let mut count: u32 = 0; info!("Serializing query solution stream..."); let start_time = Instant::now(); let mut data_buffer = SerializationDataBuffer::new(); - while let Some(solution) = solution_stream.next().await { - let solution = solution?; + while let Some(maybe_solution) = solution_stream.next().await { + let solution = match maybe_solution { + Ok(solution) => solution, + Err(e) => { + let a: VOWLRStoreError = e.into(); + data_buffer.failed_buffer.push(a.into()); + continue; + } + }; let Some(id_term) = solution.get("id") else { continue; }; @@ -62,17 +70,18 @@ impl GraphDisplayDataSolutionSerializer { element_type: node_type_term.to_owned(), target: solution.get("target").map(|term| term.to_owned()), }; - match self.write_node_triple(&mut data_buffer, triple) { - Ok(_) => (), - Err(e) => { - data_buffer - .failed_buffer - .push((e.inner.triple().cloned(), e.to_string())); - } - } + + self.write_node_triple(&mut data_buffer, triple) + .or_else(|e| { + data_buffer.failed_buffer.push(e.into()); + Ok::<(), VOWLRError>(()) + })?; count += 1; } - self.check_all_unknowns(&mut data_buffer); + self.check_all_unknowns(&mut data_buffer).or_else(|e| { + data_buffer.failed_buffer.push(e.into()); + Ok::<(), VOWLRError>(()) + })?; let finish_time = Instant::now() .checked_duration_since(start_time) @@ -91,29 +100,16 @@ impl GraphDisplayDataSolutionSerializer { data_buffer.node_element_buffer.len(), data_buffer.edge_buffer.len(), data_buffer.label_buffer.len(), + 0, data_buffer.edge_characteristics.len() + data_buffer.node_characteristics.len(), - 0 ); + debug!("{}", data_buffer); if !data_buffer.failed_buffer.is_empty() { let total = data_buffer.failed_buffer.len(); - - let mut error_log = String::from("[\n"); - for (triple, reason) in data_buffer.failed_buffer.iter() { - match triple { - Some(t) => error_log.push_str(&format!("\t{} : {}\n", t, reason)), - None => error_log.push_str(&format!("\tNO TRIPLE : {}\n", reason)), - } - } - error_log.push(']'); - error!("Failed to serialize: {}", error_log); - - return Err(VOWLRStoreError::from(format!( - "Serialization failed ({} errors): {}", - total, error_log - ))); + let err: VOWLRError = take(&mut data_buffer.failed_buffer).into(); + error!("Failed to serialize {} triples:\n{}", total, err); + return Err(err); } - - debug!("{}", data_buffer); *data = data_buffer.into(); debug!("{}", data); Ok(()) @@ -270,6 +266,10 @@ impl GraphDisplayDataSolutionSerializer { .insert(edge); } + #[expect( + clippy::result_large_err, + reason = "fixed when serializer is refactored to use pointers instead of values" + )] pub fn redirect_iri( &self, data_buffer: &mut SerializationDataBuffer, @@ -284,6 +284,10 @@ impl GraphDisplayDataSolutionSerializer { Ok(()) } + #[expect( + clippy::result_large_err, + reason = "fixed when serializer is refactored to use pointers instead of values" + )] pub fn check_unknown_buffer( &self, data_buffer: &mut SerializationDataBuffer, @@ -298,6 +302,10 @@ impl GraphDisplayDataSolutionSerializer { Ok(()) } + #[expect( + clippy::result_large_err, + reason = "fixed when serializer is refactored to use pointers instead of values" + )] fn insert_node( &self, data_buffer: &mut SerializationDataBuffer, @@ -402,6 +410,10 @@ impl GraphDisplayDataSolutionSerializer { } } + #[expect( + clippy::result_large_err, + reason = "fixed when serializer is refactored to use pointers instead of values" + )] fn merge_nodes( &self, data_buffer: &mut SerializationDataBuffer, @@ -487,31 +499,54 @@ impl GraphDisplayDataSolutionSerializer { } } - fn create_node( + #[expect( + clippy::result_large_err, + reason = "fixed when serializer is refactored to use pointers instead of values" + )] + fn create_named_node(&self, iri: String) -> Result { + Ok(NamedNode::new(&iri).map_err(|e| SerializationErrorKind::IriParseError(iri, e))?) + } + + #[expect( + clippy::result_large_err, + reason = "fixed when serializer is refactored to use pointers instead of values" + )] + fn create_blank_node(&self, id: String) -> Result { + Ok(BlankNode::new(&id).map_err(|e| SerializationErrorKind::BlankNodeParseError(id, e))?) + } + + #[expect( + clippy::result_large_err, + reason = "fixed when serializer is refactored to use pointers instead of values" + )] + fn create_triple( &self, id: String, - node_type: NamedNode, + element_type: NamedNode, object_iri: Option, ) -> Result { let subject = match NamedNode::new(id.clone()) { Ok(node) => Term::NamedNode(node), - Err(_) => Term::BlankNode(BlankNode::new(id)?), + Err(_) => Term::BlankNode(self.create_blank_node(id)?), }; let object = match object_iri { - Some(iri) => { - let obj = NamedNode::new(iri)?; - Some(Term::NamedNode(obj)) - } + Some(iri) => Some(Term::NamedNode(self.create_named_node(iri)?)), None => None, }; - let t = Triple::new(subject, Term::NamedNode(node_type), object); + let t = Triple::new(subject, Term::NamedNode(element_type), object); debug!("Created new triple: {}", t); Ok(t) } - - fn check_all_unknowns(&self, data_buffer: &mut SerializationDataBuffer) { + #[expect( + clippy::result_large_err, + reason = "fixed when serializer is refactored to use pointers instead of values" + )] + fn check_all_unknowns( + &self, + data_buffer: &mut SerializationDataBuffer, + ) -> Result<(), SerializationError> { info!("Second pass: Resolving all possible unknowns"); let unknown_nodes = take(&mut data_buffer.unknown_buffer); @@ -524,56 +559,46 @@ impl GraphDisplayDataSolutionSerializer { // dummy triple, only subject matters. let external_triple = Triple::new( term, - Term::BlankNode(BlankNode::new("_:external_class").unwrap()), + Term::BlankNode(self.create_blank_node("_:external_class".to_string())?), None, ); - match self.insert_node( + + self.insert_node( data_buffer, &external_triple, ElementType::Owl(OwlType::Node(OwlNode::ExternalClass)), - ) { - Ok(_) => (), - Err(e) => { - data_buffer - .failed_buffer - .push((e.inner.triple().cloned(), e.to_string())); - } - } + )?; } for triple in triples { - match self.write_node_triple(data_buffer, triple) { - Ok(_) => (), - Err(e) => { - data_buffer - .failed_buffer - .push((e.inner.triple().cloned(), e.to_string())); - } - } + self.write_node_triple(data_buffer, triple)?; } } + Ok(()) } + #[expect( + clippy::result_large_err, + reason = "fixed when serializer is refactored to use pointers instead of values" + )] /// Serialize a triple to `data_buffer`. fn write_node_triple( &self, data_buffer: &mut SerializationDataBuffer, triple: Triple, ) -> Result<(), SerializationError> { - // TODO: Collect errors and show to frontend debug!("{}", triple); match &triple.element_type { Term::BlankNode(bnode) => { // The query must never put blank nodes in the ?nodeType variable let msg = format!("Illegal blank node during serialization: '{bnode}'"); - return Err(ser_err!(SerializationFailed(Some(triple), msg)).into()); + return Err(SerializationErrorKind::SerializationFailed(triple, msg).into()); } Term::Literal(literal) => { - // NOTE: Any string literal goes here, e.g. 'EquivalentClass'. - // That is, every BIND("someString" AS ?nodeType) + // NOTE: Any string literal goes here, e.g, every BIND("someString" AS ?nodeType) let value = literal.value(); match value { "blanknode" => { - info!("Visualizing blank node: {}", triple.id); + debug!("Visualizing blank node: {}", triple.id); self.insert_node( data_buffer, &triple, @@ -635,10 +660,11 @@ impl GraphDisplayDataSolutionSerializer { )?; } rdfs::DOMAIN => { - error!( - "sparql query should not have rdfs:domain triples: {}", - triple - ); + return Err(SerializationErrorKind::SerializationFailed( + triple, + "SPARQL query should not have rdfs:domain triples".to_string(), + ) + .into()); } // rdfs::IS_DEFINED_BY => {} @@ -653,10 +679,11 @@ impl GraphDisplayDataSolutionSerializer { } // rdfs::MEMBER => {} rdfs::RANGE => { - error!( - "sparql query should not have rdfs:range triples: {}", - triple - ); + return Err(SerializationErrorKind::SerializationFailed( + triple, + "SPARQL query should not have rdfs:range triples".to_string(), + ) + .into()); } rdfs::RESOURCE => { self.insert_node( @@ -739,7 +766,6 @@ impl GraphDisplayDataSolutionSerializer { &triple, e, ); - return Ok(()); } //TODO: OWL1 (deprecated in OWL2, replaced by rdfs:datatype) @@ -789,17 +815,17 @@ impl GraphDisplayDataSolutionSerializer { let (index_s, index_o) = self.resolve_so(data_buffer, &triple); match (index_s, index_o) { (Some(index_s), Some(index_o)) => { - match self.merge_nodes(data_buffer, &index_o, &index_s) { - Ok(_) => (), - Err(e) => { - data_buffer - .failed_buffer - .push((e.inner.triple().cloned(), e.to_string())); - } - } - - // SAFETY: If index_s is Some it exists in node_element_buffer. - if data_buffer.node_element_buffer[&index_s] + self.merge_nodes(data_buffer, &index_o, &index_s)?; + + let index_s_element = data_buffer + .node_element_buffer + .get(&index_s) + .ok_or_else(|| { + let msg = "subject not present in node_element_buffer" + .to_string(); + SerializationErrorKind::SerializationFailed(triple, msg) + })?; + if *index_s_element != ElementType::Owl(OwlType::Node(OwlNode::AnonymousClass)) { self.upgrade_node_type( @@ -822,7 +848,10 @@ impl GraphDisplayDataSolutionSerializer { self.add_to_unknown_buffer(data_buffer, target, triple.clone()) } None => { - data_buffer.failed_buffer.push((Some(triple), "Failed to merge object of equivalence relation into subject: object not found".to_string())); + let msg = "Failed to merge object of equivalence relation into subject: object not found".to_string(); + return Err( + SerializationErrorKind::MissingObject(triple, msg).into() + ); } }, (None, Some(index_o)) => { @@ -977,10 +1006,13 @@ impl GraphDisplayDataSolutionSerializer { // owl::VERSION_INFO => {} // owl::VERSION_IRI => {} // owl::WITH_RESTRICTIONS => {} - _ => match triple.target.clone() { - Some(target) => { - let (node_triple, edge_triple): (Option>, Option) = - match ( + _ => { + match triple.target.clone() { + Some(target) => { + let (node_triple, edge_triple): ( + Option>, + Option, + ) = match ( self.resolve(data_buffer, triple.id.clone()), self.resolve(data_buffer, triple.element_type.clone()), self.resolve(data_buffer, target.clone()), @@ -990,7 +1022,7 @@ impl GraphDisplayDataSolutionSerializer { "Resolving object property: range: {}, property: {}, domain: {}", range, property, domain ); - (None, Some(triple)) + (None, Some(triple.clone())) } (Some(domain), Some(property), None) => { trace!("Missing range: {}", triple); @@ -999,7 +1031,7 @@ impl GraphDisplayDataSolutionSerializer { trim_tag_circumfix(domain.to_string().as_str()) + "_thing"; info!("Creating thing node: {}", target_iri); - Some(self.create_node( + Some(self.create_triple( target_iri.clone(), owl::THING.into(), None, @@ -1009,7 +1041,7 @@ impl GraphDisplayDataSolutionSerializer { trim_tag_circumfix(property.to_string().as_str()) + "_literal"; info!("Creating literal node: {}", target_iri); - Some(self.create_node( + Some(self.create_triple( target_iri.clone(), rdfs::LITERAL.into(), None, @@ -1021,8 +1053,8 @@ impl GraphDisplayDataSolutionSerializer { Some(node) => ( Some(vec![node.clone()]), Some(Triple { - id: triple.id, - element_type: triple.element_type, + id: triple.id.clone(), + element_type: triple.element_type.clone(), target: Some(node.id), }), ), @@ -1034,7 +1066,7 @@ impl GraphDisplayDataSolutionSerializer { self.add_to_unknown_buffer( data_buffer, target, - triple, + triple.clone(), ); (None, None) } @@ -1047,7 +1079,7 @@ impl GraphDisplayDataSolutionSerializer { trim_tag_circumfix(range.to_string().as_str()) + "_thing"; info!("Creating thing node: {}", target_iri); - Some(self.create_node( + Some(self.create_triple( target_iri.clone(), owl::THING.into(), None, @@ -1056,7 +1088,7 @@ impl GraphDisplayDataSolutionSerializer { let target_iri = trim_tag_circumfix(range.to_string().as_str()) + "_literal"; - Some(self.create_node( + Some(self.create_triple( target_iri.clone(), rdfs::LITERAL.into(), None, @@ -1069,8 +1101,8 @@ impl GraphDisplayDataSolutionSerializer { Some(vec![node.clone()]), Some(Triple { id: node.id, - element_type: triple.element_type, - target: triple.target, + element_type: triple.element_type.clone(), + target: triple.target.clone(), }), ), None => { @@ -1081,7 +1113,7 @@ impl GraphDisplayDataSolutionSerializer { self.add_to_unknown_buffer( data_buffer, target, - triple, + triple.clone(), ); (None, None) } @@ -1090,19 +1122,19 @@ impl GraphDisplayDataSolutionSerializer { (None, Some(property), None) => { trace!("Missing domain and range: {}", triple); if triple.element_type == owl::DATATYPE_PROPERTY.into() { - let local_literal = NamedNode::new( + let local_literal = self.create_named_node( property.to_string() + "_locallitral", )?; - let literal_triple = self.create_node( + let literal_triple = self.create_triple( local_literal.to_string(), rdfs::LITERAL.into(), None, )?; info!("Creating literal node: {}", local_literal); - let local_thing = NamedNode::new( + let local_thing = self.create_named_node( property.to_string() + "_localthing", )?; - let thing_triple = self.create_node( + let thing_triple = self.create_triple( local_thing.to_string(), owl::THING.into(), None, @@ -1115,15 +1147,17 @@ impl GraphDisplayDataSolutionSerializer { ]), Some(Triple { id: thing_triple.id.clone(), - element_type: triple.element_type, + element_type: triple.element_type.clone(), target: Some(literal_triple.id), }), ) } else if triple.element_type == owl::OBJECT_PROPERTY.into() { - let global_thing = - NamedNode::new(owl::THING.to_string() + "_thing")?; - let node = self.create_node( + let global_thing = self.create_named_node( + owl::THING.to_string() + "_thing", + )?; + + let node = self.create_triple( global_thing.to_string(), global_thing, None, @@ -1132,16 +1166,18 @@ impl GraphDisplayDataSolutionSerializer { Some(vec![node.clone()]), Some(Triple { id: node.id.clone(), - element_type: triple.element_type, + element_type: triple.element_type.clone(), target: Some(node.id), }), ) } else { - return Err(ser_err!(SerializationFailed( - Some(triple), - "Illegal property triple".to_string() - )) - .into()); + return Err( + SerializationErrorKind::SerializationFailed( + triple, + "Illegal property triple".to_string(), + ) + .into(), + ); } } @@ -1153,7 +1189,7 @@ impl GraphDisplayDataSolutionSerializer { self.add_to_unknown_buffer( data_buffer, triple.element_type.clone(), - triple, + triple.clone(), ); (None, None) } @@ -1162,77 +1198,100 @@ impl GraphDisplayDataSolutionSerializer { self.add_to_unknown_buffer( data_buffer, triple.id.clone(), - triple, + triple.clone(), ); (None, None) } }; - match node_triple { - Some(node_triples) => { - for node_triple in node_triples { - if node_triple.element_type == owl::THING.into() { - self.insert_node( - data_buffer, - &node_triple, - ElementType::Owl(OwlType::Node(OwlNode::Thing)), - )?; - } else if node_triple.element_type == rdfs::LITERAL.into() { - self.insert_node( - data_buffer, - &node_triple, - ElementType::Rdfs(RdfsType::Node( - RdfsNode::Literal, - )), - )?; + match node_triple { + Some(node_triples) => { + for node_triple in node_triples { + if node_triple.element_type == owl::THING.into() { + self.insert_node( + data_buffer, + &node_triple, + ElementType::Owl(OwlType::Node(OwlNode::Thing)), + )?; + } else if node_triple.element_type + == rdfs::LITERAL.into() + { + self.insert_node( + data_buffer, + &node_triple, + ElementType::Rdfs(RdfsType::Node( + RdfsNode::Literal, + )), + )?; + } } } + None => { + return Err(SerializationErrorKind::SerializationFailed( + triple.clone(), + "Error creating node".to_string(), + ) + .into()); + } } - None => error!("Error creating node {:?}", node_triple), - } - match edge_triple { - Some(edge_triple) => { - // unwrap safe, edge_triple will always be Some if property can be resolved. - let property = data_buffer + match edge_triple { + Some(edge_triple) => { + // Dummy variable + // TODO: Refactor clones away in all of serializer + let dummy = || edge_triple.clone(); + + let property= data_buffer .edge_element_buffer - .get(&edge_triple.element_type) - .unwrap(); - let edge = self.insert_edge( - data_buffer, - &edge_triple, - *property, - data_buffer - .label_buffer - .get(&edge_triple.element_type) - .cloned(), - ); - if let Some(edge) = edge { - data_buffer.add_property_edge( - edge_triple.element_type.clone(), - edge, - ); - data_buffer.add_property_domain( - edge_triple.element_type.clone(), - edge_triple - .target - .clone() - .expect("target should be a string"), - ); - data_buffer.add_property_range( - edge_triple.element_type, - edge_triple.id, + .get(&edge_triple.element_type).ok_or_else(|| { + let msg = "Edge triple not present in edge_element_buffer".to_string(); + SerializationErrorKind::SerializationFailed(dummy(), msg)})?; + let edge = self.insert_edge( + data_buffer, + &edge_triple, + *property, + data_buffer + .label_buffer + .get(&edge_triple.element_type) + .cloned(), ); + if let Some(edge) = edge { + data_buffer.add_property_edge( + edge_triple.element_type.clone(), + edge, + ); + data_buffer.add_property_domain( + edge_triple.element_type.clone(), + edge_triple.target.clone().ok_or_else(|| { + SerializationErrorKind::SerializationFailed( + dummy(), + "target should be a string".to_string(), + ) + })?, + ); + data_buffer.add_property_range( + edge_triple.element_type, + edge_triple.id, + ); + } + } + None => { + return Err(SerializationErrorKind::SerializationFailed( + triple, + "Error creating edge".to_string(), + ) + .into()); } - } - None => { - error!("Error creating edge: "); } } + None => { + return Err(SerializationErrorKind::SerializationFailed( + triple, + "Object property triples should have a target".to_string(), + ) + .into()); + } } - None => { - error!("object property triples should have a target: {}", triple); - } - }, + } } } } @@ -1250,16 +1309,16 @@ impl GraphDisplayDataSolutionSerializer { Some(s) => match data_buffer.node_characteristics.get_mut(&s) { Some(char) => { for (k, v) in data_buffer.property_edge_map.iter() { - info!("{} -> {}", k, v); + trace!("{} -> {}", k, v); } - info!("Inserting characteristic: {} -> {}", s, arg); + debug!("Inserting characteristic: {} -> {}", s, arg); char.push(arg); } None => { for (k, v) in data_buffer.property_edge_map.iter() { - info!("{} -> {}", k, v); + trace!("{} -> {}", k, v); } - info!("Inserting characteristic: {} -> {}", s, arg); + debug!("Inserting characteristic: {} -> {}", s, arg); let e = data_buffer.property_edge_map.get(&s); match e { Some(e) => { @@ -1274,7 +1333,7 @@ impl GraphDisplayDataSolutionSerializer { } }, None => { - info!("Adding characteristic to unknown buffer: {}", triple); + debug!("Adding characteristic to unknown buffer: {}", triple); self.add_to_unknown_buffer(data_buffer, triple.id.clone(), triple); } } diff --git a/crates/database/src/serializers/util.rs b/crates/database/src/serializers/util.rs index d8f5ccc..d940a92 100644 --- a/crates/database/src/serializers/util.rs +++ b/crates/database/src/serializers/util.rs @@ -1,8 +1,22 @@ use std::collections::HashSet; use crate::vocab::owl; +use grapher::prelude::{ElementType, OwlEdge, OwlType, RdfEdge, RdfType}; use rdf_fusion::model::vocab::{rdf, rdfs}; +pub const SYMMETRIC_EDGE_TYPES: [ElementType; 1] = + [ElementType::Owl(OwlType::Edge(OwlEdge::DisjointWith))]; + +pub const PROPERTY_EDGE_TYPES: [ElementType; 7] = [ + ElementType::Owl(OwlType::Edge(OwlEdge::ObjectProperty)), + ElementType::Owl(OwlType::Edge(OwlEdge::DatatypeProperty)), + ElementType::Owl(OwlType::Edge(OwlEdge::DeprecatedProperty)), + ElementType::Owl(OwlType::Edge(OwlEdge::ExternalProperty)), + ElementType::Owl(OwlType::Edge(OwlEdge::ValuesFrom)), + ElementType::Owl(OwlType::Edge(OwlEdge::InverseOf)), + ElementType::Rdf(RdfType::Edge(RdfEdge::RdfProperty)), +]; + /// Reserved IRIs should not be overridden by e.g. "external class" ElementType. pub fn get_reserved_iris() -> HashSet { let rdf = vec![rdf::XML_LITERAL]; diff --git a/crates/database/src/store.rs b/crates/database/src/store.rs index 2923774..5d43993 100644 --- a/crates/database/src/store.rs +++ b/crates/database/src/store.rs @@ -1,5 +1,5 @@ use futures::{StreamExt, stream::BoxStream}; -use log::{info, warn}; +use log::{debug, info, warn}; use rdf_fusion::store::Store; use std::path::Path; use std::time::Duration; @@ -60,7 +60,7 @@ impl VOWLRStore { &self, resource_type: DataType, ) -> Result, VOWLRStoreError>>, VOWLRStoreError> { - info!( + debug!( "Store size before export: {}", self.session.len().await.unwrap_or(0) ); diff --git a/crates/parser/Cargo.toml b/crates/parser/Cargo.toml index b161197..5de79f0 100644 --- a/crates/parser/Cargo.toml +++ b/crates/parser/Cargo.toml @@ -14,12 +14,14 @@ [dependencies] - futures={workspace=true} - horned-owl={workspace=true} - log={workspace=true} - rdf-fusion={workspace=true} + futures.workspace=true + horned-owl.workspace=true + leptos.workspace=true + log.workspace=true + rdf-fusion.workspace=true serde="1.0" - tokio={workspace=true} + strum.workspace=true + tokio.workspace=true tokio-stream="0.1.17" vowlr-util={path="../util"} diff --git a/crates/parser/src/errors.rs b/crates/parser/src/errors.rs index 96ffc70..503b930 100644 --- a/crates/parser/src/errors.rs +++ b/crates/parser/src/errors.rs @@ -1,28 +1,42 @@ use std::{io::Error, panic::Location}; use horned_owl::error::HornedError; + use rdf_fusion::{ error::LoaderError, execution::sparql::error::QueryEvaluationError, model::{IriParseError, StorageError}, }; use tokio::task::JoinError; +use vowlr_util::prelude::{ErrorRecord, ErrorSeverity, ErrorType, VOWLRError}; #[derive(Debug)] pub enum VOWLRStoreErrorKind { - InvalidInput(String), + /// The file type is not supported by the server. + /// + /// Example: server only supports `.owl` and is given `.png` + InvalidFileType(String), + /// An error raised by Horned-OWL during parsing (of OWL files). HornedError(HornedError), + /// Generic IO error. IOError(std::io::Error), + /// An error raised while trying to parse an invalid IRI. IriParseError(IriParseError), + /// An error raised while loading a file into a Store (database). LoaderError(LoaderError), + /// A SPARQL evaluation error. QueryEvaluationError(QueryEvaluationError), + /// A Tokio task failed to execute to completion. JoinError(JoinError), + /// An error related to (database) storage operations (reads, writes...). StorageError(StorageError), } #[derive(Debug)] pub struct VOWLRStoreError { + /// The contained error type. inner: VOWLRStoreErrorKind, + /// The error's location in the source code. location: &'static Location<'static>, } @@ -35,7 +49,7 @@ impl From for VOWLRStoreError { #[track_caller] fn from(error: String) -> Self { VOWLRStoreError { - inner: VOWLRStoreErrorKind::InvalidInput(error), + inner: VOWLRStoreErrorKind::InvalidFileType(error), location: Location::caller(), } } @@ -127,7 +141,7 @@ impl std::fmt::Display for VOWLRStoreError { impl std::error::Error for VOWLRStoreError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match &self.inner { - VOWLRStoreErrorKind::InvalidInput(_) => None, + VOWLRStoreErrorKind::InvalidFileType(_) => None, VOWLRStoreErrorKind::HornedError(e) => Some(e), VOWLRStoreErrorKind::IOError(e) => Some(e), VOWLRStoreErrorKind::IriParseError(e) => Some(e), @@ -138,3 +152,46 @@ impl std::error::Error for VOWLRStoreError { } } } + +impl From for ErrorRecord { + fn from(value: VOWLRStoreError) -> Self { + let (message, error_type) = match value.inner { + VOWLRStoreErrorKind::InvalidFileType(e) => (e, ErrorType::Parser), + VOWLRStoreErrorKind::HornedError(horned_error) => { + (horned_error.to_string(), ErrorType::Parser) + } + VOWLRStoreErrorKind::IOError(error) => { + (error.to_string(), ErrorType::InternalServerError) + } + VOWLRStoreErrorKind::IriParseError(iri_parse_error) => { + (iri_parse_error.to_string(), ErrorType::Parser) + } + VOWLRStoreErrorKind::LoaderError(loader_error) => { + (loader_error.to_string(), ErrorType::Database) + } + VOWLRStoreErrorKind::QueryEvaluationError(query_evaluation_error) => { + (query_evaluation_error.to_string(), ErrorType::Database) + } + VOWLRStoreErrorKind::JoinError(join_error) => { + (join_error.to_string(), ErrorType::InternalServerError) + } + VOWLRStoreErrorKind::StorageError(storage_error) => { + (storage_error.to_string(), ErrorType::Database) + } + }; + ErrorRecord::new( + ErrorSeverity::Critical, + error_type, + message, + #[cfg(debug_assertions)] + Some(value.location.to_string()), + ) + } +} + +impl From for VOWLRError { + fn from(value: VOWLRStoreError) -> Self { + let record: ErrorRecord = value.into(); + record.into() + } +} diff --git a/crates/parser/src/parser_util.rs b/crates/parser/src/parser_util.rs index 18b9877..68fee29 100644 --- a/crates/parser/src/parser_util.rs +++ b/crates/parser/src/parser_util.rs @@ -87,7 +87,7 @@ pub async fn parse_stream_to( let mut buf = Vec::new(); let mut serializer = RdfSerializer::from_format(format_from_resource_type(&DataType::OWL).ok_or( - VOWLRStoreErrorKind::InvalidInput(format!( + VOWLRStoreErrorKind::InvalidFileType(format!( "Unsupported output type: {:?}", output_type )), @@ -123,7 +123,7 @@ pub async fn parse_stream_to( writer.flush()?; Ok(writer) } - _ => Err(VOWLRStoreError::from(VOWLRStoreErrorKind::InvalidInput( + _ => Err(VOWLRStoreError::from(VOWLRStoreErrorKind::InvalidFileType( format!("Unsupported output type: {:?}", output_type), ))), })(); @@ -143,7 +143,7 @@ pub async fn parse_stream_to( let result = async { let mut serializer = RdfSerializer::from_format(format_from_resource_type(&output_type).ok_or( - VOWLRStoreErrorKind::InvalidInput(format!( + VOWLRStoreErrorKind::InvalidFileType(format!( "Unsupported output type: {:?}", output_type )), @@ -186,7 +186,7 @@ pub fn parser_from_reader( }; let Some(format) = path_type(path) else { - return Err(VOWLRStoreErrorKind::InvalidInput(format!( + return Err(VOWLRStoreErrorKind::InvalidFileType(format!( "Unsupported format {}", path.display() )) @@ -313,14 +313,14 @@ pub fn parser_from_reader( reader.read_to_end(&mut input)?; let input = ParserInput::File(input); let format = format_from_resource_type(&f).ok_or_else(|| { - VOWLRStoreErrorKind::InvalidInput(format!("could not convert {f:?} to format")) + VOWLRStoreErrorKind::InvalidFileType(format!("could not convert {f:?} to format")) })?; Ok(PreparedParser { parser: make_parser(format), input, }) } - _ => Err(VOWLRStoreErrorKind::InvalidInput(format!( + _ => Err(VOWLRStoreErrorKind::InvalidFileType(format!( "Unsupported parser: {}", format.mime_type() ))), diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml index cdb09ca..8a50aa5 100644 --- a/crates/util/Cargo.toml +++ b/crates/util/Cargo.toml @@ -13,5 +13,12 @@ [dependencies] - rkyv={workspace=true} + leptos.workspace=true + log.workspace=true + rkyv.workspace=true serde={version="1.0", features=["derive"]} + strum.workspace=true + tabled={version="0.20", optional=true} + +[features] + server=["dep:tabled"] diff --git a/crates/util/src/datatypes.rs b/crates/util/src/datatypes.rs index a75422c..709b2ec 100644 --- a/crates/util/src/datatypes.rs +++ b/crates/util/src/datatypes.rs @@ -5,21 +5,32 @@ use serde::{Deserialize, Serialize}; /// Supported content types. #[repr(C)] -#[derive(Archive, RDeserialize, RSerialize, Deserialize, Serialize, Debug, Clone)] +#[derive( + Archive, RDeserialize, RSerialize, Deserialize, Serialize, Debug, Copy, Clone, strum::Display, +)] +#[strum(serialize_all = "UPPERCASE")] pub enum DataType { OWL, OFN, OWX, TTL, RDF, + #[strum(serialize = "N-Triples")] NTriples, + #[strum(serialize = "N-Quads")] NQuads, + #[strum(serialize = "TriG")] TriG, + #[strum(serialize = "JSON-LD")] JsonLd, N3, + #[strum(serialize = "SPARQL JSON")] SPARQLJSON, + #[strum(serialize = "SPARQL XML")] SPARQLXML, + #[strum(serialize = "SPARQL CSV")] SPARQLCSV, + #[strum(serialize = "SPARQL TSV")] SPARQLTSV, /// Fallback when type can't be determined. UNKNOWN, @@ -47,6 +58,27 @@ impl DataType { Self::UNKNOWN => "application/octet-stream", } } + + /// Returns the extension of the data. + pub fn extension(&self) -> &'static str { + match self { + DataType::OWL => "owl", + DataType::OFN => "ofn", + DataType::OWX => "owx", + DataType::TTL => "ttl", + DataType::RDF => "rdf", + DataType::NTriples => "nt", + DataType::NQuads => "nq", + DataType::TriG => "trig", + DataType::JsonLd => "jsonld", + DataType::N3 => "n3", + DataType::SPARQLJSON => "srj", + DataType::SPARQLXML => "srx", + DataType::SPARQLCSV => "src", + DataType::SPARQLTSV => "tsv", + DataType::UNKNOWN => "txt", + } + } } impl From<&Path> for DataType { diff --git a/crates/util/src/error_handler.rs b/crates/util/src/error_handler.rs new file mode 100644 index 0000000..bea35bd --- /dev/null +++ b/crates/util/src/error_handler.rs @@ -0,0 +1,322 @@ +#[cfg(not(feature = "server"))] +use std::fmt::Write; + +use leptos::{ + prelude::*, + server_fn::{Decodes, Encodes, codec::RkyvEncoding, error::IntoAppError}, + view, +}; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "server")] +use tabled::{ + Table, Tabled, + settings::{Settings, Style}, +}; + +use crate::layout::TableHTML; + +#[derive( + Debug, + Copy, + Clone, + rkyv::Archive, + rkyv::Serialize, + rkyv::Deserialize, + Serialize, + Deserialize, + strum::Display, +)] +#[strum(serialize_all = "title_case")] +pub enum ErrorSeverity { + Critical, + Error, + Warning, + Unset, +} + +#[derive( + Debug, + Copy, + Clone, + rkyv::Archive, + rkyv::Serialize, + rkyv::Deserialize, + Serialize, + Deserialize, + strum::Display, +)] +pub enum ErrorType { + /// Errors related to database operations. + Database, + /// Errors related to serializing data from backend to frontend (server -> client). + Serializer, + /// Errors related to parsing data (e.g. a `.owl` file). + Parser, + /// Errors related to the graph renderer (i.e. WasmGrapher) + Renderer, + #[strum(serialize = "GUI")] + /// Errors related to the frontend GUI. + Gui, + /// Server errors without a type. + InternalServerError, + /// Client errors without a type. + ClientError, +} + +#[derive( + Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, Serialize, Deserialize, +)] +#[cfg_attr(feature = "server", derive(Tabled))] +/// The fundamental building block of the error handling system. +/// +/// It stores the data of a single error event. +/// +/// # Note +/// Every error type in use should implement [`From for ErrorRecord`]. +pub struct ErrorRecord { + /// The severity of an error. + /// + /// Useful for grouping errors by severity and applying custom color schemes in the GUI. + pub severity: ErrorSeverity, + /// The type of an error. + /// + /// Useful for grouping errors by type and debugging for devs. + pub error_type: ErrorType, + /// The actual error message to show. + pub message: String, + + #[cfg(debug_assertions)] + /// The location in the source code where the error originated. + /// + /// Only enabled with [cfg.debug_assertions] + pub location: String, +} + +impl ErrorRecord { + pub fn new( + severity: ErrorSeverity, + error_type: ErrorType, + message: String, + #[cfg(debug_assertions)] location: Option, + ) -> Self { + Self { + severity, + error_type, + message, + #[cfg(debug_assertions)] + location: location.unwrap_or("Unknown".to_string()), + } + } + + #[cfg(feature = "server")] + /// Only available on the server. + pub fn format_records(records: &[ErrorRecord]) -> String { + let table_config = Settings::default().with(Style::modern_rounded()); + Table::new(records).with(table_config).to_string() + } +} + +impl TableHTML for ErrorRecord { + // TODO: implement a leptos struct table looking like: https://datatables.net/ + // Tailwind Table: https://www.material-tailwind.com/docs/html/table#table-with-hover + fn header(&self) -> impl IntoView { + let th_css = + "p-1 font-sans text-sm antialiased font-normal leading-normal text-blue-gray-900"; + view! { + + {"Severity"} + {"Error Type"} + {"Message"} + { + #[cfg(debug_assertions)] + view! { {"Code Location"} } + } + + } + } + + fn row(&self) -> impl IntoView { + let tr_color = match self.severity { + ErrorSeverity::Critical => "border-red-300 bg-red-100 text-red-700", + ErrorSeverity::Error => "border-red-200 bg-red-50 text-red-700", + ErrorSeverity::Warning => "border-yellow-200 bg-yellow-50 text-yellow-700", + ErrorSeverity::Unset => "border-slate-200 bg-slate-50 text-slate-700", + }; + + #[cfg(debug_assertions)] + let td_css = "p-2 whitespace-pre-wrap font-sans text-sm antialiased font-normal leading-normal text-blue-gray-900"; + + #[cfg(not(debug_assertions))] + let td_css = "p-2 mr-2 whitespace-pre-wrap font-sans text-sm antialiased font-normal leading-normal text-blue-gray-900"; + + view! { + + {self.severity.to_string()} + {self.error_type.to_string()} + {self.message.clone()} + { + #[cfg(debug_assertions)] + view! { {self.location.to_string()} } + } + + } + } +} + +impl From for ErrorRecord { + fn from(value: ServerFnError) -> Self { + let (error_type, message) = match value { + #[allow(deprecated, reason = "TODO: Remove in Leptos v0.9")] + ServerFnError::WrappedServerError(_) => ( + ErrorType::InternalServerError, + "deprecated WrappedServerError".to_string(), + ), + ServerFnError::Registration(e) => (ErrorType::InternalServerError, e), + ServerFnError::Request(e) => (ErrorType::ClientError, e), + ServerFnError::Response(e) => (ErrorType::InternalServerError, e), + ServerFnError::ServerError(e) => (ErrorType::InternalServerError, e), + ServerFnError::MiddlewareError(e) => (ErrorType::InternalServerError, e), + ServerFnError::Deserialization(e) => (ErrorType::ClientError, e), + ServerFnError::Serialization(e) => (ErrorType::ClientError, e), + ServerFnError::Args(e) => (ErrorType::InternalServerError, e), + ServerFnError::MissingArg(e) => (ErrorType::InternalServerError, e), + }; + + ErrorRecord::new( + ErrorSeverity::Unset, + error_type, + message, + #[cfg(debug_assertions)] + None, + ) + } +} + +impl From for ErrorRecord { + fn from(value: ServerFnErrorErr) -> Self { + let a: ServerFnError = value.into(); + a.into() + } +} + +#[cfg(feature = "server")] +impl std::fmt::Display for ErrorRecord { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let table_config = Settings::default().with(Style::modern_rounded()); + let table = Table::new([self]).with(table_config).to_string(); + write!(f, "{}", table) + } +} + +#[cfg(not(feature = "server"))] +impl std::fmt::Display for ErrorRecord { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + #[cfg(debug_assertions)] + { + write!( + f, + "{} | {} | {} | {}", + self.severity, self.error_type, self.message, self.location + ) + } + + #[cfg(not(debug_assertions))] + { + write!( + f, + "{} | {} | {}", + self.severity, self.error_type, self.message + ) + } + } +} + +#[derive( + Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, Serialize, Deserialize, +)] +/// The struct used by VOWL-R when things go south. +/// +/// # Note +/// Every error type in use should implement [`From for VOWLRServerError`]. +pub struct VOWLRError { + pub records: Vec, +} + +impl FromServerFnError for VOWLRError { + type Encoder = RkyvEncoding; + + fn from_server_fn_error(value: ServerFnErrorErr) -> Self { + value.into() + } + + fn ser(&self) -> leptos::server_fn::Bytes { + Self::Encoder::encode(self).unwrap_or_else(|e| { + Self::Encoder::encode(&Self::from_server_fn_error( + ServerFnErrorErr::Serialization(e.to_string()), + )) + .expect("serializing should at least succeed with the serialization error type") + }) + } + + fn de(data: leptos::server_fn::Bytes) -> Self { + Self::Encoder::decode(data) + .unwrap_or_else(|e| ServerFnErrorErr::Deserialization(e.to_string()).into_app_error()) + } +} + +impl From for VOWLRError { + fn from(value: ServerFnError) -> Self { + let record: ErrorRecord = value.into(); + record.into() + } +} + +impl From for VOWLRError { + fn from(value: ServerFnErrorErr) -> Self { + let record: ErrorRecord = value.into(); + record.into() + } +} + +impl From for VOWLRError { + fn from(value: ErrorRecord) -> Self { + VOWLRError { + records: vec![value], + } + } +} + +impl From> for VOWLRError { + fn from(value: Vec) -> Self { + VOWLRError { records: value } + } +} + +#[cfg(feature = "server")] +impl std::fmt::Display for VOWLRError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", ErrorRecord::format_records(&self.records)) + } +} + +#[cfg(not(feature = "server"))] +impl std::fmt::Display for VOWLRError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + #[cfg(debug_assertions)] + { + writeln!(f, "Severity | Error Type | Message | Location")?; + } + + #[cfg(not(debug_assertions))] + { + writeln!(f, "Severity | Error Type | Message")?; + } + + let mut buffer = String::new(); + for record in self.records.iter() { + writeln!(buffer, "{}", record)?; + } + write!(f, "{buffer}") + } +} diff --git a/crates/util/src/layout.rs b/crates/util/src/layout.rs new file mode 100644 index 0000000..09d38ce --- /dev/null +++ b/crates/util/src/layout.rs @@ -0,0 +1,6 @@ +use leptos::IntoView; + +pub trait TableHTML { + fn header(&self) -> impl IntoView; + fn row(&self) -> impl IntoView; +} diff --git a/crates/util/src/lib.rs b/crates/util/src/lib.rs index b698f65..4a8033c 100644 --- a/crates/util/src/lib.rs +++ b/crates/util/src/lib.rs @@ -1,5 +1,9 @@ mod datatypes; +mod error_handler; +mod layout; pub mod prelude { pub use crate::datatypes::DataType; + pub use crate::error_handler::{ErrorRecord, ErrorSeverity, ErrorType, VOWLRError}; + pub use crate::layout::TableHTML; } diff --git a/src/blocks/workbench.rs b/src/blocks/workbench.rs index 8e4abbf..9c9bc0a 100644 --- a/src/blocks/workbench.rs +++ b/src/blocks/workbench.rs @@ -4,6 +4,7 @@ mod export_menu; mod filter_menu; mod ontology_menu; mod options_menu; + // mod search_menu;1 use crate::components::lists::{ListDetails, ListElement}; use crate::components::menu::vertical_menu::VerticalMenu; @@ -27,11 +28,11 @@ pub struct GraphDataContext { #[component] fn WorkbenchMenuItems(#[prop(into)] title: String, children: Children) -> impl IntoView { view! { -
+

{title}

-
{children()}
+ {children()}
} } @@ -46,14 +47,11 @@ pub fn NewWorkbench() -> impl IntoView { total_graph_data, }); - let all_errors = RwSignal::new(Vec::::new()); - - let error_context = ErrorLogContext { errors: all_errors }; - - provide_context(error_context.clone()); + let error_context = ErrorLogContext::default(); + provide_context(error_context); let error_title = Signal::derive(move || { - let count = error_context.errors.get().len(); + let count = error_context.len(); if count > 0 { format!("Error Log ({count})") } else { diff --git a/src/blocks/workbench/error_log.rs b/src/blocks/workbench/error_log.rs index 991ccad..30e8f07 100644 --- a/src/blocks/workbench/error_log.rs +++ b/src/blocks/workbench/error_log.rs @@ -1,46 +1,103 @@ use super::WorkbenchMenuItems; +use crate::components::table::Table; +use leptos::either::Either; use leptos::prelude::*; +use vowlr_util::prelude::{ErrorRecord, VOWLRError}; -#[derive(Clone)] +#[derive(Debug, Copy, Clone)] pub struct ErrorLogContext { - pub errors: RwSignal>, + pub records: RwSignal>, } -pub fn ErrorLog() -> impl IntoView { - fn unescape_log(s: &str) -> String { - s.replace("\\n", "\n").replace("\\t", "\t") +impl ErrorLogContext { + pub fn new(records: Vec) -> Self { + Self { + records: RwSignal::new(records), + } + } + + /// Appends an element to the back of a collection. + /// + /// # Panics + /// Panics if you update the value of the signal of `self` before this function returns. + pub fn push(&self, record: ErrorRecord) { + self.records.update(|records| records.push(record)); + } + + /// Extends a collection with the contents of an iterator. + /// + /// # Panics + /// Panics if you update the value of the signal of `self` before this function returns. + pub fn extend(&self, records: Vec) { + self.records.update(|records_| records_.extend(records)); + } + + #[expect(unused)] + /// Clears the collection, removing all values. + /// + /// Note that this method has no effect on the allocated capacity of the vector. + /// + /// # Panics + /// Panics if you update the value of the signal of `self` before this function returns. + pub fn clear(&self) { + // self.records.update(|records| records.clear()); + self.records.update(std::vec::Vec::clear); + } + + /// Returns the number of elements in the collection, also referred to as its 'length' + /// + /// # Panics + /// Panics if you try to access the signal of `self` when it has been disposed. + pub fn len(&self) -> usize { + self.records.read().len() + } + + /// Returns `true` if the vector contains no elements. + /// + /// # Panics + /// Panics if you try to access the signal of `self` when it has been disposed. + pub fn is_empty(&self) -> bool { + self.records.read().is_empty() + } +} + +impl Default for ErrorLogContext { + fn default() -> Self { + Self { + records: RwSignal::new(Vec::new()), + } + } +} + +impl From for ErrorLogContext { + fn from(value: VOWLRError) -> Self { + Self::new(value.records) } +} - let error_log = expect_context::(); +pub fn ErrorLog() -> impl IntoView { + let error_context = expect_context::(); view! { {move || { - let errors = error_log.errors.get(); - view! { -
- {if errors.is_empty() { - view! {

"No errors"

} - .into_any() - } else { - view! { -
    - {errors - .into_iter() - .map(|err| { - let err = unescape_log(&err); - view! { -
  • "• " {err}
  • - } - }) - .collect_view()} - -
- } - .into_any() - }} -
+ if error_context.is_empty() { + Either::Left( + view! { +

+ "No errors" +

+ }, + ) + } else { + Either::Right( + + view! { +
+ + + }, + ) } - .into_any() }} } } diff --git a/src/blocks/workbench/export_menu.rs b/src/blocks/workbench/export_menu.rs index 61f70c4..0293262 100644 --- a/src/blocks/workbench/export_menu.rs +++ b/src/blocks/workbench/export_menu.rs @@ -1,38 +1,105 @@ use super::WorkbenchMenuItems; +use crate::blocks::workbench::error_log::ErrorLogContext; use crate::components::icon::Icon; +use crate::error::ClientErrorKind; use futures::StreamExt; use leptos::prelude::*; use leptos::server_fn::codec::{ByteStream, Streaming}; #[cfg(feature = "server")] use vowlr_database::prelude::VOWLRStore; +use vowlr_util::prelude::{DataType, VOWLRError}; +use web_sys::{Blob, BlobPropertyBag, HtmlAnchorElement, Url, js_sys, wasm_bindgen::JsCast}; #[server(output = Streaming)] -pub async fn export_owl(resource_type: String) -> Result, ServerFnError> { +/// Export a graph from the database +pub async fn export_graph(resource_type: DataType) -> Result, VOWLRError> { let store = VOWLRStore::default(); - let stream = store.serialize_stream(resource_type.into()).await?; + let stream = store.serialize_stream(resource_type).await?; Ok(ByteStream::new(stream.map(|chunk| { chunk - .map_err(|e| ServerFnError::new(e.to_string())) .map(bytes::Bytes::from) + .map_err(std::convert::Into::into) }))) } +pub async fn download_ontology( + resource_type: DataType, + progress_message: RwSignal, +) -> Result<(), VOWLRError> { + let byte_stream = export_graph(resource_type).await?; + + // Download data from server + progress_message.set("Downloaded: 0 MB".to_string()); + let mut stream = byte_stream.into_inner(); + let mut data = Vec::new(); + let mut downloaded = 0; + while let Some(chunk) = stream.next().await { + let bytes = chunk?; + downloaded += bytes.len(); + let mb = downloaded / 1_024 / 1_024; + progress_message.set(format!("Downloaded: {mb:.2} MB")); + data.extend(bytes); + } + progress_message.set("Processing...".to_string()); + + // Package received data into a blob object with apropriate metadata + let window = web_sys::window().ok_or_else(|| { + ClientErrorKind::JavaScriptError("Failed to get the Window object".to_string()) + })?; + let document = window.document().ok_or_else(|| { + ClientErrorKind::JavaScriptError("Failed to get Document object".to_string()) + })?; + let body = document.body().ok_or_else(|| { + ClientErrorKind::JavaScriptError("Failed to get the docoment body".to_string()) + })?; + + let blob_parts = js_sys::Array::new(); + let uint8_array = js_sys::Uint8Array::from(data.as_slice()); + blob_parts.push(&uint8_array.into()); + + let blob_options = BlobPropertyBag::new(); + blob_options.set_type(resource_type.mime_type()); + + let blob = Blob::new_with_str_sequence_and_options(&blob_parts, &blob_options) + .map_err(|e| ClientErrorKind::JavaScriptError(format!("{e:#?}")))?; + + // Create the URL and anchor button to show download in the browser. + let url = Url::create_object_url_with_blob(&blob) + .map_err(|e| ClientErrorKind::JavaScriptError(format!("{e:#?}")))?; + let a: HtmlAnchorElement = document + .create_element("a") + .map_err(|e| ClientErrorKind::JavaScriptError(format!("{e:#?}")))? + // SAFETY: Creating HTML element "a" will always have type 'HtmlAnchorElement'. + .unchecked_into::(); + a.set_href(&url); + a.set_download(format!("ontology.{}", resource_type.extension()).as_str()); + a.set_attribute("style", "display: none") + .map_err(|e| ClientErrorKind::JavaScriptError(format!("{e:#?}")))?; + + body.append_child(&a) + .map_err(|e| ClientErrorKind::JavaScriptError(format!("{e:#?}")))?; + a.click(); + + // Cleanup + body.remove_child(&a) + .map_err(|e| ClientErrorKind::JavaScriptError(format!("{e:#?}")))?; + Url::revoke_object_url(&url) + .map_err(|e| ClientErrorKind::JavaScriptError(format!("{e:#?}")))?; + progress_message.set("Download complete".to_string()); + + Ok(()) +} + #[component] pub fn ExportButton( #[prop(into)] label: String, #[prop(into)] icon: icondata::Icon, - #[prop(optional)] on_click: Option>, + on_click: impl Fn() + 'static, ) -> impl IntoView { - let onclick_handler = move |_| { - if let Some(cb) = &on_click { - cb.run(()); - } - }; - view! {
+ + {stuff.iter().map(T::header).next().collect_view()} + + {stuff.iter().map(T::row).collect_view()} +
+ } + .into_any() + }} + } +} diff --git a/src/components/user_input/file_upload.rs b/src/components/user_input/file_upload.rs index 2b20c07..da3724f 100644 --- a/src/components/user_input/file_upload.rs +++ b/src/components/user_input/file_upload.rs @@ -15,7 +15,9 @@ use std::path::Path; use std::rc::Rc; #[cfg(feature = "server")] use vowlr_database::prelude::{GraphDisplayDataSolutionSerializer, QueryResults, VOWLRStore}; -use vowlr_util::prelude::DataType; +#[cfg(feature = "server")] +use vowlr_parser::errors::VOWLRStoreError; +use vowlr_util::prelude::{DataType, VOWLRError}; use web_sys::{FileList, FormData}; #[cfg(feature = "ssr")] @@ -121,10 +123,7 @@ pub async fn handle_local(data: MultipartData) -> Result<(DataType, usize), Serv } } - session - .complete_upload() - .await - .map_err(|e| ServerFnError::new(e.to_string()))?; + session.complete_upload().await?; Ok((dtype, count)) } @@ -159,10 +158,7 @@ pub async fn handle_remote(url: String) -> Result<(DataType, usize), ServerFnErr chunk_result.map_err(|e| ServerFnError::new(format!("Error reading chunk: {e}")))?; total += chunk.len(); - session - .upload_chunk(&chunk) - .await - .map_err(|e| ServerFnError::new(e.to_string()))?; + session.upload_chunk(&chunk).await?; progress::add_chunk(&progress_key, chunk.len()).await; } @@ -199,10 +195,7 @@ pub async fn handle_sparql( let progress_key = format!("sparql-{endpoint}"); progress::reset(&progress_key); - session - .start_upload(&progress_key) - .await - .map_err(|e| ServerFnError::new(e.to_string()))?; + session.start_upload(&progress_key).await?; let mut total = 0; let mut stream = resp.bytes_stream(); @@ -219,10 +212,7 @@ pub async fn handle_sparql( } progress::remove(&progress_key); - session - .complete_upload() - .await - .map_err(|e| ServerFnError::new(e.to_string()))?; + session.complete_upload().await?; let dtype = if accept_type.contains("xml") { DataType::SPARQLXML @@ -233,9 +223,7 @@ pub async fn handle_sparql( } #[server (input = Rkyv, output = Rkyv)] -pub async fn handle_internal_sparql( - query: String, -) -> Result> { +pub async fn handle_internal_sparql(query: String) -> Result { let vowlr = VOWLRStore::default(); let mut data_buffer = GraphDisplayData::new(); @@ -244,16 +232,16 @@ pub async fn handle_internal_sparql( .session .query(query.as_str()) .await - .map_err(|e| ServerFnError::ServerError(format!("SPARQL query failed: {e}")))?; + .map_err(|e| >::into(e.into()))?; if let QueryResults::Solutions(solutions) = query_stream { solution_serializer .serialize_nodes_stream(&mut data_buffer, solutions) - .await - .map_err(|e| ServerFnError::ServerError(e.to_string()))?; + .await?; } else { return Err(ServerFnError::ServerError( "Query stream is not a solutions stream".to_string(), - )); + ) + .into()); } Ok(data_buffer) } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..61bcde1 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,81 @@ +use std::panic::Location; + +use vowlr_util::prelude::{ErrorRecord, ErrorSeverity, ErrorType, VOWLRError}; + +#[derive(Debug)] +pub enum ClientErrorKind { + /// An error raised when an unexpected value was received from JS-land. + JavaScriptError(String), + /// Errors related to the graph renderer (i.e. ``WasmGrapher``) + RenderError(String), +} + +impl From for ErrorRecord { + #[track_caller] + fn from(value: ClientErrorKind) -> Self { + let (message, error_type, severity) = match value { + ClientErrorKind::JavaScriptError(e) => (e, ErrorType::Gui, ErrorSeverity::Error), + ClientErrorKind::RenderError(e) => (e, ErrorType::Renderer, ErrorSeverity::Critical), + }; + Self::new( + severity, + error_type, + message, + #[cfg(debug_assertions)] + Some(Location::caller().to_string()), + ) + } +} + +impl From for VOWLRError { + fn from(value: ClientErrorKind) -> Self { + let a: ErrorRecord = value.into(); + a.into() + } +} + +// #[derive(Debug)] +// pub struct VOWLRClientError { +// /// The contained error type. +// inner: ClientErrorKind, +// /// The error's location in the source code. +// location: &'static Location<'static>, +// } +// impl std::fmt::Display for VOWLRClientError { +// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +// write!(f, "{:?}", self.inner) +// } +// } + +// impl From for VOWLRClientError { +// #[track_caller] +// fn from(error: ClientErrorKind) -> Self { +// VOWLRClientError { +// inner: error, +// location: Location::caller(), +// } +// } +// } + +// impl From for ErrorRecord { +// fn from(value: VOWLRClientError) -> Self { +// let (message, error_type, severity) = match value.inner { +// ClientErrorKind::JavaScriptError(e) => (e, ErrorType::Gui, ErrorSeverity::Error), +// ClientErrorKind::RenderError(e) => (e, ErrorType::Renderer, ErrorSeverity::Critical), +// }; +// ErrorRecord::new( +// severity, +// error_type, +// message, +// #[cfg(debug_assertions)] +// Some(value.location.to_string()), +// ) +// } +// } + +// impl From for VOWLRError { +// fn from(value: VOWLRClientError) -> Self { +// let a: ErrorRecord = value.into(); +// a.into() +// } +// } diff --git a/src/lib.rs b/src/lib.rs index bf039cf..ebf5754 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,7 @@ pub use wasm_bindgen_rayon::init_thread_pool; pub mod app; pub mod blocks; pub mod components; +pub mod error; pub mod hydration_scripts; pub mod pages;