use serde::{Deserialize, Serialize};
use std::fmt;
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Term {
Iri(Iri),
BlankNode(BlankNode),
Literal(Literal),
}
impl Term {
#[inline]
pub fn iri(value: impl Into<Arc<str>>) -> Self {
Term::Iri(Iri::new(value))
}
#[inline]
pub fn blank(id: impl Into<Arc<str>>) -> Self {
Term::BlankNode(BlankNode::new(id))
}
#[inline]
pub fn literal(value: impl Into<Arc<str>>) -> Self {
Term::Literal(Literal::simple(value))
}
#[inline]
pub fn typed_literal(value: impl Into<Arc<str>>, datatype: impl Into<Arc<str>>) -> Self {
Term::Literal(Literal::typed(value, datatype))
}
#[inline]
pub fn lang_literal(value: impl Into<Arc<str>>, lang: impl Into<Arc<str>>) -> Self {
Term::Literal(Literal::with_language(value, lang))
}
#[inline]
#[must_use]
pub fn is_iri(&self) -> bool {
matches!(self, Term::Iri(_))
}
#[inline]
#[must_use]
pub fn is_blank_node(&self) -> bool {
matches!(self, Term::BlankNode(_))
}
#[inline]
#[must_use]
pub fn is_literal(&self) -> bool {
matches!(self, Term::Literal(_))
}
#[inline]
#[must_use]
pub fn as_iri(&self) -> Option<&Iri> {
match self {
Term::Iri(iri) => Some(iri),
_ => None,
}
}
#[inline]
#[must_use]
pub fn as_blank_node(&self) -> Option<&BlankNode> {
match self {
Term::BlankNode(bn) => Some(bn),
_ => None,
}
}
#[inline]
#[must_use]
pub fn as_literal(&self) -> Option<&Literal> {
match self {
Term::Literal(lit) => Some(lit),
_ => None,
}
}
}
impl Term {
pub fn from_ntriples(s: &str) -> Option<Self> {
let s = s.trim();
if let Some(inner) = s.strip_prefix('<').and_then(|s| s.strip_suffix('>')) {
Some(Term::Iri(Iri::new(inner)))
} else if let Some(id) = s.strip_prefix("_:") {
Some(Term::BlankNode(BlankNode::new(id)))
} else if s.starts_with('"') {
let bytes = s.as_bytes();
let mut pos = 1;
let mut value = String::new();
while pos < bytes.len() {
if bytes[pos] == b'\\' && pos + 1 < bytes.len() {
match bytes[pos + 1] {
b'"' => value.push('"'),
b'\\' => value.push('\\'),
b'n' => value.push('\n'),
b'r' => value.push('\r'),
b't' => value.push('\t'),
other => {
value.push('\\');
value.push(other as char);
}
}
pos += 2;
} else if bytes[pos] == b'"' {
pos += 1;
break;
} else {
value.push(bytes[pos] as char);
pos += 1;
}
}
let rest = &s[pos..];
if let Some(lang) = rest.strip_prefix('@') {
Some(Term::Literal(Literal::with_language(value, lang)))
} else if let Some(typed) = rest.strip_prefix("^^<").and_then(|s| s.strip_suffix('>')) {
Some(Term::Literal(Literal::typed(value, typed)))
} else {
Some(Term::Literal(Literal::simple(value)))
}
} else {
None
}
}
pub fn to_ntriples(&self) -> String {
self.to_string()
}
}
impl fmt::Display for Term {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Term::Iri(iri) => write!(f, "{}", iri),
Term::BlankNode(bn) => write!(f, "{}", bn),
Term::Literal(lit) => write!(f, "{}", lit),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Iri {
value: Arc<str>,
}
impl Iri {
#[inline]
pub fn new(value: impl Into<Arc<str>>) -> Self {
Self {
value: value.into(),
}
}
#[inline]
#[must_use]
pub fn as_str(&self) -> &str {
&self.value
}
#[must_use]
pub fn local_name(&self) -> &str {
if let Some(pos) = self.value.rfind('#') {
&self.value[pos + 1..]
} else if let Some(pos) = self.value.rfind('/') {
&self.value[pos + 1..]
} else {
&self.value
}
}
#[must_use]
pub fn namespace(&self) -> &str {
if let Some(pos) = self.value.rfind('#') {
&self.value[..=pos]
} else if let Some(pos) = self.value.rfind('/') {
&self.value[..=pos]
} else {
""
}
}
}
impl fmt::Display for Iri {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "<{}>", self.value)
}
}
impl From<&str> for Iri {
fn from(s: &str) -> Self {
Self::new(s)
}
}
impl From<String> for Iri {
fn from(s: String) -> Self {
Self::new(s)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct BlankNode {
id: Arc<str>,
}
impl BlankNode {
#[inline]
pub fn new(id: impl Into<Arc<str>>) -> Self {
Self { id: id.into() }
}
#[inline]
#[must_use]
pub fn id(&self) -> &str {
&self.id
}
}
impl fmt::Display for BlankNode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "_:{}", self.id)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Literal {
value: Arc<str>,
datatype: Arc<str>,
language: Option<Arc<str>>,
}
impl Literal {
pub const XSD: &'static str = "http://www.w3.org/2001/XMLSchema#";
pub const RDF: &'static str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
pub const XSD_STRING: &'static str = "http://www.w3.org/2001/XMLSchema#string";
pub const XSD_INTEGER: &'static str = "http://www.w3.org/2001/XMLSchema#integer";
pub const XSD_DECIMAL: &'static str = "http://www.w3.org/2001/XMLSchema#decimal";
pub const XSD_DOUBLE: &'static str = "http://www.w3.org/2001/XMLSchema#double";
pub const XSD_BOOLEAN: &'static str = "http://www.w3.org/2001/XMLSchema#boolean";
pub const XSD_DATETIME: &'static str = "http://www.w3.org/2001/XMLSchema#dateTime";
pub const RDF_LANG_STRING: &'static str =
"http://www.w3.org/1999/02/22-rdf-syntax-ns#langString";
#[inline]
pub fn simple(value: impl Into<Arc<str>>) -> Self {
Self {
value: value.into(),
datatype: Self::XSD_STRING.into(),
language: None,
}
}
#[inline]
pub fn typed(value: impl Into<Arc<str>>, datatype: impl Into<Arc<str>>) -> Self {
Self {
value: value.into(),
datatype: datatype.into(),
language: None,
}
}
#[inline]
pub fn with_language(value: impl Into<Arc<str>>, language: impl Into<Arc<str>>) -> Self {
Self {
value: value.into(),
datatype: Self::RDF_LANG_STRING.into(),
language: Some(language.into()),
}
}
#[inline]
pub fn integer(value: i64) -> Self {
Self::typed(value.to_string(), Self::XSD_INTEGER)
}
#[inline]
pub fn double(value: f64) -> Self {
Self::typed(value.to_string(), Self::XSD_DOUBLE)
}
#[inline]
pub fn boolean(value: bool) -> Self {
Self::typed(if value { "true" } else { "false" }, Self::XSD_BOOLEAN)
}
#[inline]
#[must_use]
pub fn value(&self) -> &str {
&self.value
}
#[inline]
#[must_use]
pub fn datatype(&self) -> &str {
&self.datatype
}
#[inline]
#[must_use]
pub fn language(&self) -> Option<&str> {
self.language.as_deref()
}
#[inline]
#[must_use]
pub fn is_simple(&self) -> bool {
self.datatype.as_ref() == Self::XSD_STRING && self.language.is_none()
}
#[inline]
#[must_use]
pub fn is_lang_string(&self) -> bool {
self.language.is_some()
}
#[must_use]
pub fn as_integer(&self) -> Option<i64> {
self.value.parse().ok()
}
#[must_use]
pub fn as_double(&self) -> Option<f64> {
self.value.parse().ok()
}
#[must_use]
pub fn as_boolean(&self) -> Option<bool> {
match self.value.as_ref() {
"true" | "1" => Some(true),
"false" | "0" => Some(false),
_ => None,
}
}
}
impl fmt::Display for Literal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "\"")?;
for ch in self.value.chars() {
match ch {
'"' => write!(f, "\\\"")?,
'\\' => write!(f, "\\\\")?,
'\n' => write!(f, "\\n")?,
'\r' => write!(f, "\\r")?,
'\t' => write!(f, "\\t")?,
_ => write!(f, "{}", ch)?,
}
}
write!(f, "\"")?;
if let Some(ref lang) = self.language {
write!(f, "@{}", lang)
} else if self.datatype.as_ref() != Self::XSD_STRING {
write!(f, "^^<{}>", self.datatype)
} else {
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_iri_creation() {
let iri = Iri::new("http://example.org/resource");
assert_eq!(iri.as_str(), "http://example.org/resource");
assert_eq!(iri.local_name(), "resource");
assert_eq!(iri.namespace(), "http://example.org/");
}
#[test]
fn test_iri_with_fragment() {
let iri = Iri::new("http://xmlns.com/foaf/0.1#Person");
assert_eq!(iri.local_name(), "Person");
assert_eq!(iri.namespace(), "http://xmlns.com/foaf/0.1#");
}
#[test]
fn test_blank_node() {
let bn = BlankNode::new("b0");
assert_eq!(bn.id(), "b0");
assert_eq!(bn.to_string(), "_:b0");
}
#[test]
fn test_simple_literal() {
let lit = Literal::simple("Hello");
assert_eq!(lit.value(), "Hello");
assert_eq!(lit.datatype(), Literal::XSD_STRING);
assert!(lit.is_simple());
assert!(!lit.is_lang_string());
}
#[test]
fn test_typed_literal() {
let lit = Literal::integer(42);
assert_eq!(lit.value(), "42");
assert_eq!(lit.datatype(), Literal::XSD_INTEGER);
assert_eq!(lit.as_integer(), Some(42));
}
#[test]
fn test_lang_literal() {
let lit = Literal::with_language("Bonjour", "fr");
assert_eq!(lit.value(), "Bonjour");
assert_eq!(lit.language(), Some("fr"));
assert!(lit.is_lang_string());
}
#[test]
fn test_term_display() {
assert_eq!(
Term::iri("http://example.org").to_string(),
"<http://example.org>"
);
assert_eq!(Term::blank("b0").to_string(), "_:b0");
assert_eq!(Term::literal("Hello").to_string(), "\"Hello\"");
assert_eq!(
Term::lang_literal("Bonjour", "fr").to_string(),
"\"Bonjour\"@fr"
);
assert_eq!(
Term::typed_literal("42", Literal::XSD_INTEGER).to_string(),
"\"42\"^^<http://www.w3.org/2001/XMLSchema#integer>"
);
}
}