Skip to main content

oxirs_core/model/
iri.rs

1//! IRI and Named Node implementations for RDF
2//!
3//! This module provides high-performance IRI and Named Node implementations
4//! based on oxrdf patterns with oxiri for fast validation.
5
6use crate::model::{GraphNameTerm, ObjectTerm, PredicateTerm, RdfTerm, SubjectTerm};
7use crate::OxirsError;
8use std::fmt;
9use std::hash::Hash;
10use std::str::FromStr;
11
12// Import and re-export oxiri types for compatibility
13pub use oxiri::{Iri, IriParseError};
14
15/// Convert IriParseError to OxirsError
16impl From<IriParseError> for OxirsError {
17    fn from(err: IriParseError) -> Self {
18        OxirsError::Parse(format!("IRI parse error: {err}"))
19    }
20}
21
22/// An owned RDF [IRI](https://www.w3.org/TR/rdf11-concepts/#dfn-iri).
23///
24/// The default string formatter is returning an N-Triples, Turtle, and SPARQL compatible representation:
25/// ```
26/// use oxirs_core::model::NamedNode;
27///
28/// assert_eq!(
29///     "<http://example.com/foo>",
30///     NamedNode::new("http://example.com/foo").unwrap().to_string()
31/// );
32/// ```
33#[derive(Eq, PartialEq, Ord, PartialOrd, Debug, Clone, Hash)]
34pub struct NamedNode {
35    iri: String,
36}
37
38impl NamedNode {
39    /// Builds and validate an RDF [IRI](https://www.w3.org/TR/rdf11-concepts/#dfn-iri).
40    pub fn new(iri: impl Into<String>) -> Result<Self, OxirsError> {
41        Ok(Self::new_from_iri(Iri::parse(iri.into())?))
42    }
43
44    #[inline]
45    pub(crate) fn new_from_iri(iri: Iri<String>) -> Self {
46        Self::new_unchecked(iri.into_inner())
47    }
48
49    /// Builds an RDF [IRI](https://www.w3.org/TR/rdf11-concepts/#dfn-iri) from a string with normalization.
50    ///
51    /// This applies IRI normalization before validation.
52    pub fn new_normalized(iri: impl Into<String>) -> Result<Self, OxirsError> {
53        let iri_str = iri.into();
54        let normalized = normalize_iri(&iri_str)?;
55        Ok(Self::new_from_iri(Iri::parse(normalized)?))
56    }
57
58    /// Builds an RDF [IRI](https://www.w3.org/TR/rdf11-concepts/#dfn-iri) from a string.
59    ///
60    /// It is the caller's responsibility to ensure that `iri` is a valid IRI.
61    ///
62    /// [`NamedNode::new()`] is a safe version of this constructor and should be used for untrusted data.
63    #[inline]
64    pub fn new_unchecked(iri: impl Into<String>) -> Self {
65        Self { iri: iri.into() }
66    }
67
68    #[inline]
69    pub fn as_str(&self) -> &str {
70        self.iri.as_str()
71    }
72
73    #[inline]
74    pub fn into_string(self) -> String {
75        self.iri
76    }
77
78    #[inline]
79    pub fn as_ref(&self) -> NamedNodeRef<'_> {
80        NamedNodeRef::new_unchecked(&self.iri)
81    }
82}
83
84impl fmt::Display for NamedNode {
85    #[inline]
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        self.as_ref().fmt(f)
88    }
89}
90
91impl PartialEq<str> for NamedNode {
92    #[inline]
93    fn eq(&self, other: &str) -> bool {
94        self.as_str() == other
95    }
96}
97
98impl PartialEq<NamedNode> for str {
99    #[inline]
100    fn eq(&self, other: &NamedNode) -> bool {
101        self == other.as_str()
102    }
103}
104
105impl PartialEq<&str> for NamedNode {
106    #[inline]
107    fn eq(&self, other: &&str) -> bool {
108        self == *other
109    }
110}
111
112impl PartialEq<NamedNode> for &str {
113    #[inline]
114    fn eq(&self, other: &NamedNode) -> bool {
115        *self == other
116    }
117}
118
119impl FromStr for NamedNode {
120    type Err = OxirsError;
121
122    fn from_str(s: &str) -> Result<Self, Self::Err> {
123        Self::new(s)
124    }
125}
126
127/// A borrowed RDF [IRI](https://www.w3.org/TR/rdf11-concepts/#dfn-iri).
128///
129/// The default string formatter is returning an N-Triples, Turtle, and SPARQL compatible representation:
130/// ```
131/// use oxirs_core::model::NamedNodeRef;
132///
133/// assert_eq!(
134///     "<http://example.com/foo>",
135///     NamedNodeRef::new("http://example.com/foo").unwrap().to_string()
136/// );
137/// ```
138#[derive(Eq, PartialEq, Ord, PartialOrd, Debug, Clone, Copy, Hash)]
139pub struct NamedNodeRef<'a> {
140    iri: &'a str,
141}
142
143impl<'a> NamedNodeRef<'a> {
144    /// Builds and validate an RDF [IRI](https://www.w3.org/TR/rdf11-concepts/#dfn-iri)
145    pub fn new(iri: &'a str) -> Result<Self, OxirsError> {
146        Ok(Self::new_from_iri(Iri::parse(iri)?))
147    }
148
149    #[inline]
150    pub(crate) fn new_from_iri(iri: Iri<&'a str>) -> Self {
151        Self::new_unchecked(iri.into_inner())
152    }
153
154    /// Builds an RDF [IRI](https://www.w3.org/TR/rdf11-concepts/#dfn-iri) from a string.
155    ///
156    /// It is the caller's responsibility to ensure that `iri` is a valid IRI.
157    ///
158    /// [`NamedNodeRef::new()`] is a safe version of this constructor and should be used for untrusted data.
159    #[inline]
160    pub const fn new_unchecked(iri: &'a str) -> Self {
161        Self { iri }
162    }
163
164    #[inline]
165    pub const fn as_str(self) -> &'a str {
166        self.iri
167    }
168
169    #[inline]
170    pub fn into_owned(self) -> NamedNode {
171        NamedNode::new_unchecked(self.iri)
172    }
173}
174
175impl fmt::Display for NamedNodeRef<'_> {
176    #[inline]
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        write!(f, "<{}>", self.as_str())
179    }
180}
181
182impl PartialEq<str> for NamedNodeRef<'_> {
183    #[inline]
184    fn eq(&self, other: &str) -> bool {
185        self.as_str() == other
186    }
187}
188
189impl PartialEq<NamedNodeRef<'_>> for str {
190    #[inline]
191    fn eq(&self, other: &NamedNodeRef<'_>) -> bool {
192        self == other.as_str()
193    }
194}
195
196impl PartialEq<&str> for NamedNodeRef<'_> {
197    #[inline]
198    fn eq(&self, other: &&str) -> bool {
199        self.as_str() == *other
200    }
201}
202
203impl PartialEq<NamedNodeRef<'_>> for &str {
204    #[inline]
205    fn eq(&self, other: &NamedNodeRef<'_>) -> bool {
206        *self == other.as_str()
207    }
208}
209
210impl PartialEq<NamedNode> for NamedNodeRef<'_> {
211    #[inline]
212    fn eq(&self, other: &NamedNode) -> bool {
213        self.as_str() == other.as_str()
214    }
215}
216
217impl PartialEq<NamedNodeRef<'_>> for NamedNode {
218    #[inline]
219    fn eq(&self, other: &NamedNodeRef<'_>) -> bool {
220        self.as_str() == other.as_str()
221    }
222}
223
224impl<'a> From<NamedNodeRef<'a>> for NamedNode {
225    #[inline]
226    fn from(node: NamedNodeRef<'a>) -> Self {
227        node.into_owned()
228    }
229}
230
231impl<'a> From<&'a NamedNode> for NamedNodeRef<'a> {
232    #[inline]
233    fn from(node: &'a NamedNode) -> Self {
234        node.as_ref()
235    }
236}
237
238// Implement RDF term traits
239impl RdfTerm for NamedNode {
240    fn as_str(&self) -> &str {
241        self.as_str()
242    }
243
244    fn is_named_node(&self) -> bool {
245        true
246    }
247}
248
249impl RdfTerm for NamedNodeRef<'_> {
250    fn as_str(&self) -> &str {
251        self.iri
252    }
253
254    fn is_named_node(&self) -> bool {
255        true
256    }
257}
258
259impl SubjectTerm for NamedNode {}
260impl PredicateTerm for NamedNode {}
261impl ObjectTerm for NamedNode {}
262impl GraphNameTerm for NamedNode {}
263
264impl SubjectTerm for NamedNodeRef<'_> {}
265impl PredicateTerm for NamedNodeRef<'_> {}
266impl ObjectTerm for NamedNodeRef<'_> {}
267impl GraphNameTerm for NamedNodeRef<'_> {}
268
269// Implement conversions from NamedNodeRef to union types
270impl From<NamedNodeRef<'_>> for crate::model::Subject {
271    #[inline]
272    fn from(node: NamedNodeRef<'_>) -> Self {
273        crate::model::Subject::NamedNode(node.into_owned())
274    }
275}
276
277impl From<NamedNodeRef<'_>> for crate::model::Predicate {
278    #[inline]
279    fn from(node: NamedNodeRef<'_>) -> Self {
280        crate::model::Predicate::NamedNode(node.into_owned())
281    }
282}
283
284impl From<NamedNodeRef<'_>> for crate::model::Object {
285    #[inline]
286    fn from(node: NamedNodeRef<'_>) -> Self {
287        crate::model::Object::NamedNode(node.into_owned())
288    }
289}
290
291// Serialization support
292#[cfg(feature = "serde")]
293impl serde::Serialize for NamedNode {
294    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
295    where
296        S: serde::Serializer,
297    {
298        serializer.serialize_str(&self.iri)
299    }
300}
301
302#[cfg(feature = "serde")]
303impl<'de> serde::Deserialize<'de> for NamedNode {
304    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
305    where
306        D: serde::Deserializer<'de>,
307    {
308        let iri = String::deserialize(deserializer)?;
309        Self::new(iri).map_err(serde::de::Error::custom)
310    }
311}
312
313/// Normalize an IRI string according to RFC 3987 and RDF specifications
314fn normalize_iri(iri: &str) -> Result<String, OxirsError> {
315    // Simple approach: normalize scheme and host to lowercase
316    let mut result = String::new();
317    let mut in_scheme = true;
318    let mut in_authority = false;
319    let mut after_scheme = false;
320    let mut slash_count = 0;
321
322    for ch in iri.chars() {
323        if in_scheme && ch == ':' {
324            result.push(ch.to_ascii_lowercase());
325            in_scheme = false;
326            after_scheme = true;
327        } else if in_scheme {
328            result.push(ch.to_ascii_lowercase());
329        } else if after_scheme && ch == '/' {
330            result.push(ch);
331            slash_count += 1;
332            if slash_count == 2 {
333                in_authority = true;
334                after_scheme = false;
335            }
336        } else if in_authority && (ch == '/' || ch == '?' || ch == '#') {
337            result.push(ch);
338            in_authority = false;
339        } else if in_authority {
340            result.push(ch.to_ascii_lowercase());
341        } else {
342            result.push(ch);
343        }
344    }
345
346    // Normalize percent encoding
347    result = normalize_percent_encoding(&result);
348
349    // Normalize path
350    if result.contains("./") {
351        result = normalize_path_in_iri(&result);
352    }
353
354    Ok(result)
355}
356
357/// Normalize path segments in full IRI by resolving . and .. segments
358fn normalize_path_in_iri(iri: &str) -> String {
359    // Find the path part of the IRI
360    let (prefix, path_and_after) = if let Some(pos) = iri.find("://") {
361        if let Some(path_start) = iri[pos + 3..].find('/') {
362            let path_start_abs = pos + 3 + path_start;
363            iri.split_at(path_start_abs)
364        } else {
365            return iri.to_string();
366        }
367    } else {
368        return iri.to_string();
369    };
370
371    // Split path from query/fragment
372    let (path_part, query_fragment) = if let Some(query_pos) = path_and_after.find('?') {
373        path_and_after.split_at(query_pos)
374    } else if let Some(fragment_pos) = path_and_after.find('#') {
375        path_and_after.split_at(fragment_pos)
376    } else {
377        (path_and_after, "")
378    };
379
380    // Normalize the path part
381    let normalized_path = normalize_path_segments(path_part);
382
383    format!("{prefix}{normalized_path}{query_fragment}")
384}
385
386/// Normalize path segments by resolving . and .. segments
387fn normalize_path_segments(path: &str) -> String {
388    if path.is_empty() {
389        return String::new();
390    }
391
392    let segments: Vec<&str> = path.split('/').collect();
393    let mut normalized_segments = Vec::new();
394
395    for segment in segments {
396        match segment {
397            "." => {
398                // Skip current directory
399                continue;
400            }
401            ".." => {
402                // Go up one directory
403                if !normalized_segments.is_empty() && normalized_segments.last() != Some(&"") {
404                    normalized_segments.pop();
405                }
406            }
407            _ => {
408                normalized_segments.push(segment);
409            }
410        }
411    }
412
413    normalized_segments.join("/")
414}
415
416/// Normalize percent encoding to uppercase
417fn normalize_percent_encoding(s: &str) -> String {
418    let mut result = String::new();
419    let mut chars = s.chars().peekable();
420
421    while let Some(ch) = chars.next() {
422        if ch == '%' {
423            if let (Some(hex1), Some(hex2)) = (chars.next(), chars.next()) {
424                result.push('%');
425                result.push(hex1.to_ascii_uppercase());
426                result.push(hex2.to_ascii_uppercase());
427            } else {
428                result.push(ch);
429            }
430        } else {
431            result.push(ch);
432        }
433    }
434
435    result
436}
437
438#[cfg(test)]
439mod tests {
440    use super::*;
441
442    #[test]
443    fn test_named_node_creation() {
444        let node = NamedNode::new("http://example.com/test").unwrap();
445        assert_eq!(node.as_str(), "http://example.com/test");
446        assert_eq!(node.to_string(), "<http://example.com/test>");
447    }
448
449    #[test]
450    fn test_named_node_ref() {
451        let node = NamedNode::new("http://example.com/test").unwrap();
452        let node_ref = node.as_ref();
453        assert_eq!(node_ref.as_str(), "http://example.com/test");
454        assert_eq!(node_ref.to_string(), "<http://example.com/test>");
455    }
456
457    #[test]
458    fn test_named_node_comparison() {
459        let node = NamedNode::new("http://example.com/test").unwrap();
460        assert_eq!(node, "http://example.com/test");
461        assert_eq!("http://example.com/test", node);
462    }
463
464    #[test]
465    fn test_invalid_iri() {
466        assert!(NamedNode::new("not a valid iri").is_err());
467        assert!(NamedNode::new("").is_err());
468    }
469
470    #[test]
471    fn test_owned_borrowed_conversion() {
472        let owned = NamedNode::new("http://example.com/test").unwrap();
473        let borrowed = owned.as_ref();
474        let owned_again = borrowed.into_owned();
475        assert_eq!(owned, owned_again);
476    }
477
478    #[test]
479    fn test_rdf_term_trait() {
480        let node = NamedNode::new("http://example.com/test").unwrap();
481        assert!(node.is_named_node());
482        assert!(!node.is_blank_node());
483        assert!(!node.is_literal());
484    }
485}