Skip to main content

iri_s/iri/
iris.rs

1use crate::error::IriSError;
2use oxiri::Iri;
3use oxrdf::{NamedNode, NamedOrBlankNode, Term};
4use serde::{Serialize, Serializer};
5use std::fmt;
6use std::fmt::{Display, Formatter};
7use std::str::FromStr;
8use url::Url;
9#[cfg(not(target_family = "wasm"))]
10use {std::fs::canonicalize, std::path::Path};
11
12#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub struct IriS {
14    iri: NamedNode,
15}
16
17impl IriS {
18    /// Get the RDF type IRI
19    pub fn rdf_type() -> IriS {
20        IriS::new_unchecked("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")
21    }
22
23    /// Create an `IriS` from a string with an optional base IRI string
24    pub fn from_str_base(str: &str, base: Option<&str>) -> Result<IriS, IriSError> {
25        match base {
26            Some(base_str) => IriS::from_str(base_str)?.resolve_str(str),
27            None => IriS::from_str(str),
28        }
29    }
30
31    /// Create an `IriS` from a string with an optional base IRI
32    pub fn from_str_base_iri(str: &str, base_iri: Option<&IriS>) -> Result<IriS, IriSError> {
33        match base_iri {
34            Some(base_iri) => base_iri.resolve_str(str),
35            None => IriS::from_str(str),
36        }
37    }
38
39    /// Get the IRI as a `&str`
40    pub fn as_str(&self) -> &str {
41        self.iri.as_str()
42    }
43
44    /// Gets the [`NamedNode`] of an [`IriS`]
45    pub fn named_node(&self) -> &NamedNode {
46        &self.iri
47    }
48
49    /// Create an [`IriS`] from a `&str` without checking for possible syntactic errors
50    pub fn new_unchecked(str: &str) -> IriS {
51        let iri = NamedNode::new_unchecked(str);
52        IriS { iri }
53    }
54
55    pub fn new(str: &str) -> Result<IriS, IriSError> {
56        let iri = NamedNode::new(str).map_err(|e| IriSError::IriParseError {
57            str: str.to_string(),
58            error: e.to_string(),
59        })?;
60        Ok(IriS { iri })
61    }
62
63    /// Join a string to the current IRI
64    pub fn join(self, str: &str) -> Result<Self, IriSError> {
65        let url = Url::from_str(self.as_str()).map_err(|e| IriSError::IriParseError {
66            str: str.to_string(),
67            error: e.to_string(),
68        })?;
69
70        let joined = url.join(str).map_err(|e| IriSError::JoinError {
71            str: str.to_string(),
72            current: Box::new(self),
73            error: e.to_string(),
74        })?;
75
76        Ok(IriS::new_unchecked(joined.as_str()))
77    }
78
79    /// Extends the current IRI with a new string
80    ///
81    /// This function checks for possible errors returning a `Result`
82    pub fn extend(&self, str: &str) -> Result<Self, IriSError> {
83        let current_str = self.iri.as_str();
84        let extend_str = if current_str.ends_with("/") || current_str.ends_with("#") {
85            format!("{current_str}{str}")
86        } else {
87            format!("{current_str}/{str}")
88        };
89
90        let iri = NamedNode::new(extend_str.as_str()).map_err(|e| IriSError::IriParseError {
91            str: extend_str,
92            error: e.to_string(),
93        })?;
94
95        Ok(IriS { iri })
96    }
97
98    /// Extend an IRI with a new string without checking for possible syntactic errors
99    pub fn extend_unchecked(&self, str: &str) -> Self {
100        let extended_str = format!("{}{}", self.iri.as_str(), str);
101        let iri = NamedNode::new_unchecked(extended_str);
102
103        IriS { iri }
104    }
105
106    /// Resolve the IRI `other` with this IRI
107    pub fn resolve(&self, other: IriS) -> Result<Self, IriSError> {
108        let resolved = self.resolve_iri(other.as_str())?;
109        let iri = IriS::namednode_from_iri(resolved)?;
110        Ok(IriS { iri })
111    }
112
113    /// Resolve `other` with this IRI
114    pub fn resolve_str(&self, other: &str) -> Result<Self, IriSError> {
115        let resolved = self.resolve_iri(other)?;
116        let iri = IriS::namednode_from_iri(resolved)?;
117        Ok(IriS { iri })
118    }
119
120    /// Resolve `other` with this IRI
121    pub fn resolve_iri(&self, other: &str) -> Result<Iri<String>, IriSError> {
122        let base = Iri::parse(self.as_str()).map_err(|e| IriSError::IriParseError {
123            str: self.to_string(),
124            error: e.to_string(),
125        })?;
126
127        base.resolve(other).map_err(|e| IriSError::IriResolveError {
128            error: e.to_string(),
129            base: Box::new(IriS::new_unchecked(base.as_str())),
130            other: other.to_string(),
131        })
132    }
133
134    /// Create a `NamedNode` from an IRI
135    pub fn namednode_from_iri(iri: Iri<String>) -> Result<NamedNode, IriSError> {
136        NamedNode::new(iri.as_str()).map_err(|e| IriSError::IriParseError {
137            str: iri.as_str().to_string(),
138            error: e.to_string(),
139        })
140    }
141
142    pub fn parse_turtle(str: &str) -> Result<Self, IriSError> {
143        let str = str.trim();
144        if str.starts_with('<') && str.ends_with('>') {
145            let inner_str = &str[1..str.len() - 1];
146            IriS::new(inner_str)
147        } else {
148            Err(IriSError::TurtleParseError {
149                str: str.to_string(),
150                error: "IRI must be enclosed in angle brackets".to_string(),
151            })
152        }
153    }
154
155    /// [Dereference](https://www.w3.org/wiki/DereferenceURI) the IRI and get the content available from it.
156    /// It handles also IRIs with the `file` scheme as local file names. For example: `file:///person.txt`
157    #[cfg(not(target_family = "wasm"))]
158    pub fn dereference(&self, base: Option<&IriS>) -> Result<String, IriSError> {
159        use reqwest::blocking::Client;
160        use reqwest::header;
161        use reqwest::header::HeaderMap;
162        use reqwest::header::USER_AGENT;
163        use std::fs;
164
165        let url = match base {
166            Some(base_iri) => {
167                let base = Url::from_str(base_iri.as_str()).map_err(|e| IriSError::UrlParseError {
168                    str: base_iri.to_string(),
169                    error: e.to_string(),
170                })?;
171                Url::options()
172                    .base_url(Some(&base))
173                    .parse(self.iri.as_str())
174                    .map_err(|e| IriSError::IriParseErrorWithBase {
175                        str: self.iri.as_str().to_string(),
176                        base: format!("{base}"),
177                        error: e.to_string(),
178                    })?
179            },
180            None => Url::from_str(self.iri.as_str()).map_err(|e| IriSError::UrlParseError {
181                str: self.iri.as_str().to_string(),
182                error: e.to_string(),
183            })?,
184        };
185
186        match url.scheme() {
187            "file" => {
188                let path = url
189                    .to_file_path()
190                    .map_err(|_| IriSError::ConvertingFileUrlToPath { url: url.to_string() })?;
191                let path_name = path.to_string_lossy().to_string();
192                let body = fs::read_to_string(path).map_err(|e| IriSError::IOErrorFile {
193                    path: path_name,
194                    url: url.to_string(),
195                    error: e.to_string(),
196                })?;
197                Ok(body)
198            },
199            _ => {
200                let mut headers = HeaderMap::new();
201                /* TODO: Add a parameter with the Accept header ?
202                headers.insert(
203                    ACCEPT,
204                    header::HeaderValue::from_static(""),
205                );*/
206                headers.insert(USER_AGENT, header::HeaderValue::from_static("rudof"));
207                let client = Client::builder()
208                    .default_headers(headers)
209                    .build()
210                    .map_err(|e| IriSError::ReqwestClientCreation { error: e.to_string() })?;
211                let res = client
212                    .get(url)
213                    .send()
214                    .map_err(|e| IriSError::ReqwestError { error: e.to_string() })?
215                    .text()
216                    .map_err(|e| IriSError::ReqwestTextError { error: e.to_string() })?;
217                Ok(res)
218            },
219        }
220    }
221
222    #[cfg(target_family = "wasm")]
223    pub fn dereference(&self, _base: Option<&IriS>) -> Result<String, IriSError> {
224        Err(IriSError::ReqwestClientCreation {
225            error: String::from("rewquest is not enabled"),
226        })
227    }
228}
229
230impl Display for IriS {
231    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
232        write!(f, "{}", self.iri.as_str())
233    }
234}
235
236impl Serialize for IriS {
237    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
238    where
239        S: Serializer,
240    {
241        serializer.serialize_str(self.iri.as_str())
242    }
243}
244
245impl From<NamedNode> for IriS {
246    fn from(iri: NamedNode) -> Self {
247        IriS { iri }
248    }
249}
250
251impl From<Url> for IriS {
252    fn from(value: Url) -> Self {
253        IriS {
254            iri: NamedNode::new_unchecked(value),
255        }
256    }
257}
258
259#[cfg(not(target_family = "wasm"))]
260impl TryFrom<&Path> for IriS {
261    type Error = IriSError;
262
263    fn try_from(value: &Path) -> Result<Self, Self::Error> {
264        let abs_path = if value.is_absolute() {
265            Ok(value.to_path_buf())
266        } else {
267            canonicalize(value).map_err(|e| IriSError::ConvertingPathToIri {
268                path: value.to_string_lossy().to_string(),
269                error: e.to_string(),
270            })
271        }?;
272
273        let url = Url::from_file_path(&abs_path).map_err(|_| IriSError::ConvertingPathToIri {
274            path: abs_path.to_string_lossy().to_string(),
275            error: String::from("Cannot convert path to file URL"),
276        })?;
277
278        let iri = NamedNode::new(url.as_str()).map_err(|e| IriSError::IriParseError {
279            str: url.as_str().to_string(),
280            error: e.to_string(),
281        })?;
282
283        Ok(IriS { iri })
284    }
285}
286
287impl FromStr for IriS {
288    type Err = IriSError;
289
290    fn from_str(s: &str) -> Result<Self, Self::Err> {
291        let iri = NamedNode::new(s).map_err(|e| IriSError::IriParseError {
292            str: s.to_string(),
293            error: e.to_string(),
294        })?;
295        Ok(IriS { iri })
296    }
297}
298
299impl From<IriS> for NamedNode {
300    fn from(iri: IriS) -> Self {
301        NamedNode::new_unchecked(iri.as_str())
302    }
303}
304
305impl From<IriS> for NamedOrBlankNode {
306    fn from(value: IriS) -> Self {
307        let named_node: NamedNode = value.into();
308        named_node.into()
309    }
310}
311
312impl From<IriS> for Term {
313    fn from(value: IriS) -> Self {
314        let named_node: NamedNode = value.into();
315        named_node.into()
316    }
317}
318
319impl Default for IriS {
320    fn default() -> Self {
321        IriS::new_unchecked(&String::default())
322    }
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328
329    #[test]
330    fn test_iri_creation() {
331        let iri = IriS::new("http://example.org/test").unwrap();
332        assert_eq!(iri.as_str(), "http://example.org/test");
333    }
334
335    #[test]
336    fn test_iri_join() {
337        let base = IriS::new("http://example.org/").unwrap();
338        let joined = base.join("test").unwrap();
339        assert_eq!(joined.as_str(), "http://example.org/test");
340    }
341
342    #[test]
343    fn test_iri_extend() {
344        let base = IriS::new("http://example.org").unwrap();
345        let extended = base.extend("test").unwrap();
346        assert_eq!(extended.as_str(), "http://example.org/test");
347    }
348
349    #[test]
350    fn test_iri_resolve() {
351        let base = IriS::new("http://example.org/").unwrap();
352        let resolved = base.resolve_str("test").unwrap();
353        assert_eq!(resolved.as_str(), "http://example.org/test");
354    }
355
356    #[test]
357    fn parse_iri_turtle() {
358        let str = "<http://example.org/test>";
359        let iri = IriS::parse_turtle(str).unwrap();
360        assert_eq!(iri.as_str(), "http://example.org/test");
361    }
362}