1use crate::model::{GraphNameTerm, ObjectTerm, PredicateTerm, RdfTerm, SubjectTerm};
7use crate::OxirsError;
8use std::fmt;
9use std::hash::Hash;
10use std::str::FromStr;
11
12pub use oxiri::{Iri, IriParseError};
14
15impl From<IriParseError> for OxirsError {
17 fn from(err: IriParseError) -> Self {
18 OxirsError::Parse(format!("IRI parse error: {err}"))
19 }
20}
21
22#[derive(Eq, PartialEq, Ord, PartialOrd, Debug, Clone, Hash)]
34pub struct NamedNode {
35 iri: String,
36}
37
38impl NamedNode {
39 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 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 #[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#[derive(Eq, PartialEq, Ord, PartialOrd, Debug, Clone, Copy, Hash)]
139pub struct NamedNodeRef<'a> {
140 iri: &'a str,
141}
142
143impl<'a> NamedNodeRef<'a> {
144 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 #[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
238impl 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
269impl 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#[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
313fn normalize_iri(iri: &str) -> Result<String, OxirsError> {
315 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 result = normalize_percent_encoding(&result);
348
349 if result.contains("./") {
351 result = normalize_path_in_iri(&result);
352 }
353
354 Ok(result)
355}
356
357fn normalize_path_in_iri(iri: &str) -> String {
359 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 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 let normalized_path = normalize_path_segments(path_part);
382
383 format!("{prefix}{normalized_path}{query_fragment}")
384}
385
386fn 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 continue;
400 }
401 ".." => {
402 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
416fn 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}