use crate::model::{NamedNode, NamedNodeRef, ObjectTerm, RdfTerm};
use crate::vocab::{rdf, xsd};
use crate::OxirsError;
use lazy_static::lazy_static;
use oxilangtag::LanguageTag as OxiLanguageTag;
use oxsdatatypes::{Boolean, Date, DateTime, Decimal, Double, Float, Integer, Time};
use regex::Regex;
use std::borrow::Cow;
use std::fmt::{self, Write};
use std::hash::Hash;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LanguageTagParseError {
message: String,
}
impl fmt::Display for LanguageTagParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Language tag parse error: {}", self.message)
}
}
impl std::error::Error for LanguageTagParseError {}
impl From<LanguageTagParseError> for OxirsError {
fn from(err: LanguageTagParseError) -> Self {
OxirsError::Parse(err.message)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct LanguageTag {
tag: String,
}
impl LanguageTag {
pub fn parse(tag: impl Into<String>) -> Result<Self, LanguageTagParseError> {
let tag = tag.into();
validate_language_tag(&tag)?;
Ok(LanguageTag { tag })
}
pub fn as_str(&self) -> &str {
&self.tag
}
pub fn into_inner(self) -> String {
self.tag
}
}
impl fmt::Display for LanguageTag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.tag)
}
}
lazy_static! {
static ref LANGUAGE_TAG_REGEX: Regex = Regex::new(
r"^([a-zA-Z]{2,3}(-[a-zA-Z]{3}){0,3}(-[a-zA-Z]{4})?(-[a-zA-Z]{2}|\d{3})?(-[0-9a-zA-Z]{5,8}|-\d[0-9a-zA-Z]{3})*(-[0-9a-wyzA-WYZ](-[0-9a-zA-Z]{2,8})+)*(-x(-[0-9a-zA-Z]{1,8})+)?|x(-[0-9a-zA-Z]{1,8})+|[a-zA-Z]{4}|[a-zA-Z]{5,8})$"
).expect("Language tag regex compilation failed");
static ref SIMPLE_LANGUAGE_REGEX: Regex = Regex::new(
r"^[a-zA-Z]{2,3}$"
).expect("Simple language regex compilation failed");
static ref INTEGER_REGEX: Regex = Regex::new(
r"^[+-]?\d+$"
).expect("Integer regex compilation failed");
static ref DECIMAL_REGEX: Regex = Regex::new(
r"^[+-]?(\d+(\.\d*)?|\.\d+)$"
).expect("Decimal regex compilation failed");
static ref DOUBLE_REGEX: Regex = Regex::new(
r"^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$|^[+-]?INF$|^NaN$"
).expect("Double regex compilation failed");
static ref BOOLEAN_REGEX: Regex = Regex::new(
r"^(true|false|1|0)$"
).expect("Boolean regex compilation failed");
static ref DATETIME_REGEX: Regex = Regex::new(
r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$"
).expect("DateTime regex compilation failed");
static ref DATE_REGEX: Regex = Regex::new(
r"^\d{4}-\d{2}-\d{2}(Z|[+-]\d{2}:\d{2})?$"
).expect("Date regex compilation failed");
static ref TIME_REGEX: Regex = Regex::new(
r"^\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$"
).expect("Time regex compilation failed");
}
fn validate_language_tag(tag: &str) -> Result<(), LanguageTagParseError> {
OxiLanguageTag::parse(tag)
.map(|_| ())
.map_err(|e| LanguageTagParseError {
message: format!("Invalid language tag '{tag}': {e}"),
})
}
pub fn validate_xsd_value(value: &str, datatype_iri: &str) -> Result<(), OxirsError> {
match datatype_iri {
"http://www.w3.org/2001/XMLSchema#string"
| "http://www.w3.org/2001/XMLSchema#normalizedString"
| "http://www.w3.org/2001/XMLSchema#token" => {
Ok(())
}
"http://www.w3.org/2001/XMLSchema#boolean" => Boolean::from_str(value)
.map(|_| ())
.map_err(|e| OxirsError::Parse(format!("Invalid boolean value '{value}': {e}"))),
"http://www.w3.org/2001/XMLSchema#integer"
| "http://www.w3.org/2001/XMLSchema#long"
| "http://www.w3.org/2001/XMLSchema#int"
| "http://www.w3.org/2001/XMLSchema#short"
| "http://www.w3.org/2001/XMLSchema#byte"
| "http://www.w3.org/2001/XMLSchema#unsignedLong"
| "http://www.w3.org/2001/XMLSchema#unsignedInt"
| "http://www.w3.org/2001/XMLSchema#unsignedShort"
| "http://www.w3.org/2001/XMLSchema#unsignedByte"
| "http://www.w3.org/2001/XMLSchema#positiveInteger"
| "http://www.w3.org/2001/XMLSchema#nonNegativeInteger"
| "http://www.w3.org/2001/XMLSchema#negativeInteger"
| "http://www.w3.org/2001/XMLSchema#nonPositiveInteger" => Integer::from_str(value)
.map_err(|e| OxirsError::Parse(format!("Invalid integer value '{value}': {e}")))
.and_then(|integer| validate_integer_range_oxs(integer, datatype_iri)),
"http://www.w3.org/2001/XMLSchema#decimal" => Decimal::from_str(value)
.map(|_| ())
.map_err(|e| OxirsError::Parse(format!("Invalid decimal value '{value}': {e}"))),
"http://www.w3.org/2001/XMLSchema#float" => Float::from_str(value)
.map(|_| ())
.map_err(|e| OxirsError::Parse(format!("Invalid float value '{value}': {e}"))),
"http://www.w3.org/2001/XMLSchema#double" => Double::from_str(value)
.map(|_| ())
.map_err(|e| OxirsError::Parse(format!("Invalid double value '{value}': {e}"))),
"http://www.w3.org/2001/XMLSchema#dateTime" => DateTime::from_str(value)
.map(|_| ())
.map_err(|e| OxirsError::Parse(format!("Invalid dateTime value '{value}': {e}"))),
"http://www.w3.org/2001/XMLSchema#date" => Date::from_str(value)
.map(|_| ())
.map_err(|e| OxirsError::Parse(format!("Invalid date value '{value}': {e}"))),
"http://www.w3.org/2001/XMLSchema#time" => Time::from_str(value)
.map(|_| ())
.map_err(|e| OxirsError::Parse(format!("Invalid time value '{value}': {e}"))),
_ => Ok(()),
}
}
#[allow(dead_code)]
fn validate_integer_range(value: &str, datatype_iri: &str) -> Result<(), OxirsError> {
let parsed_value: i64 = value
.parse()
.map_err(|_| OxirsError::Parse(format!("Cannot parse integer: '{value}'")))?;
match datatype_iri {
"http://www.w3.org/2001/XMLSchema#byte" => {
if !(-128..=127).contains(&parsed_value) {
return Err(OxirsError::Parse(format!(
"Byte value out of range: {parsed_value}. Must be between -128 and 127"
)));
}
}
"http://www.w3.org/2001/XMLSchema#short" => {
if !(-32768..=32767).contains(&parsed_value) {
return Err(OxirsError::Parse(format!(
"Short value out of range: {parsed_value}. Must be between -32768 and 32767"
)));
}
}
"http://www.w3.org/2001/XMLSchema#int" => {
if !(-2147483648..=2147483647).contains(&parsed_value) {
return Err(OxirsError::Parse(format!(
"Int value out of range: {parsed_value}. Must be between -2147483648 and 2147483647"
)));
}
}
"http://www.w3.org/2001/XMLSchema#unsignedByte" => {
if !(0..=255).contains(&parsed_value) {
return Err(OxirsError::Parse(format!(
"Unsigned byte value out of range: {parsed_value}. Must be between 0 and 255"
)));
}
}
"http://www.w3.org/2001/XMLSchema#unsignedShort" => {
if !(0..=65535).contains(&parsed_value) {
return Err(OxirsError::Parse(format!(
"Unsigned short value out of range: {parsed_value}. Must be between 0 and 65535"
)));
}
}
"http://www.w3.org/2001/XMLSchema#unsignedInt" => {
if !(0..=4294967295).contains(&parsed_value) {
return Err(OxirsError::Parse(format!(
"Unsigned int value out of range: {parsed_value}. Must be between 0 and 4294967295"
)));
}
}
"http://www.w3.org/2001/XMLSchema#positiveInteger" => {
if parsed_value <= 0 {
return Err(OxirsError::Parse(format!(
"Positive integer must be greater than 0, got: {parsed_value}"
)));
}
}
"http://www.w3.org/2001/XMLSchema#nonNegativeInteger" => {
if parsed_value < 0 {
return Err(OxirsError::Parse(format!(
"Non-negative integer must be >= 0, got: {parsed_value}"
)));
}
}
"http://www.w3.org/2001/XMLSchema#negativeInteger" => {
if parsed_value >= 0 {
return Err(OxirsError::Parse(format!(
"Negative integer must be less than 0, got: {parsed_value}"
)));
}
}
"http://www.w3.org/2001/XMLSchema#nonPositiveInteger" => {
if parsed_value > 0 {
return Err(OxirsError::Parse(format!(
"Non-positive integer must be <= 0, got: {parsed_value}"
)));
}
}
_ => {} }
Ok(())
}
fn validate_integer_range_oxs(integer: Integer, datatype_iri: &str) -> Result<(), OxirsError> {
let parsed_value: i64 = integer.to_string().parse().map_err(|_| {
OxirsError::Parse("Cannot convert integer to i64 for range validation".to_string())
})?;
match datatype_iri {
"http://www.w3.org/2001/XMLSchema#byte" => {
if !(-128..=127).contains(&parsed_value) {
return Err(OxirsError::Parse(format!(
"Byte value out of range: {parsed_value}. Must be between -128 and 127"
)));
}
}
"http://www.w3.org/2001/XMLSchema#short" => {
if !(-32768..=32767).contains(&parsed_value) {
return Err(OxirsError::Parse(format!(
"Short value out of range: {parsed_value}. Must be between -32768 and 32767"
)));
}
}
"http://www.w3.org/2001/XMLSchema#int" => {
if !(-2147483648..=2147483647).contains(&parsed_value) {
return Err(OxirsError::Parse(format!(
"Int value out of range: {parsed_value}. Must be between -2147483648 and 2147483647"
)));
}
}
"http://www.w3.org/2001/XMLSchema#unsignedByte" => {
if !(0..=255).contains(&parsed_value) {
return Err(OxirsError::Parse(format!(
"Unsigned byte value out of range: {parsed_value}. Must be between 0 and 255"
)));
}
}
"http://www.w3.org/2001/XMLSchema#unsignedShort" => {
if !(0..=65535).contains(&parsed_value) {
return Err(OxirsError::Parse(format!(
"Unsigned short value out of range: {parsed_value}. Must be between 0 and 65535"
)));
}
}
"http://www.w3.org/2001/XMLSchema#unsignedInt" => {
if !(0..=4294967295).contains(&parsed_value) {
return Err(OxirsError::Parse(format!(
"Unsigned int value out of range: {parsed_value}. Must be between 0 and 4294967295"
)));
}
}
"http://www.w3.org/2001/XMLSchema#positiveInteger" => {
if parsed_value <= 0 {
return Err(OxirsError::Parse(format!(
"Positive integer must be greater than 0, got: {parsed_value}"
)));
}
}
"http://www.w3.org/2001/XMLSchema#nonNegativeInteger" => {
if parsed_value < 0 {
return Err(OxirsError::Parse(format!(
"Non-negative integer must be >= 0, got: {parsed_value}"
)));
}
}
"http://www.w3.org/2001/XMLSchema#negativeInteger" => {
if parsed_value >= 0 {
return Err(OxirsError::Parse(format!(
"Negative integer must be less than 0, got: {parsed_value}"
)));
}
}
"http://www.w3.org/2001/XMLSchema#nonPositiveInteger" => {
if parsed_value > 0 {
return Err(OxirsError::Parse(format!(
"Non-positive integer must be <= 0, got: {parsed_value}"
)));
}
}
_ => {} }
Ok(())
}
#[derive(Eq, PartialEq, Debug, Clone, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Literal(LiteralContent);
#[derive(PartialEq, Eq, Debug, Clone, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
enum LiteralContent {
String(String),
LanguageTaggedString {
value: String,
language: String,
},
#[cfg(feature = "rdf-12")]
DirectionalLanguageTaggedString {
value: String,
language: String,
direction: BaseDirection,
},
TypedLiteral {
value: String,
datatype: NamedNode,
},
}
impl Literal {
#[inline]
pub fn new_simple_literal(value: impl Into<String>) -> Self {
Self(LiteralContent::String(value.into()))
}
#[inline]
pub fn new(value: impl Into<String>) -> Self {
Self::new_simple_literal(value)
}
#[inline]
pub fn new_typed_literal(value: impl Into<String>, datatype: impl Into<NamedNode>) -> Self {
let value = value.into();
let datatype = datatype.into();
Self(if datatype == *xsd::STRING {
LiteralContent::String(value)
} else {
LiteralContent::TypedLiteral { value, datatype }
})
}
#[inline]
pub fn new_typed(value: impl Into<String>, datatype: NamedNode) -> Self {
Self::new_typed_literal(value, datatype)
}
pub fn new_typed_validated(
value: impl Into<String>,
datatype: NamedNode,
) -> Result<Self, OxirsError> {
let value = value.into();
validate_xsd_value(&value, datatype.as_str())?;
Ok(Literal::new_typed_literal(value, datatype))
}
#[inline]
pub fn new_language_tagged_literal(
value: impl Into<String>,
language: impl Into<String>,
) -> Result<Self, LanguageTagParseError> {
let language = language.into().to_ascii_lowercase();
validate_language_tag(&language)?;
Ok(Self::new_language_tagged_literal_unchecked(value, language))
}
#[inline]
pub fn new_language_tagged_literal_unchecked(
value: impl Into<String>,
language: impl Into<String>,
) -> Self {
Self(LiteralContent::LanguageTaggedString {
value: value.into(),
language: language.into(),
})
}
pub fn new_lang(
value: impl Into<String>,
language: impl Into<String>,
) -> Result<Self, OxirsError> {
let result = Self::new_language_tagged_literal(value, language)?;
Ok(result)
}
#[cfg(feature = "rdf-12")]
#[inline]
pub fn new_directional_language_tagged_literal(
value: impl Into<String>,
language: impl Into<String>,
direction: impl Into<BaseDirection>,
) -> Result<Self, LanguageTagParseError> {
let mut language = language.into();
language.make_ascii_lowercase();
validate_language_tag(&language)?;
Ok(Self::new_directional_language_tagged_literal_unchecked(
value, language, direction,
))
}
#[cfg(feature = "rdf-12")]
#[inline]
pub fn new_directional_language_tagged_literal_unchecked(
value: impl Into<String>,
language: impl Into<String>,
direction: impl Into<BaseDirection>,
) -> Self {
Self(LiteralContent::DirectionalLanguageTaggedString {
value: value.into(),
language: language.into(),
direction: direction.into(),
})
}
#[inline]
pub fn value(&self) -> &str {
self.as_ref().value()
}
#[inline]
pub fn language(&self) -> Option<&str> {
self.as_ref().language()
}
#[cfg(feature = "rdf-12")]
#[inline]
pub fn direction(&self) -> Option<BaseDirection> {
self.as_ref().direction()
}
#[inline]
pub fn datatype(&self) -> NamedNodeRef<'_> {
self.as_ref().datatype()
}
#[inline]
#[deprecated(note = "Plain literal concept is removed in RDF 1.1", since = "0.3.0")]
pub fn is_plain(&self) -> bool {
#[allow(deprecated)]
self.as_ref().is_plain()
}
pub fn is_lang_string(&self) -> bool {
self.language().is_some()
}
pub fn is_typed(&self) -> bool {
matches!(&self.0, LiteralContent::TypedLiteral { .. })
}
#[inline]
pub fn as_ref(&self) -> LiteralRef<'_> {
LiteralRef(match &self.0 {
LiteralContent::String(value) => LiteralRefContent::String(value),
LiteralContent::LanguageTaggedString { value, language } => {
LiteralRefContent::LanguageTaggedString { value, language }
}
#[cfg(feature = "rdf-12")]
LiteralContent::DirectionalLanguageTaggedString {
value,
language,
direction,
} => LiteralRefContent::DirectionalLanguageTaggedString {
value,
language,
direction: *direction,
},
LiteralContent::TypedLiteral { value, datatype } => LiteralRefContent::TypedLiteral {
value,
datatype: NamedNodeRef::new_unchecked(datatype.as_str()),
},
})
}
#[inline]
pub fn destruct(self) -> (String, Option<NamedNode>, Option<String>) {
match self.0 {
LiteralContent::String(s) => (s, None, None),
LiteralContent::LanguageTaggedString { value, language } => {
(value, None, Some(language))
}
#[cfg(feature = "rdf-12")]
LiteralContent::DirectionalLanguageTaggedString {
value,
language,
direction: _,
} => (value, None, Some(language)),
LiteralContent::TypedLiteral { value, datatype } => (value, Some(datatype), None),
}
}
pub fn as_bool(&self) -> Option<bool> {
match self.value().to_lowercase().as_str() {
"true" | "1" => Some(true),
"false" | "0" => Some(false),
_ => None,
}
}
pub fn as_i64(&self) -> Option<i64> {
self.value().parse().ok()
}
pub fn as_i32(&self) -> Option<i32> {
self.value().parse().ok()
}
pub fn as_f64(&self) -> Option<f64> {
self.value().parse().ok()
}
pub fn as_f32(&self) -> Option<f32> {
self.value().parse().ok()
}
pub fn is_numeric(&self) -> bool {
match &self.0 {
LiteralContent::TypedLiteral { datatype, .. } => {
let dt_iri = datatype.as_str();
matches!(
dt_iri,
"http://www.w3.org/2001/XMLSchema#integer"
| "http://www.w3.org/2001/XMLSchema#decimal"
| "http://www.w3.org/2001/XMLSchema#double"
| "http://www.w3.org/2001/XMLSchema#float"
| "http://www.w3.org/2001/XMLSchema#long"
| "http://www.w3.org/2001/XMLSchema#int"
| "http://www.w3.org/2001/XMLSchema#short"
| "http://www.w3.org/2001/XMLSchema#byte"
| "http://www.w3.org/2001/XMLSchema#unsignedLong"
| "http://www.w3.org/2001/XMLSchema#unsignedInt"
| "http://www.w3.org/2001/XMLSchema#unsignedShort"
| "http://www.w3.org/2001/XMLSchema#unsignedByte"
| "http://www.w3.org/2001/XMLSchema#positiveInteger"
| "http://www.w3.org/2001/XMLSchema#nonNegativeInteger"
| "http://www.w3.org/2001/XMLSchema#negativeInteger"
| "http://www.w3.org/2001/XMLSchema#nonPositiveInteger"
)
}
_ => {
self.as_f64().is_some()
}
}
}
pub fn is_boolean(&self) -> bool {
match &self.0 {
LiteralContent::TypedLiteral { datatype, .. } => {
datatype.as_str() == "http://www.w3.org/2001/XMLSchema#boolean"
}
_ => self.as_bool().is_some(),
}
}
pub fn canonical_form(&self) -> Literal {
match &self.0 {
LiteralContent::TypedLiteral { value, datatype } => {
let dt_iri = datatype.as_str();
match dt_iri {
"http://www.w3.org/2001/XMLSchema#boolean" => {
if let Some(bool_val) = self.as_bool() {
let canonical_value = if bool_val { "true" } else { "false" };
return Literal::new_typed(canonical_value, datatype.clone());
}
}
"http://www.w3.org/2001/XMLSchema#integer"
| "http://www.w3.org/2001/XMLSchema#long"
| "http://www.w3.org/2001/XMLSchema#int"
| "http://www.w3.org/2001/XMLSchema#short"
| "http://www.w3.org/2001/XMLSchema#byte" => {
if let Some(int_val) = self.as_i64() {
return Literal::new_typed(int_val.to_string(), datatype.clone());
}
}
"http://www.w3.org/2001/XMLSchema#unsignedLong"
| "http://www.w3.org/2001/XMLSchema#unsignedInt"
| "http://www.w3.org/2001/XMLSchema#unsignedShort"
| "http://www.w3.org/2001/XMLSchema#unsignedByte"
| "http://www.w3.org/2001/XMLSchema#positiveInteger"
| "http://www.w3.org/2001/XMLSchema#nonNegativeInteger" => {
if let Some(int_val) = self.as_i64() {
if int_val >= 0 {
return Literal::new_typed(int_val.to_string(), datatype.clone());
}
}
}
"http://www.w3.org/2001/XMLSchema#negativeInteger"
| "http://www.w3.org/2001/XMLSchema#nonPositiveInteger" => {
if let Some(int_val) = self.as_i64() {
if int_val <= 0 {
return Literal::new_typed(int_val.to_string(), datatype.clone());
}
}
}
"http://www.w3.org/2001/XMLSchema#decimal" => {
if let Some(dec_val) = self.as_f64() {
let formatted = format!("{dec_val}");
if formatted.contains('.') {
let trimmed = formatted.trim_end_matches('0').trim_end_matches('.');
return Literal::new_typed(
if trimmed.is_empty() || trimmed == "-" {
"0"
} else {
trimmed
},
datatype.clone(),
);
} else {
return Literal::new_typed(
format!("{formatted}.0"),
datatype.clone(),
);
}
}
}
"http://www.w3.org/2001/XMLSchema#double"
| "http://www.w3.org/2001/XMLSchema#float" => {
if let Some(float_val) = self.as_f64() {
if float_val.is_infinite() {
return Literal::new_typed(
if float_val.is_sign_positive() {
"INF"
} else {
"-INF"
},
datatype.clone(),
);
} else if float_val.is_nan() {
return Literal::new_typed("NaN", datatype.clone());
} else {
let formatted = if float_val.abs() >= 1e6
|| (float_val.abs() < 1e-3 && float_val != 0.0)
{
format!("{float_val:E}")
} else {
format!("{float_val}")
};
return Literal::new_typed(formatted, datatype.clone());
}
}
}
"http://www.w3.org/2001/XMLSchema#string"
| "http://www.w3.org/2001/XMLSchema#normalizedString" => {
if dt_iri == "http://www.w3.org/2001/XMLSchema#normalizedString" {
let normalized = value.replace(['\t', '\n', '\r'], " ");
return Literal::new_typed(normalized, datatype.clone());
}
}
"http://www.w3.org/2001/XMLSchema#token" => {
let normalized = value.split_whitespace().collect::<Vec<_>>().join(" ");
return Literal::new_typed(normalized, datatype.clone());
}
_ => {}
}
}
LiteralContent::LanguageTaggedString { value, language } => {
return Self(LiteralContent::LanguageTaggedString {
value: value.clone(),
language: language.clone(),
});
}
_ => {}
}
self.clone()
}
pub fn validate(&self) -> Result<(), OxirsError> {
match &self.0 {
LiteralContent::String(_) => Ok(()),
LiteralContent::LanguageTaggedString { language, .. } => {
validate_language_tag(language).map_err(Into::into)
}
#[cfg(feature = "rdf-12")]
LiteralContent::DirectionalLanguageTaggedString { language, .. } => {
validate_language_tag(language).map_err(Into::into)
}
LiteralContent::TypedLiteral { value, datatype } => {
validate_xsd_value(value, datatype.as_str())
}
}
}
}
impl fmt::Display for Literal {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.as_ref().fmt(f)
}
}
impl RdfTerm for Literal {
fn as_str(&self) -> &str {
self.value()
}
fn is_literal(&self) -> bool {
true
}
}
impl ObjectTerm for Literal {}
#[derive(Eq, PartialEq, Debug, Clone, Copy, Hash)]
pub struct LiteralRef<'a>(LiteralRefContent<'a>);
#[derive(PartialEq, Eq, Debug, Clone, Copy, Hash)]
enum LiteralRefContent<'a> {
String(&'a str),
LanguageTaggedString {
value: &'a str,
language: &'a str,
},
#[cfg(feature = "rdf-12")]
DirectionalLanguageTaggedString {
value: &'a str,
language: &'a str,
direction: BaseDirection,
},
TypedLiteral {
value: &'a str,
datatype: NamedNodeRef<'a>,
},
}
impl<'a> LiteralRef<'a> {
#[inline]
pub const fn new_simple_literal(value: &'a str) -> Self {
LiteralRef(LiteralRefContent::String(value))
}
#[inline]
pub const fn new(value: &'a str) -> Self {
Self::new_simple_literal(value)
}
#[inline]
pub fn new_typed_literal(value: &'a str, datatype: impl Into<NamedNodeRef<'a>>) -> Self {
let datatype = datatype.into();
LiteralRef(if datatype == xsd::STRING.as_ref() {
LiteralRefContent::String(value)
} else {
LiteralRefContent::TypedLiteral { value, datatype }
})
}
#[inline]
pub fn new_typed(value: &'a str, datatype: NamedNodeRef<'a>) -> Self {
Self::new_typed_literal(value, datatype)
}
#[inline]
pub const fn new_language_tagged_literal_unchecked(value: &'a str, language: &'a str) -> Self {
LiteralRef(LiteralRefContent::LanguageTaggedString { value, language })
}
#[inline]
pub const fn new_lang(value: &'a str, language: &'a str) -> Self {
Self::new_language_tagged_literal_unchecked(value, language)
}
#[cfg(feature = "rdf-12")]
#[inline]
pub const fn new_directional_language_tagged_literal_unchecked(
value: &'a str,
language: &'a str,
direction: BaseDirection,
) -> Self {
LiteralRef(LiteralRefContent::DirectionalLanguageTaggedString {
value,
language,
direction,
})
}
#[inline]
pub const fn value(self) -> &'a str {
match self.0 {
LiteralRefContent::String(value)
| LiteralRefContent::LanguageTaggedString { value, .. }
| LiteralRefContent::TypedLiteral { value, .. } => value,
#[cfg(feature = "rdf-12")]
LiteralRefContent::DirectionalLanguageTaggedString { value, .. } => value,
}
}
#[inline]
pub const fn language(self) -> Option<&'a str> {
match self.0 {
LiteralRefContent::LanguageTaggedString { language, .. } => Some(language),
#[cfg(feature = "rdf-12")]
LiteralRefContent::DirectionalLanguageTaggedString { language, .. } => Some(language),
_ => None,
}
}
#[cfg(feature = "rdf-12")]
#[inline]
pub const fn direction(self) -> Option<BaseDirection> {
match self.0 {
LiteralRefContent::DirectionalLanguageTaggedString { direction, .. } => Some(direction),
_ => None,
}
}
#[inline]
pub fn datatype(self) -> NamedNodeRef<'a> {
match self.0 {
LiteralRefContent::String(_) => xsd::STRING.as_ref(),
LiteralRefContent::LanguageTaggedString { .. } => rdf::LANG_STRING.as_ref(),
#[cfg(feature = "rdf-12")]
LiteralRefContent::DirectionalLanguageTaggedString { .. } => {
rdf::DIR_LANG_STRING.as_ref()
}
LiteralRefContent::TypedLiteral { datatype, .. } => datatype,
}
}
#[inline]
#[deprecated(note = "Plain literal concept is removed in RDF 1.1", since = "0.3.0")]
pub const fn is_plain(self) -> bool {
matches!(
self.0,
LiteralRefContent::String(_) | LiteralRefContent::LanguageTaggedString { .. }
)
}
#[inline]
pub fn into_owned(self) -> Literal {
Literal(match self.0 {
LiteralRefContent::String(value) => LiteralContent::String(value.to_owned()),
LiteralRefContent::LanguageTaggedString { value, language } => {
LiteralContent::LanguageTaggedString {
value: value.to_owned(),
language: language.to_owned(),
}
}
#[cfg(feature = "rdf-12")]
LiteralRefContent::DirectionalLanguageTaggedString {
value,
language,
direction,
} => LiteralContent::DirectionalLanguageTaggedString {
value: value.to_owned(),
language: language.to_owned(),
direction,
},
LiteralRefContent::TypedLiteral { value, datatype } => LiteralContent::TypedLiteral {
value: value.to_owned(),
datatype: datatype.into_owned(),
},
})
}
#[inline]
pub fn to_owned(&self) -> Literal {
self.into_owned()
}
}
impl fmt::Display for LiteralRef<'_> {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
LiteralRefContent::String(value) => print_quoted_str(value, f),
LiteralRefContent::LanguageTaggedString { value, language } => {
print_quoted_str(value, f)?;
write!(f, "@{language}")
}
#[cfg(feature = "rdf-12")]
LiteralRefContent::DirectionalLanguageTaggedString {
value,
language,
direction,
} => {
print_quoted_str(value, f)?;
write!(f, "@{language}--{direction}")
}
LiteralRefContent::TypedLiteral { value, datatype } => {
print_quoted_str(value, f)?;
write!(f, "^^{datatype}")
}
}
}
}
impl<'a> RdfTerm for LiteralRef<'a> {
fn as_str(&self) -> &str {
self.value()
}
fn is_literal(&self) -> bool {
true
}
}
#[inline]
pub fn print_quoted_str(string: &str, f: &mut impl Write) -> fmt::Result {
f.write_char('"')?;
for c in string.chars() {
match c {
'\u{08}' => f.write_str("\\b"),
'\t' => f.write_str("\\t"),
'\n' => f.write_str("\\n"),
'\u{0C}' => f.write_str("\\f"),
'\r' => f.write_str("\\r"),
'"' => f.write_str("\\\""),
'\\' => f.write_str("\\\\"),
'\0'..='\u{1F}' | '\u{7F}' => write!(f, "\\u{:04X}", u32::from(c)),
_ => f.write_char(c),
}?;
}
f.write_char('"')
}
#[cfg(feature = "rdf-12")]
#[derive(Eq, PartialEq, Debug, Clone, Copy, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum BaseDirection {
Ltr,
Rtl,
}
#[cfg(feature = "rdf-12")]
impl fmt::Display for BaseDirection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Ltr => "ltr",
Self::Rtl => "rtl",
})
}
}
impl<'a> From<&'a Literal> for LiteralRef<'a> {
#[inline]
fn from(node: &'a Literal) -> Self {
node.as_ref()
}
}
impl<'a> From<LiteralRef<'a>> for Literal {
#[inline]
fn from(node: LiteralRef<'a>) -> Self {
node.into_owned()
}
}
impl<'a> From<&'a str> for LiteralRef<'a> {
#[inline]
fn from(value: &'a str) -> Self {
LiteralRef(LiteralRefContent::String(value))
}
}
impl PartialEq<Literal> for LiteralRef<'_> {
#[inline]
fn eq(&self, other: &Literal) -> bool {
*self == other.as_ref()
}
}
impl PartialEq<LiteralRef<'_>> for Literal {
#[inline]
fn eq(&self, other: &LiteralRef<'_>) -> bool {
self.as_ref() == *other
}
}
impl<'a> From<&'a str> for Literal {
#[inline]
fn from(value: &'a str) -> Self {
Self(LiteralContent::String(value.into()))
}
}
impl From<String> for Literal {
#[inline]
fn from(value: String) -> Self {
Self(LiteralContent::String(value))
}
}
impl<'a> From<Cow<'a, str>> for Literal {
#[inline]
fn from(value: Cow<'a, str>) -> Self {
Self(LiteralContent::String(value.into()))
}
}
impl From<bool> for Literal {
#[inline]
fn from(value: bool) -> Self {
Self(LiteralContent::TypedLiteral {
value: value.to_string(),
datatype: xsd::BOOLEAN.clone(),
})
}
}
impl From<i128> for Literal {
#[inline]
fn from(value: i128) -> Self {
Self(LiteralContent::TypedLiteral {
value: value.to_string(),
datatype: xsd::INTEGER.clone(),
})
}
}
impl From<i64> for Literal {
#[inline]
fn from(value: i64) -> Self {
Self(LiteralContent::TypedLiteral {
value: value.to_string(),
datatype: xsd::INTEGER.clone(),
})
}
}
impl From<i32> for Literal {
#[inline]
fn from(value: i32) -> Self {
Self(LiteralContent::TypedLiteral {
value: value.to_string(),
datatype: xsd::INTEGER.clone(),
})
}
}
impl From<i16> for Literal {
#[inline]
fn from(value: i16) -> Self {
Self(LiteralContent::TypedLiteral {
value: value.to_string(),
datatype: xsd::INTEGER.clone(),
})
}
}
impl From<u64> for Literal {
#[inline]
fn from(value: u64) -> Self {
Self(LiteralContent::TypedLiteral {
value: value.to_string(),
datatype: xsd::INTEGER.clone(),
})
}
}
impl From<u32> for Literal {
#[inline]
fn from(value: u32) -> Self {
Self(LiteralContent::TypedLiteral {
value: value.to_string(),
datatype: xsd::INTEGER.clone(),
})
}
}
impl From<u16> for Literal {
#[inline]
fn from(value: u16) -> Self {
Self(LiteralContent::TypedLiteral {
value: value.to_string(),
datatype: xsd::INTEGER.clone(),
})
}
}
impl From<f32> for Literal {
#[inline]
fn from(value: f32) -> Self {
Self(LiteralContent::TypedLiteral {
value: if value == f32::INFINITY {
"INF".to_owned()
} else if value == f32::NEG_INFINITY {
"-INF".to_owned()
} else {
value.to_string()
},
datatype: xsd::FLOAT.clone(),
})
}
}
impl From<f64> for Literal {
#[inline]
fn from(value: f64) -> Self {
Self(LiteralContent::TypedLiteral {
value: if value == f64::INFINITY {
"INF".to_owned()
} else if value == f64::NEG_INFINITY {
"-INF".to_owned()
} else {
value.to_string()
},
datatype: xsd::DOUBLE.clone(),
})
}
}
pub mod xsd_literals {
use super::*;
use crate::vocab::xsd;
pub fn boolean_literal(value: bool) -> Literal {
Literal::new_typed(value.to_string(), xsd::BOOLEAN.clone())
}
pub fn integer_literal(value: i64) -> Literal {
Literal::new_typed(value.to_string(), xsd::INTEGER.clone())
}
pub fn decimal_literal(value: f64) -> Literal {
Literal::new_typed(value.to_string(), xsd::DECIMAL.clone())
}
pub fn double_literal(value: f64) -> Literal {
Literal::new_typed(value.to_string(), xsd::DOUBLE.clone())
}
pub fn string_literal(value: &str) -> Literal {
Literal::new_typed(value, xsd::STRING.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_literal_equality() {
assert_eq!(
Literal::new_simple_literal("foo"),
Literal::new_typed_literal("foo", xsd::STRING.clone())
);
assert_eq!(
Literal::new_simple_literal("foo"),
LiteralRef::new_typed_literal("foo", xsd::STRING.as_ref())
);
assert_eq!(
LiteralRef::new_simple_literal("foo"),
Literal::new_typed_literal("foo", xsd::STRING.clone())
);
assert_eq!(
LiteralRef::new_simple_literal("foo"),
LiteralRef::new_typed_literal("foo", xsd::STRING.as_ref())
);
}
#[test]
fn test_float_format() {
assert_eq!("INF", Literal::from(f32::INFINITY).value());
assert_eq!("INF", Literal::from(f64::INFINITY).value());
assert_eq!("-INF", Literal::from(f32::NEG_INFINITY).value());
assert_eq!("-INF", Literal::from(f64::NEG_INFINITY).value());
assert_eq!("NaN", Literal::from(f32::NAN).value());
assert_eq!("NaN", Literal::from(f64::NAN).value());
}
#[test]
fn test_plain_literal() {
let literal = Literal::new("Hello");
assert_eq!(literal.value(), "Hello");
#[allow(deprecated)]
{
assert!(literal.is_plain());
}
assert!(!literal.is_lang_string());
assert!(!literal.is_typed());
assert_eq!(format!("{literal}"), "\"Hello\"");
}
#[test]
fn test_lang_literal() {
let literal = Literal::new_lang("Hello", "en").expect("construction should succeed");
assert_eq!(literal.value(), "Hello");
assert_eq!(literal.language(), Some("en"));
#[allow(deprecated)]
{
assert!(literal.is_plain());
}
assert!(literal.is_lang_string());
assert!(!literal.is_typed());
assert_eq!(format!("{literal}"), "\"Hello\"@en");
}
#[test]
fn test_typed_literal() {
let literal = Literal::new_typed("42", xsd::INTEGER.clone());
assert_eq!(literal.value(), "42");
assert_eq!(
literal.datatype().as_str(),
"http://www.w3.org/2001/XMLSchema#integer"
);
#[allow(deprecated)]
{
assert!(!literal.is_plain());
}
assert!(!literal.is_lang_string());
assert!(literal.is_typed());
assert_eq!(
format!("{literal}"),
"\"42\"^^<http://www.w3.org/2001/XMLSchema#integer>"
);
}
#[test]
fn test_literal_ref() {
let literal_ref = LiteralRef::new("test");
assert_eq!(literal_ref.value(), "test");
let owned = literal_ref.to_owned();
assert_eq!(owned.value(), "test");
}
#[test]
fn test_boolean_extraction() {
let bool_literal = xsd_literals::boolean_literal(true);
assert!(bool_literal.is_boolean());
assert_eq!(bool_literal.as_bool(), Some(true));
let false_literal = Literal::new_typed("false", xsd::BOOLEAN.clone());
assert_eq!(false_literal.as_bool(), Some(false));
let true_str = Literal::new("true");
assert_eq!(true_str.as_bool(), Some(true));
let false_str = Literal::new("0");
assert_eq!(false_str.as_bool(), Some(false));
}
#[test]
fn test_numeric_extraction() {
let int_literal = xsd_literals::integer_literal(42);
assert!(int_literal.is_numeric());
assert_eq!(int_literal.as_i64(), Some(42));
assert_eq!(int_literal.as_i32(), Some(42));
assert_eq!(int_literal.as_f64(), Some(42.0));
let decimal_literal = xsd_literals::decimal_literal(3.25);
assert!(decimal_literal.is_numeric());
assert_eq!(decimal_literal.as_f64(), Some(3.25));
assert_eq!(decimal_literal.as_f32(), Some(3.25_f32));
let untyped_num = Literal::new("123");
assert!(untyped_num.is_numeric());
assert_eq!(untyped_num.as_i64(), Some(123));
}
#[test]
fn test_canonical_form() {
let bool_literal = Literal::new_typed("True", xsd::BOOLEAN.clone());
let canonical = bool_literal.canonical_form();
assert_eq!(canonical.value(), "true");
let int_literal = Literal::new_typed(" 42 ", xsd::INTEGER.clone());
let canonical = int_literal.canonical_form();
assert_eq!(
canonical.datatype().as_str(),
"http://www.w3.org/2001/XMLSchema#integer"
);
let dec_literal = Literal::new_typed("3.140", xsd::DECIMAL.clone());
let canonical = dec_literal.canonical_form();
assert_eq!(canonical.value(), "3.14"); }
#[test]
fn test_xsd_convenience_functions() {
assert_eq!(xsd_literals::boolean_literal(true).value(), "true");
assert_eq!(xsd_literals::integer_literal(123).value(), "123");
assert_eq!(xsd_literals::decimal_literal(3.25).value(), "3.25");
assert_eq!(xsd_literals::double_literal(2.71).value(), "2.71");
assert_eq!(xsd_literals::string_literal("hello").value(), "hello");
assert_eq!(
xsd_literals::boolean_literal(true).datatype().as_str(),
"http://www.w3.org/2001/XMLSchema#boolean"
);
assert_eq!(
xsd_literals::integer_literal(123).datatype().as_str(),
"http://www.w3.org/2001/XMLSchema#integer"
);
}
#[test]
fn test_numeric_type_detection() {
let int_lit = Literal::new_typed("42", xsd::INTEGER.clone());
assert!(int_lit.is_numeric());
let float_lit = Literal::new_typed("3.14", xsd::FLOAT.clone());
assert!(float_lit.is_numeric());
let double_lit = Literal::new_typed("2.71", xsd::DOUBLE.clone());
assert!(double_lit.is_numeric());
let string_lit = Literal::new_typed("hello", xsd::STRING.clone());
assert!(!string_lit.is_numeric());
let bool_lit = Literal::new_typed("true", xsd::BOOLEAN.clone());
assert!(!bool_lit.is_numeric());
}
}