diff --git a/lib/oxrdf/src/blank_node.rs b/lib/oxrdf/src/blank_node.rs index 6d565fda..984ac5ef 100644 --- a/lib/oxrdf/src/blank_node.rs +++ b/lib/oxrdf/src/blank_node.rs @@ -1,3 +1,4 @@ +use crate::{Term, TryFromTermError}; use rand::random; use std::io::Write; use std::{fmt, str}; @@ -232,6 +233,22 @@ impl<'a> From> for BlankNode { } } +impl TryFrom for BlankNode { + type Error = TryFromTermError; + + #[inline] + fn try_from(term: Term) -> Result { + if let Term::BlankNode(node) = term { + Ok(node) + } else { + Err(TryFromTermError { + term, + target: "BlankNode", + }) + } + } +} + impl PartialEq for BlankNodeRef<'_> { #[inline] fn eq(&self, other: &BlankNode) -> bool { @@ -352,6 +369,8 @@ pub struct BlankNodeIdParseError; mod tests { #![allow(clippy::panic_in_result_fn)] + use crate::{Literal, NamedNode}; + use super::*; #[test] @@ -360,6 +379,39 @@ mod tests { assert_eq!(b.as_str(), "42"); } + #[test] + fn casting() { + let bnode: Result = + Term::BlankNode(BlankNode::new_from_unique_id(0x42)).try_into(); + assert_eq!(bnode.unwrap(), BlankNode::new_from_unique_id(0x42)); + + let literal: Result = + Term::Literal(Literal::new_simple_literal("Hello World!")).try_into(); + assert_eq!(literal.is_err(), true); + let err = literal.unwrap_err(); + assert_eq!( + err.to_string(), + "\"Hello World!\" can not be converted to a BlankNode" + ); + assert_eq!( + Term::from(err), + Term::Literal(Literal::new_simple_literal("Hello World!")) + ); + + let named_node: Result = + Term::NamedNode(NamedNode::new("http://example.org/test").unwrap()).try_into(); + assert_eq!(named_node.is_err(), true); + let named_node_error = named_node.unwrap_err(); + assert_eq!( + named_node_error.to_string(), + " can not be converted to a BlankNode" + ); + assert_eq!( + Term::from(named_node_error), + Term::NamedNode(NamedNode::new("http://example.org/test").unwrap()) + ); + } + #[test] fn as_str_full() { let b = BlankNode::new_from_unique_id(0x7777_6666_5555_4444_3333_2222_1111_0000); diff --git a/lib/oxrdf/src/cast_error.rs b/lib/oxrdf/src/cast_error.rs new file mode 100644 index 00000000..e4c0852b --- /dev/null +++ b/lib/oxrdf/src/cast_error.rs @@ -0,0 +1,38 @@ +use std::{fmt, str}; + +use crate::{NamedNode, Subject, Term}; + +// An error return if trying to cast a term as something it cannot be converted to. +#[derive(Debug, Clone, thiserror::Error)] +#[error("{term} can not be converted to a {target}")] +pub struct TryFromTermError { + pub(crate) term: Term, + pub(crate) target: &'static str, +} + +impl From for Term { + #[inline] + fn from(error: TryFromTermError) -> Self { + error.term + } +} + +// An error return if trying to construct an invalid triple. +#[derive(Debug, thiserror::Error)] +pub struct TripleConstructionError { + pub(crate) subject: Result, + pub(crate) predicate: Result, + #[allow(dead_code)] + pub(crate) object: Term, +} + +impl fmt::Display for TripleConstructionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match (self.subject.clone().err(), self.predicate.clone().err()) { + (Some(e), Some(e2)) => write!(f, "subject: [{}], predicate: [{}]", e, e2), + (Some(e), _) => write!(f, "subject: [{}]", e), + (_, Some(e)) => write!(f, "predicate: [{}]", e), + _ => write!(f, "No Errors"), + } + } +} diff --git a/lib/oxrdf/src/lib.rs b/lib/oxrdf/src/lib.rs index aa6f712b..5a82ed19 100644 --- a/lib/oxrdf/src/lib.rs +++ b/lib/oxrdf/src/lib.rs @@ -5,6 +5,7 @@ #![doc(html_logo_url = "https://raw.githubusercontent.com/oxigraph/oxigraph/main/logo.svg")] mod blank_node; +mod cast_error; pub mod dataset; pub mod graph; mod interning; @@ -16,6 +17,7 @@ mod variable; pub mod vocab; pub use crate::blank_node::{BlankNode, BlankNodeIdParseError, BlankNodeRef}; +pub use crate::cast_error::{TripleConstructionError, TryFromTermError}; pub use crate::dataset::Dataset; pub use crate::graph::Graph; pub use crate::literal::{Literal, LiteralRef}; diff --git a/lib/oxrdf/src/literal.rs b/lib/oxrdf/src/literal.rs index 13ab07fe..ec817202 100644 --- a/lib/oxrdf/src/literal.rs +++ b/lib/oxrdf/src/literal.rs @@ -1,6 +1,6 @@ use crate::named_node::NamedNode; use crate::vocab::{rdf, xsd}; -use crate::NamedNodeRef; +use crate::{NamedNodeRef, Term, TryFromTermError}; use oxilangtag::{LanguageTag, LanguageTagParseError}; #[cfg(feature = "oxsdatatypes")] use oxsdatatypes::*; @@ -422,6 +422,22 @@ impl From for Literal { } } +impl TryFrom for Literal { + type Error = TryFromTermError; + + #[inline] + fn try_from(term: Term) -> Result { + if let Term::Literal(node) = term { + Ok(node) + } else { + Err(TryFromTermError { + term, + target: "Literal", + }) + } + } +} + /// A borrowed RDF [literal](https://www.w3.org/TR/rdf11-concepts/#dfn-literal). /// /// The default string formatter is returning an N-Triples, Turtle, and SPARQL compatible representation: @@ -638,6 +654,7 @@ mod tests { #![allow(clippy::panic_in_result_fn)] use super::*; + use crate::BlankNode; #[test] fn test_simple_literal_equality() { @@ -659,6 +676,50 @@ mod tests { ); } + #[test] + fn casting() { + let literal: Result = + Term::Literal(Literal::new_simple_literal("Hello World!")).try_into(); + assert_eq!( + literal.unwrap(), + Literal::new_simple_literal("Hello World!") + ); + + let bnode: Result = + Term::BlankNode(BlankNode::new_from_unique_id(0x42)).try_into(); + let bnode_err = bnode.unwrap_err(); + assert_eq!( + bnode_err.term, + Term::BlankNode(BlankNode::new_from_unique_id(0x42)) + ); + assert_eq!(bnode_err.target, "Literal"); + assert_eq!( + bnode_err.to_string(), + "_:42 can not be converted to a Literal" + ); + assert_eq!( + Term::from(bnode_err), + Term::BlankNode(BlankNode::new_from_unique_id(0x42)) + ); + + let named_node: Result = + Term::NamedNode(NamedNode::new("http://example.org/test").unwrap()).try_into(); + let named_node_err = named_node.unwrap_err(); + assert_eq!( + named_node_err.term, + Term::NamedNode(NamedNode::new("http://example.org/test").unwrap()) + ); + assert_eq!(named_node_err.target, "Literal"); + assert_eq!( + named_node_err.to_string(), + " can not be converted to a Literal" + ); + assert_eq!( + Term::from(named_node_err), + Term::NamedNode(NamedNode::new("http://example.org/test").unwrap()) + ); + } + #[test] fn test_float_format() { assert_eq!("INF", Literal::from(f32::INFINITY).value()); diff --git a/lib/oxrdf/src/named_node.rs b/lib/oxrdf/src/named_node.rs index 9b545bcc..3f8637a4 100644 --- a/lib/oxrdf/src/named_node.rs +++ b/lib/oxrdf/src/named_node.rs @@ -1,3 +1,4 @@ +use crate::{Term, TryFromTermError}; use oxiri::{Iri, IriParseError}; use std::cmp::Ordering; use std::fmt; @@ -234,3 +235,72 @@ impl<'a> From> for NamedNodeRef<'a> { } } } + +impl TryFrom for NamedNode { + type Error = TryFromTermError; + + #[inline] + fn try_from(term: Term) -> Result { + if let Term::NamedNode(node) = term { + Ok(node) + } else { + Err(TryFromTermError { + term, + target: "NamedNode", + }) + } + } +} + +#[cfg(test)] +mod tests { + #![allow(clippy::panic_in_result_fn)] + + use crate::{BlankNode, Literal}; + + use super::*; + + #[test] + fn casting() { + let named_node: Result = + Term::NamedNode(NamedNode::new("http://example.org/test").unwrap()).try_into(); + assert_eq!( + named_node.unwrap(), + NamedNode::new("http://example.org/test").unwrap() + ); + + let literal: Result = + Term::Literal(Literal::new_simple_literal("Hello World!")).try_into(); + let literal_err = literal.unwrap_err(); + assert_eq!( + literal_err.term, + Term::Literal(Literal::new_simple_literal("Hello World!")) + ); + assert_eq!(literal_err.target, "NamedNode"); + assert_eq!( + literal_err.to_string(), + "\"Hello World!\" can not be converted to a NamedNode" + ); + assert_eq!( + Term::from(literal_err), + Term::Literal(Literal::new_simple_literal("Hello World!")) + ); + + let bnode: Result = + Term::BlankNode(BlankNode::new_from_unique_id(0x42)).try_into(); + let bnode_err = bnode.unwrap_err(); + assert_eq!( + bnode_err.term, + Term::BlankNode(BlankNode::new_from_unique_id(0x42)) + ); + assert_eq!(bnode_err.target, "NamedNode"); + assert_eq!( + bnode_err.to_string(), + "_:42 can not be converted to a NamedNode" + ); + assert_eq!( + Term::from(bnode_err), + Term::BlankNode(BlankNode::new_from_unique_id(0x42)) + ); + } +} diff --git a/lib/oxrdf/src/triple.rs b/lib/oxrdf/src/triple.rs index 76039644..184c3edb 100644 --- a/lib/oxrdf/src/triple.rs +++ b/lib/oxrdf/src/triple.rs @@ -1,7 +1,8 @@ use crate::blank_node::BlankNode; +use crate::cast_error::TripleConstructionError; use crate::literal::Literal; use crate::named_node::NamedNode; -use crate::{BlankNodeRef, LiteralRef, NamedNodeRef}; +use crate::{BlankNodeRef, LiteralRef, NamedNodeRef, TryFromTermError}; use std::fmt; /// The owned union of [IRIs](https://www.w3.org/TR/rdf11-concepts/#dfn-iri) and [blank nodes](https://www.w3.org/TR/rdf11-concepts/#dfn-blank-node). @@ -223,6 +224,24 @@ impl From> for Subject { } } +impl TryFrom for Subject { + type Error = TryFromTermError; + + #[inline] + fn try_from(term: Term) -> Result { + match term { + Term::NamedNode(node) => Ok(Subject::NamedNode(node)), + Term::BlankNode(node) => Ok(Subject::BlankNode(node)), + #[cfg(feature = "rdf-star")] + Term::Triple(triple) => Ok(Subject::Triple(triple)), + Term::Literal(_) => Err(TryFromTermError { + term, + target: "Subject", + }), + } + } +} + #[cfg(feature = "rdf-star")] impl From for Subject { #[inline] @@ -738,6 +757,30 @@ impl Triple { } } + /// Builds an RDF [triple](https://www.w3.org/TR/rdf11-concepts/#dfn-rdf-triple). + #[inline] + pub fn new_from_terms( + subject: impl Into, + predicate: impl Into, + object: impl Into, + ) -> Result { + let subject: Result = subject.into().try_into(); + let predicate: Result = predicate.into().try_into(); + if let (Ok(subject), Ok(predicate)) = (subject.clone(), predicate.clone()) { + Ok(Self { + subject, + predicate, + object: object.into(), + }) + } else { + Err(TripleConstructionError { + subject, + predicate, + object: object.into(), + }) + } + } + /// Encodes that this triple is in an [RDF dataset](https://www.w3.org/TR/rdf11-concepts/#dfn-rdf-dataset). #[inline] pub fn in_graph(self, graph_name: impl Into) -> Quad { @@ -766,6 +809,23 @@ impl fmt::Display for Triple { } } +#[cfg(feature = "rdf-star")] +impl TryFrom for Box { + type Error = TryFromTermError; + + #[inline] + fn try_from(term: Term) -> Result { + if let Term::Triple(node) = term { + Ok(node) + } else { + Err(TryFromTermError { + term, + target: "Box", + }) + } + } +} + /// A borrowed [RDF triple](https://www.w3.org/TR/rdf11-concepts/#dfn-rdf-triple). /// /// The default string formatter is returning an N-Triples, Turtle, and SPARQL compatible representation: @@ -1224,3 +1284,237 @@ impl<'a> From> for Quad { quad.into_owned() } } + +#[cfg(feature = "rdf-star")] +#[cfg(test)] +mod tests { + #![allow(clippy::panic_in_result_fn)] + + use super::*; + + #[test] + fn casting_triple() { + let triple = Triple { + subject: NamedNode::new("http://example.org/s").unwrap().into(), + predicate: NamedNode::new("http://example.org/p").unwrap(), + object: NamedNode::new("http://example.org/o").unwrap().into(), + }; + let triple_box = Box::new(triple); + + let t: Result, TryFromTermError> = Term::Triple(triple_box.clone()).try_into(); + assert_eq!(t.unwrap(), triple_box.clone()); + + let literal: Result, TryFromTermError> = + Term::Literal(Literal::new_simple_literal("Hello World!")).try_into(); + let literal_err = literal.unwrap_err(); + assert_eq!( + literal_err.term, + Term::Literal(Literal::new_simple_literal("Hello World!")) + ); + assert_eq!(literal_err.target, "Box"); + assert_eq!( + literal_err.to_string(), + "\"Hello World!\" can not be converted to a Box" + ); + assert_eq!( + Term::from(literal_err), + Term::Literal(Literal::new_simple_literal("Hello World!")) + ); + + let bnode: Result, TryFromTermError> = + Term::BlankNode(BlankNode::new_from_unique_id(0x42)).try_into(); + let bnode_err = bnode.unwrap_err(); + assert_eq!( + bnode_err.term, + Term::BlankNode(BlankNode::new_from_unique_id(0x42)) + ); + assert_eq!(bnode_err.target, "Box"); + assert_eq!( + bnode_err.to_string(), + "_:42 can not be converted to a Box" + ); + assert_eq!( + Term::from(bnode_err), + Term::BlankNode(BlankNode::new_from_unique_id(0x42)) + ); + + let named_node: Result, TryFromTermError> = + Term::NamedNode(NamedNode::new("http://example.org/test").unwrap()).try_into(); + let named_node_err = named_node.unwrap_err(); + assert_eq!( + named_node_err.term, + Term::NamedNode(NamedNode::new("http://example.org/test").unwrap()) + ); + assert_eq!(named_node_err.target, "Box"); + assert_eq!( + named_node_err.to_string(), + " can not be converted to a Box" + ); + assert_eq!( + Term::from(named_node_err), + Term::NamedNode(NamedNode::new("http://example.org/test").unwrap()) + ); + } + + #[test] + fn constructing_triple() { + use super::*; + + let optional_triple = Triple::new_from_terms( + Term::NamedNode(NamedNode::new("http://example.org/test").unwrap()), + Term::NamedNode(NamedNode::new("http://example.org/test").unwrap()), + Term::NamedNode(NamedNode::new("http://example.org/test").unwrap()), + ); + + let optional_triple_2 = Triple::new_from_terms( + NamedNode::new("http://example.org/test").unwrap(), + NamedNode::new("http://example.org/test").unwrap(), + NamedNode::new("http://example.org/test").unwrap(), + ); + + let bad_triple: Result = Triple::new_from_terms( + Term::Literal(Literal::new_simple_literal("abc123")), + Term::NamedNode(NamedNode::new("http://example.org/test").unwrap()), + Term::NamedNode(NamedNode::new("http://example.org/test").unwrap()), + ); + + let bad_triple_2 = Triple::new_from_terms( + Term::Literal(Literal::new_simple_literal("abc123")), + NamedNode::new("http://example.org/test").unwrap(), + NamedNode::new("http://example.org/test").unwrap(), + ); + + let bad_triple_3 = Triple::new_from_terms( + Term::Literal(Literal::new_simple_literal("abc123")), + BlankNode::new_from_unique_id(0x42), + NamedNode::new("http://example.org/test").unwrap(), + ); + + let triple: Triple = Triple::new( + Subject::NamedNode(NamedNode::new("http://example.org/test").unwrap()), + NamedNode::new("http://example.org/test").unwrap(), + Term::NamedNode(NamedNode::new("http://example.org/test").unwrap()), + ); + + let triple_2: Triple = Triple::new( + NamedNode::new("http://example.org/test").unwrap(), + NamedNode::new("http://example.org/test").unwrap(), + NamedNode::new("http://example.org/test").unwrap(), + ); + + let unwrapped = optional_triple.unwrap(); + + assert_eq!(unwrapped, triple); + assert_eq!(unwrapped, triple_2.clone()); + assert_eq!(optional_triple_2.unwrap(), triple_2); + let bad_triple_err = bad_triple.unwrap_err(); + assert_eq!( + bad_triple_err.to_string(), + "subject: [\"abc123\" can not be converted to a Subject]" + ); + assert_eq!( + bad_triple_err.subject.clone().unwrap_err().clone().term, + Term::Literal(Literal::new_simple_literal("abc123")) + ); + assert_eq!( + bad_triple_err.subject.clone().unwrap_err().clone().target, + "Subject" + ); + assert_eq!( + bad_triple_err.subject.unwrap_err().clone().to_string(), + "\"abc123\" can not be converted to a Subject" + ); + + assert_eq!(bad_triple_2.is_err(), true); + let bad_triple_2_err = bad_triple_2.unwrap_err(); + assert_eq!( + bad_triple_2_err.to_string(), + "subject: [\"abc123\" can not be converted to a Subject]" + ); + assert_eq!( + bad_triple_2_err.subject.clone().unwrap_err().clone().term, + Term::Literal(Literal::new_simple_literal("abc123")) + ); + assert_eq!( + bad_triple_2_err.subject.clone().unwrap_err().clone().target, + "Subject" + ); + assert_eq!( + bad_triple_2_err.subject.unwrap_err().clone().to_string(), + "\"abc123\" can not be converted to a Subject" + ); + assert_eq!( + bad_triple_2_err.predicate.unwrap(), + NamedNode::new("http://example.org/test").unwrap() + ); + assert_eq!( + bad_triple_2_err.object, + Term::NamedNode(NamedNode::new("http://example.org/test").unwrap()) + ); + + assert_eq!(bad_triple_3.is_err(), true); + let bad_triple_3_err = bad_triple_3.unwrap_err(); + assert_eq!(bad_triple_3_err.to_string(), "subject: [\"abc123\" can not be converted to a Subject], predicate: [_:42 can not be converted to a NamedNode]"); + assert_eq!( + bad_triple_3_err.subject.clone().unwrap_err().clone().term, + Term::Literal(Literal::new_simple_literal("abc123")) + ); + assert_eq!( + bad_triple_3_err.subject.clone().unwrap_err().clone().target, + "Subject" + ); + assert_eq!( + bad_triple_3_err.subject.unwrap_err().clone().to_string(), + "\"abc123\" can not be converted to a Subject" + ); + assert_eq!( + bad_triple_3_err.predicate.clone().unwrap_err().clone().term, + Term::BlankNode(BlankNode::new_from_unique_id(0x42)) + ); + assert_eq!( + bad_triple_3_err + .predicate + .clone() + .unwrap_err() + .clone() + .target, + "NamedNode" + ); + assert_eq!( + bad_triple_3_err.predicate.unwrap_err().clone().to_string(), + "_:42 can not be converted to a NamedNode" + ); + } + + #[test] + fn casting_subject() { + let triple = Triple { + subject: NamedNode::new("http://example.org/s").unwrap().into(), + predicate: NamedNode::new("http://example.org/p").unwrap(), + object: NamedNode::new("http://example.org/o").unwrap().into(), + }; + let triple_box = Box::new(triple); + + let t: Result = Term::Triple(triple_box.clone()).try_into(); + assert_eq!(t.unwrap(), Subject::Triple(triple_box.clone())); + + let literal: Result = + Term::Literal(Literal::new_simple_literal("Hello World!")).try_into(); + assert_eq!(literal.is_err(), true); + assert_eq!(literal.is_err(), true); + + let bnode: Result = + Term::BlankNode(BlankNode::new_from_unique_id(0x42)).try_into(); + assert_eq!( + bnode.unwrap(), + Subject::BlankNode(BlankNode::new_from_unique_id(0x42)) + ); + + let named_node: Result = + Term::NamedNode(NamedNode::new("http://example.org/test").unwrap()).try_into(); + assert_eq!( + named_node.unwrap(), + Subject::NamedNode(NamedNode::new("http://example.org/test").unwrap()) + ); + } +}