iri_s 0.2.9

RDF data shapes implementation in Rust
Documentation
use crate::error::IriSError;
use oxiri::Iri;
use oxrdf::{NamedNode, NamedOrBlankNode, Term};
use serde::{Serialize, Serializer};
use std::fmt;
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use url::Url;
#[cfg(not(target_family = "wasm"))]
use {std::fs::canonicalize, std::path::Path};

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct IriS {
    iri: NamedNode,
}

impl IriS {
    /// Get the RDF type IRI
    pub fn rdf_type() -> IriS {
        IriS::new_unchecked("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")
    }

    /// Create an `IriS` from a string with an optional base IRI string
    pub fn from_str_base(str: &str, base: Option<&str>) -> Result<IriS, IriSError> {
        match base {
            Some(base_str) => IriS::from_str(base_str)?.resolve_str(str),
            None => IriS::from_str(str),
        }
    }

    /// Create an `IriS` from a string with an optional base IRI
    pub fn from_str_base_iri(str: &str, base_iri: Option<&IriS>) -> Result<IriS, IriSError> {
        match base_iri {
            Some(base_iri) => base_iri.resolve_str(str),
            None => IriS::from_str(str),
        }
    }

    /// Get the IRI as a `&str`
    pub fn as_str(&self) -> &str {
        self.iri.as_str()
    }

    /// Gets the [`NamedNode`] of an [`IriS`]
    pub fn named_node(&self) -> &NamedNode {
        &self.iri
    }

    /// Create an [`IriS`] from a `&str` without checking for possible syntactic errors
    pub fn new_unchecked(str: &str) -> IriS {
        let iri = NamedNode::new_unchecked(str);
        IriS { iri }
    }

    pub fn new(str: &str) -> Result<IriS, IriSError> {
        let iri = NamedNode::new(str).map_err(|e| IriSError::IriParseError {
            str: str.to_string(),
            error: e.to_string(),
        })?;
        Ok(IriS { iri })
    }

    /// Join a string to the current IRI
    pub fn join(self, str: &str) -> Result<Self, IriSError> {
        let url = Url::from_str(self.as_str()).map_err(|e| IriSError::IriParseError {
            str: str.to_string(),
            error: e.to_string(),
        })?;

        let joined = url.join(str).map_err(|e| IriSError::JoinError {
            str: str.to_string(),
            current: Box::new(self),
            error: e.to_string(),
        })?;

        Ok(IriS::new_unchecked(joined.as_str()))
    }

    /// Extends the current IRI with a new string
    ///
    /// This function checks for possible errors returning a `Result`
    pub fn extend(&self, str: &str) -> Result<Self, IriSError> {
        let current_str = self.iri.as_str();
        let extend_str = if current_str.ends_with("/") || current_str.ends_with("#") {
            format!("{current_str}{str}")
        } else {
            format!("{current_str}/{str}")
        };

        let iri = NamedNode::new(extend_str.as_str()).map_err(|e| IriSError::IriParseError {
            str: extend_str,
            error: e.to_string(),
        })?;

        Ok(IriS { iri })
    }

    /// Extend an IRI with a new string without checking for possible syntactic errors
    pub fn extend_unchecked(&self, str: &str) -> Self {
        let extended_str = format!("{}{}", self.iri.as_str(), str);
        let iri = NamedNode::new_unchecked(extended_str);

        IriS { iri }
    }

    /// Resolve the IRI `other` with this IRI
    pub fn resolve(&self, other: IriS) -> Result<Self, IriSError> {
        let resolved = self.resolve_iri(other.as_str())?;
        let iri = IriS::namednode_from_iri(resolved)?;
        Ok(IriS { iri })
    }

    /// Resolve `other` with this IRI
    pub fn resolve_str(&self, other: &str) -> Result<Self, IriSError> {
        let resolved = self.resolve_iri(other)?;
        let iri = IriS::namednode_from_iri(resolved)?;
        Ok(IriS { iri })
    }

    /// Resolve `other` with this IRI
    pub fn resolve_iri(&self, other: &str) -> Result<Iri<String>, IriSError> {
        let base = Iri::parse(self.as_str()).map_err(|e| IriSError::IriParseError {
            str: self.to_string(),
            error: e.to_string(),
        })?;

        base.resolve(other).map_err(|e| IriSError::IriResolveError {
            error: e.to_string(),
            base: Box::new(IriS::new_unchecked(base.as_str())),
            other: other.to_string(),
        })
    }

    /// Create a `NamedNode` from an IRI
    pub fn namednode_from_iri(iri: Iri<String>) -> Result<NamedNode, IriSError> {
        NamedNode::new(iri.as_str()).map_err(|e| IriSError::IriParseError {
            str: iri.as_str().to_string(),
            error: e.to_string(),
        })
    }

    pub fn parse_turtle(str: &str) -> Result<Self, IriSError> {
        let str = str.trim();
        if str.starts_with('<') && str.ends_with('>') {
            let inner_str = &str[1..str.len() - 1];
            IriS::new(inner_str)
        } else {
            Err(IriSError::TurtleParseError {
                str: str.to_string(),
                error: "IRI must be enclosed in angle brackets".to_string(),
            })
        }
    }

    /// [Dereference](https://www.w3.org/wiki/DereferenceURI) the IRI and get the content available from it.
    /// It handles also IRIs with the `file` scheme as local file names. For example: `file:///person.txt`
    #[cfg(not(target_family = "wasm"))]
    pub fn dereference(&self, base: Option<&IriS>) -> Result<String, IriSError> {
        use reqwest::blocking::Client;
        use reqwest::header;
        use reqwest::header::HeaderMap;
        use reqwest::header::USER_AGENT;
        use std::fs;

        let url = match base {
            Some(base_iri) => {
                let base = Url::from_str(base_iri.as_str()).map_err(|e| IriSError::UrlParseError {
                    str: base_iri.to_string(),
                    error: e.to_string(),
                })?;
                Url::options()
                    .base_url(Some(&base))
                    .parse(self.iri.as_str())
                    .map_err(|e| IriSError::IriParseErrorWithBase {
                        str: self.iri.as_str().to_string(),
                        base: format!("{base}"),
                        error: e.to_string(),
                    })?
            },
            None => Url::from_str(self.iri.as_str()).map_err(|e| IriSError::UrlParseError {
                str: self.iri.as_str().to_string(),
                error: e.to_string(),
            })?,
        };

        match url.scheme() {
            "file" => {
                let path = url
                    .to_file_path()
                    .map_err(|_| IriSError::ConvertingFileUrlToPath { url: url.to_string() })?;
                let path_name = path.to_string_lossy().to_string();
                let body = fs::read_to_string(path).map_err(|e| IriSError::IOErrorFile {
                    path: path_name,
                    url: url.to_string(),
                    error: e.to_string(),
                })?;
                Ok(body)
            },
            _ => {
                let mut headers = HeaderMap::new();
                /* TODO: Add a parameter with the Accept header ?
                headers.insert(
                    ACCEPT,
                    header::HeaderValue::from_static(""),
                );*/
                headers.insert(USER_AGENT, header::HeaderValue::from_static("rudof"));
                let client = Client::builder()
                    .default_headers(headers)
                    .build()
                    .map_err(|e| IriSError::ReqwestClientCreation { error: e.to_string() })?;
                let res = client
                    .get(url)
                    .send()
                    .map_err(|e| IriSError::ReqwestError { error: e.to_string() })?
                    .text()
                    .map_err(|e| IriSError::ReqwestTextError { error: e.to_string() })?;
                Ok(res)
            },
        }
    }

    #[cfg(target_family = "wasm")]
    pub fn dereference(&self, _base: Option<&IriS>) -> Result<String, IriSError> {
        Err(IriSError::ReqwestClientCreation {
            error: String::from("rewquest is not enabled"),
        })
    }
}

impl Display for IriS {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.iri.as_str())
    }
}

impl Serialize for IriS {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(self.iri.as_str())
    }
}

impl From<NamedNode> for IriS {
    fn from(iri: NamedNode) -> Self {
        IriS { iri }
    }
}

impl From<Url> for IriS {
    fn from(value: Url) -> Self {
        IriS {
            iri: NamedNode::new_unchecked(value),
        }
    }
}

#[cfg(not(target_family = "wasm"))]
impl TryFrom<&Path> for IriS {
    type Error = IriSError;

    fn try_from(value: &Path) -> Result<Self, Self::Error> {
        let abs_path = if value.is_absolute() {
            Ok(value.to_path_buf())
        } else {
            canonicalize(value).map_err(|e| IriSError::ConvertingPathToIri {
                path: value.to_string_lossy().to_string(),
                error: e.to_string(),
            })
        }?;

        let url = Url::from_file_path(&abs_path).map_err(|_| IriSError::ConvertingPathToIri {
            path: abs_path.to_string_lossy().to_string(),
            error: String::from("Cannot convert path to file URL"),
        })?;

        let iri = NamedNode::new(url.as_str()).map_err(|e| IriSError::IriParseError {
            str: url.as_str().to_string(),
            error: e.to_string(),
        })?;

        Ok(IriS { iri })
    }
}

impl FromStr for IriS {
    type Err = IriSError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let iri = NamedNode::new(s).map_err(|e| IriSError::IriParseError {
            str: s.to_string(),
            error: e.to_string(),
        })?;
        Ok(IriS { iri })
    }
}

impl From<IriS> for NamedNode {
    fn from(iri: IriS) -> Self {
        NamedNode::new_unchecked(iri.as_str())
    }
}

impl From<IriS> for NamedOrBlankNode {
    fn from(value: IriS) -> Self {
        let named_node: NamedNode = value.into();
        named_node.into()
    }
}

impl From<IriS> for Term {
    fn from(value: IriS) -> Self {
        let named_node: NamedNode = value.into();
        named_node.into()
    }
}

impl Default for IriS {
    fn default() -> Self {
        IriS::new_unchecked(&String::default())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_iri_creation() {
        let iri = IriS::new("http://example.org/test").unwrap();
        assert_eq!(iri.as_str(), "http://example.org/test");
    }

    #[test]
    fn test_iri_join() {
        let base = IriS::new("http://example.org/").unwrap();
        let joined = base.join("test").unwrap();
        assert_eq!(joined.as_str(), "http://example.org/test");
    }

    #[test]
    fn test_iri_extend() {
        let base = IriS::new("http://example.org").unwrap();
        let extended = base.extend("test").unwrap();
        assert_eq!(extended.as_str(), "http://example.org/test");
    }

    #[test]
    fn test_iri_resolve() {
        let base = IriS::new("http://example.org/").unwrap();
        let resolved = base.resolve_str("test").unwrap();
        assert_eq!(resolved.as_str(), "http://example.org/test");
    }

    #[test]
    fn parse_iri_turtle() {
        let str = "<http://example.org/test>";
        let iri = IriS::parse_turtle(str).unwrap();
        assert_eq!(iri.as_str(), "http://example.org/test");
    }
}