1use crate::model::{NamedNode, NamedNodeRef, ObjectTerm, RdfTerm};
7use crate::vocab::{rdf, xsd};
8use crate::OxirsError;
9use oxilangtag::LanguageTag as OxiLanguageTag;
10use oxsdatatypes::{Boolean, Date, DateTime, Decimal, Double, Float, Integer, Time};
11use std::borrow::Cow;
12use std::fmt::{self, Write};
13use std::hash::Hash;
14use std::str::FromStr;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct LanguageTagParseError {
19 message: String,
20}
21
22impl fmt::Display for LanguageTagParseError {
23 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24 write!(f, "Language tag parse error: {}", self.message)
25 }
26}
27
28impl std::error::Error for LanguageTagParseError {}
29
30impl From<LanguageTagParseError> for OxirsError {
31 fn from(err: LanguageTagParseError) -> Self {
32 OxirsError::Parse(err.message)
33 }
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Hash)]
38pub struct LanguageTag {
39 tag: String,
40}
41
42impl LanguageTag {
43 pub fn parse(tag: impl Into<String>) -> Result<Self, LanguageTagParseError> {
45 let tag = tag.into();
46 validate_language_tag(&tag)?;
47 Ok(LanguageTag { tag })
48 }
49
50 pub fn as_str(&self) -> &str {
52 &self.tag
53 }
54
55 pub fn into_inner(self) -> String {
57 self.tag
58 }
59}
60
61impl fmt::Display for LanguageTag {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 f.write_str(&self.tag)
64 }
65}
66
67fn validate_language_tag(tag: &str) -> Result<(), LanguageTagParseError> {
69 OxiLanguageTag::parse(tag)
70 .map(|_| ())
71 .map_err(|e| LanguageTagParseError {
72 message: format!("Invalid language tag '{tag}': {e}"),
73 })
74}
75
76pub fn validate_xsd_value(value: &str, datatype_iri: &str) -> Result<(), OxirsError> {
78 match datatype_iri {
79 "http://www.w3.org/2001/XMLSchema#string"
81 | "http://www.w3.org/2001/XMLSchema#normalizedString"
82 | "http://www.w3.org/2001/XMLSchema#token" => {
83 Ok(())
85 }
86
87 "http://www.w3.org/2001/XMLSchema#boolean" => Boolean::from_str(value)
89 .map(|_| ())
90 .map_err(|e| OxirsError::Parse(format!("Invalid boolean value '{value}': {e}"))),
91
92 "http://www.w3.org/2001/XMLSchema#integer"
94 | "http://www.w3.org/2001/XMLSchema#long"
95 | "http://www.w3.org/2001/XMLSchema#int"
96 | "http://www.w3.org/2001/XMLSchema#short"
97 | "http://www.w3.org/2001/XMLSchema#byte"
98 | "http://www.w3.org/2001/XMLSchema#unsignedLong"
99 | "http://www.w3.org/2001/XMLSchema#unsignedInt"
100 | "http://www.w3.org/2001/XMLSchema#unsignedShort"
101 | "http://www.w3.org/2001/XMLSchema#unsignedByte"
102 | "http://www.w3.org/2001/XMLSchema#positiveInteger"
103 | "http://www.w3.org/2001/XMLSchema#nonNegativeInteger"
104 | "http://www.w3.org/2001/XMLSchema#negativeInteger"
105 | "http://www.w3.org/2001/XMLSchema#nonPositiveInteger" => Integer::from_str(value)
106 .map_err(|e| OxirsError::Parse(format!("Invalid integer value '{value}': {e}")))
107 .and_then(|integer| validate_integer_range_oxs(integer, datatype_iri)),
108
109 "http://www.w3.org/2001/XMLSchema#decimal" => Decimal::from_str(value)
111 .map(|_| ())
112 .map_err(|e| OxirsError::Parse(format!("Invalid decimal value '{value}': {e}"))),
113
114 "http://www.w3.org/2001/XMLSchema#float" => Float::from_str(value)
116 .map(|_| ())
117 .map_err(|e| OxirsError::Parse(format!("Invalid float value '{value}': {e}"))),
118 "http://www.w3.org/2001/XMLSchema#double" => Double::from_str(value)
119 .map(|_| ())
120 .map_err(|e| OxirsError::Parse(format!("Invalid double value '{value}': {e}"))),
121
122 "http://www.w3.org/2001/XMLSchema#dateTime" => DateTime::from_str(value)
124 .map(|_| ())
125 .map_err(|e| OxirsError::Parse(format!("Invalid dateTime value '{value}': {e}"))),
126
127 "http://www.w3.org/2001/XMLSchema#date" => Date::from_str(value)
128 .map(|_| ())
129 .map_err(|e| OxirsError::Parse(format!("Invalid date value '{value}': {e}"))),
130
131 "http://www.w3.org/2001/XMLSchema#time" => Time::from_str(value)
132 .map(|_| ())
133 .map_err(|e| OxirsError::Parse(format!("Invalid time value '{value}': {e}"))),
134
135 _ => Ok(()),
137 }
138}
139
140#[allow(dead_code)]
142fn validate_integer_range(value: &str, datatype_iri: &str) -> Result<(), OxirsError> {
143 let parsed_value: i64 = value
144 .parse()
145 .map_err(|_| OxirsError::Parse(format!("Cannot parse integer: '{value}'")))?;
146
147 match datatype_iri {
148 "http://www.w3.org/2001/XMLSchema#byte" if !(-128..=127).contains(&parsed_value) => {
149 return Err(OxirsError::Parse(format!(
150 "Byte value out of range: {parsed_value}. Must be between -128 and 127"
151 )));
152 }
153 "http://www.w3.org/2001/XMLSchema#short" if !(-32768..=32767).contains(&parsed_value) => {
154 return Err(OxirsError::Parse(format!(
155 "Short value out of range: {parsed_value}. Must be between -32768 and 32767"
156 )));
157 }
158 "http://www.w3.org/2001/XMLSchema#int"
159 if !(-2147483648..=2147483647).contains(&parsed_value) =>
160 {
161 return Err(OxirsError::Parse(format!(
162 "Int value out of range: {parsed_value}. Must be between -2147483648 and 2147483647"
163 )));
164 }
165 "http://www.w3.org/2001/XMLSchema#unsignedByte" if !(0..=255).contains(&parsed_value) => {
166 return Err(OxirsError::Parse(format!(
167 "Unsigned byte value out of range: {parsed_value}. Must be between 0 and 255"
168 )));
169 }
170 "http://www.w3.org/2001/XMLSchema#unsignedShort"
171 if !(0..=65535).contains(&parsed_value) =>
172 {
173 return Err(OxirsError::Parse(format!(
174 "Unsigned short value out of range: {parsed_value}. Must be between 0 and 65535"
175 )));
176 }
177 "http://www.w3.org/2001/XMLSchema#unsignedInt"
178 if !(0..=4294967295).contains(&parsed_value) =>
179 {
180 return Err(OxirsError::Parse(format!(
181 "Unsigned int value out of range: {parsed_value}. Must be between 0 and 4294967295"
182 )));
183 }
184 "http://www.w3.org/2001/XMLSchema#positiveInteger" if parsed_value <= 0 => {
185 return Err(OxirsError::Parse(format!(
186 "Positive integer must be greater than 0, got: {parsed_value}"
187 )));
188 }
189 "http://www.w3.org/2001/XMLSchema#nonNegativeInteger" if parsed_value < 0 => {
190 return Err(OxirsError::Parse(format!(
191 "Non-negative integer must be >= 0, got: {parsed_value}"
192 )));
193 }
194 "http://www.w3.org/2001/XMLSchema#negativeInteger" if parsed_value >= 0 => {
195 return Err(OxirsError::Parse(format!(
196 "Negative integer must be less than 0, got: {parsed_value}"
197 )));
198 }
199 "http://www.w3.org/2001/XMLSchema#nonPositiveInteger" if parsed_value > 0 => {
200 return Err(OxirsError::Parse(format!(
201 "Non-positive integer must be <= 0, got: {parsed_value}"
202 )));
203 }
204 _ => {} }
206
207 Ok(())
208}
209
210fn validate_integer_range_oxs(integer: Integer, datatype_iri: &str) -> Result<(), OxirsError> {
212 let parsed_value: i64 = integer.to_string().parse().map_err(|_| {
214 OxirsError::Parse("Cannot convert integer to i64 for range validation".to_string())
215 })?;
216
217 match datatype_iri {
218 "http://www.w3.org/2001/XMLSchema#byte" if !(-128..=127).contains(&parsed_value) => {
219 return Err(OxirsError::Parse(format!(
220 "Byte value out of range: {parsed_value}. Must be between -128 and 127"
221 )));
222 }
223 "http://www.w3.org/2001/XMLSchema#short" if !(-32768..=32767).contains(&parsed_value) => {
224 return Err(OxirsError::Parse(format!(
225 "Short value out of range: {parsed_value}. Must be between -32768 and 32767"
226 )));
227 }
228 "http://www.w3.org/2001/XMLSchema#int"
229 if !(-2147483648..=2147483647).contains(&parsed_value) =>
230 {
231 return Err(OxirsError::Parse(format!(
232 "Int value out of range: {parsed_value}. Must be between -2147483648 and 2147483647"
233 )));
234 }
235 "http://www.w3.org/2001/XMLSchema#unsignedByte" if !(0..=255).contains(&parsed_value) => {
236 return Err(OxirsError::Parse(format!(
237 "Unsigned byte value out of range: {parsed_value}. Must be between 0 and 255"
238 )));
239 }
240 "http://www.w3.org/2001/XMLSchema#unsignedShort"
241 if !(0..=65535).contains(&parsed_value) =>
242 {
243 return Err(OxirsError::Parse(format!(
244 "Unsigned short value out of range: {parsed_value}. Must be between 0 and 65535"
245 )));
246 }
247 "http://www.w3.org/2001/XMLSchema#unsignedInt"
248 if !(0..=4294967295).contains(&parsed_value) =>
249 {
250 return Err(OxirsError::Parse(format!(
251 "Unsigned int value out of range: {parsed_value}. Must be between 0 and 4294967295"
252 )));
253 }
254 "http://www.w3.org/2001/XMLSchema#positiveInteger" if parsed_value <= 0 => {
255 return Err(OxirsError::Parse(format!(
256 "Positive integer must be greater than 0, got: {parsed_value}"
257 )));
258 }
259 "http://www.w3.org/2001/XMLSchema#nonNegativeInteger" if parsed_value < 0 => {
260 return Err(OxirsError::Parse(format!(
261 "Non-negative integer must be >= 0, got: {parsed_value}"
262 )));
263 }
264 "http://www.w3.org/2001/XMLSchema#negativeInteger" if parsed_value >= 0 => {
265 return Err(OxirsError::Parse(format!(
266 "Negative integer must be less than 0, got: {parsed_value}"
267 )));
268 }
269 "http://www.w3.org/2001/XMLSchema#nonPositiveInteger" if parsed_value > 0 => {
270 return Err(OxirsError::Parse(format!(
271 "Non-positive integer must be <= 0, got: {parsed_value}"
272 )));
273 }
274 _ => {} }
276
277 Ok(())
278}
279
280#[derive(Eq, PartialEq, Debug, Clone, Hash, PartialOrd, Ord)]
303#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
304pub struct Literal(LiteralContent);
305
306#[derive(Debug, Clone)]
307#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
308enum LiteralContent {
309 String(String),
310 LanguageTaggedString {
311 value: String,
312 language: String,
313 },
314 #[cfg(feature = "rdf-12")]
315 DirectionalLanguageTaggedString {
316 value: String,
317 language: String,
318 direction: BaseDirection,
319 },
320 TypedLiteral {
321 value: String,
322 datatype: NamedNode,
323 },
324}
325
326fn literal_content_variant_rank(content: &LiteralContent) -> u8 {
330 match content {
331 LiteralContent::String(_) => 0,
332 LiteralContent::LanguageTaggedString { .. } => 1,
333 #[cfg(feature = "rdf-12")]
334 LiteralContent::DirectionalLanguageTaggedString { .. } => 2,
335 LiteralContent::TypedLiteral { .. } => 3,
336 }
337}
338
339impl PartialEq for LiteralContent {
350 fn eq(&self, other: &Self) -> bool {
351 match (self, other) {
352 (LiteralContent::String(a), LiteralContent::String(b)) => a == b,
353 (
354 LiteralContent::LanguageTaggedString {
355 value: v1,
356 language: l1,
357 },
358 LiteralContent::LanguageTaggedString {
359 value: v2,
360 language: l2,
361 },
362 ) => v1 == v2 && l1.eq_ignore_ascii_case(l2),
363 #[cfg(feature = "rdf-12")]
364 (
365 LiteralContent::DirectionalLanguageTaggedString {
366 value: v1,
367 language: l1,
368 direction: d1,
369 },
370 LiteralContent::DirectionalLanguageTaggedString {
371 value: v2,
372 language: l2,
373 direction: d2,
374 },
375 ) => v1 == v2 && l1.eq_ignore_ascii_case(l2) && d1 == d2,
376 (
377 LiteralContent::TypedLiteral {
378 value: v1,
379 datatype: d1,
380 },
381 LiteralContent::TypedLiteral {
382 value: v2,
383 datatype: d2,
384 },
385 ) => v1 == v2 && d1 == d2,
386 _ => false,
387 }
388 }
389}
390
391impl Eq for LiteralContent {}
392
393impl std::hash::Hash for LiteralContent {
394 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
395 literal_content_variant_rank(self).hash(state);
396 match self {
397 LiteralContent::String(value) => value.hash(state),
398 LiteralContent::LanguageTaggedString { value, language } => {
399 value.hash(state);
400 for b in language.bytes() {
403 b.to_ascii_lowercase().hash(state);
404 }
405 }
406 #[cfg(feature = "rdf-12")]
407 LiteralContent::DirectionalLanguageTaggedString {
408 value,
409 language,
410 direction,
411 } => {
412 value.hash(state);
413 for b in language.bytes() {
414 b.to_ascii_lowercase().hash(state);
415 }
416 direction.hash(state);
417 }
418 LiteralContent::TypedLiteral { value, datatype } => {
419 value.hash(state);
420 datatype.hash(state);
421 }
422 }
423 }
424}
425
426impl PartialOrd for LiteralContent {
427 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
428 Some(self.cmp(other))
429 }
430}
431
432impl Ord for LiteralContent {
433 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
434 match (self, other) {
435 (LiteralContent::String(a), LiteralContent::String(b)) => a.cmp(b),
436 (
437 LiteralContent::LanguageTaggedString {
438 value: v1,
439 language: l1,
440 },
441 LiteralContent::LanguageTaggedString {
442 value: v2,
443 language: l2,
444 },
445 ) => v1
446 .cmp(v2)
447 .then_with(|| l1.to_ascii_lowercase().cmp(&l2.to_ascii_lowercase())),
448 #[cfg(feature = "rdf-12")]
449 (
450 LiteralContent::DirectionalLanguageTaggedString {
451 value: v1,
452 language: l1,
453 direction: d1,
454 },
455 LiteralContent::DirectionalLanguageTaggedString {
456 value: v2,
457 language: l2,
458 direction: d2,
459 },
460 ) => v1
461 .cmp(v2)
462 .then_with(|| l1.to_ascii_lowercase().cmp(&l2.to_ascii_lowercase()))
463 .then_with(|| d1.cmp(d2)),
464 (
465 LiteralContent::TypedLiteral {
466 value: v1,
467 datatype: d1,
468 },
469 LiteralContent::TypedLiteral {
470 value: v2,
471 datatype: d2,
472 },
473 ) => v1.cmp(v2).then_with(|| d1.cmp(d2)),
474 _ => literal_content_variant_rank(self).cmp(&literal_content_variant_rank(other)),
475 }
476 }
477}
478
479impl Literal {
480 #[inline]
482 pub fn new_simple_literal(value: impl Into<String>) -> Self {
483 Self(LiteralContent::String(value.into()))
484 }
485
486 #[inline]
488 pub fn new(value: impl Into<String>) -> Self {
489 Self::new_simple_literal(value)
490 }
491
492 #[inline]
494 pub fn new_typed_literal(value: impl Into<String>, datatype: impl Into<NamedNode>) -> Self {
495 let value = value.into();
496 let datatype = datatype.into();
497 Self(if datatype == *xsd::STRING {
498 LiteralContent::String(value)
499 } else {
500 LiteralContent::TypedLiteral { value, datatype }
501 })
502 }
503
504 #[inline]
506 pub fn new_typed(value: impl Into<String>, datatype: NamedNode) -> Self {
507 Self::new_typed_literal(value, datatype)
508 }
509
510 pub fn new_typed_validated(
512 value: impl Into<String>,
513 datatype: NamedNode,
514 ) -> Result<Self, OxirsError> {
515 let value = value.into();
516 validate_xsd_value(&value, datatype.as_str())?;
517 Ok(Literal::new_typed_literal(value, datatype))
518 }
519
520 #[inline]
522 pub fn new_language_tagged_literal(
523 value: impl Into<String>,
524 language: impl Into<String>,
525 ) -> Result<Self, LanguageTagParseError> {
526 let language = language.into();
527 validate_language_tag(&language)?;
535 Ok(Self::new_language_tagged_literal_unchecked(value, language))
536 }
537
538 #[inline]
546 pub fn new_language_tagged_literal_unchecked(
547 value: impl Into<String>,
548 language: impl Into<String>,
549 ) -> Self {
550 Self(LiteralContent::LanguageTaggedString {
551 value: value.into(),
552 language: language.into(),
553 })
554 }
555
556 pub fn new_lang(
558 value: impl Into<String>,
559 language: impl Into<String>,
560 ) -> Result<Self, OxirsError> {
561 let result = Self::new_language_tagged_literal(value, language)?;
562 Ok(result)
563 }
564
565 #[cfg(feature = "rdf-12")]
567 #[inline]
568 pub fn new_directional_language_tagged_literal(
569 value: impl Into<String>,
570 language: impl Into<String>,
571 direction: impl Into<BaseDirection>,
572 ) -> Result<Self, LanguageTagParseError> {
573 let language = language.into();
577 validate_language_tag(&language)?;
578 Ok(Self::new_directional_language_tagged_literal_unchecked(
579 value, language, direction,
580 ))
581 }
582
583 #[cfg(feature = "rdf-12")]
591 #[inline]
592 pub fn new_directional_language_tagged_literal_unchecked(
593 value: impl Into<String>,
594 language: impl Into<String>,
595 direction: impl Into<BaseDirection>,
596 ) -> Self {
597 Self(LiteralContent::DirectionalLanguageTaggedString {
598 value: value.into(),
599 language: language.into(),
600 direction: direction.into(),
601 })
602 }
603
604 #[inline]
606 pub fn value(&self) -> &str {
607 self.as_ref().value()
608 }
609
610 #[inline]
615 pub fn language(&self) -> Option<&str> {
616 self.as_ref().language()
617 }
618
619 #[cfg(feature = "rdf-12")]
623 #[inline]
624 pub fn direction(&self) -> Option<BaseDirection> {
625 self.as_ref().direction()
626 }
627
628 #[inline]
633 pub fn datatype(&self) -> NamedNodeRef<'_> {
634 self.as_ref().datatype()
635 }
636
637 #[inline]
642 #[deprecated(note = "Plain literal concept is removed in RDF 1.1", since = "0.3.0")]
643 pub fn is_plain(&self) -> bool {
644 #[allow(deprecated)]
645 self.as_ref().is_plain()
646 }
647
648 pub fn is_lang_string(&self) -> bool {
650 self.language().is_some()
651 }
652
653 pub fn is_typed(&self) -> bool {
655 matches!(&self.0, LiteralContent::TypedLiteral { .. })
656 }
657
658 #[inline]
659 pub fn as_ref(&self) -> LiteralRef<'_> {
660 LiteralRef(match &self.0 {
661 LiteralContent::String(value) => LiteralRefContent::String(value),
662 LiteralContent::LanguageTaggedString { value, language } => {
663 LiteralRefContent::LanguageTaggedString { value, language }
664 }
665 #[cfg(feature = "rdf-12")]
666 LiteralContent::DirectionalLanguageTaggedString {
667 value,
668 language,
669 direction,
670 } => LiteralRefContent::DirectionalLanguageTaggedString {
671 value,
672 language,
673 direction: *direction,
674 },
675 LiteralContent::TypedLiteral { value, datatype } => LiteralRefContent::TypedLiteral {
676 value,
677 datatype: NamedNodeRef::new_unchecked(datatype.as_str()),
678 },
679 })
680 }
681
682 #[inline]
684 pub fn destruct(self) -> (String, Option<NamedNode>, Option<String>) {
685 match self.0 {
686 LiteralContent::String(s) => (s, None, None),
687 LiteralContent::LanguageTaggedString { value, language } => {
688 (value, None, Some(language))
689 }
690 #[cfg(feature = "rdf-12")]
691 LiteralContent::DirectionalLanguageTaggedString {
692 value,
693 language,
694 direction: _,
695 } => (value, None, Some(language)),
696 LiteralContent::TypedLiteral { value, datatype } => (value, Some(datatype), None),
697 }
698 }
699
700 pub fn as_bool(&self) -> Option<bool> {
704 match self.value().to_lowercase().as_str() {
705 "true" | "1" => Some(true),
706 "false" | "0" => Some(false),
707 _ => None,
708 }
709 }
710
711 pub fn as_i64(&self) -> Option<i64> {
715 self.value().parse().ok()
716 }
717
718 pub fn as_i32(&self) -> Option<i32> {
720 self.value().parse().ok()
721 }
722
723 pub fn as_f64(&self) -> Option<f64> {
727 self.value().parse().ok()
728 }
729
730 pub fn as_f32(&self) -> Option<f32> {
732 self.value().parse().ok()
733 }
734
735 pub fn is_numeric(&self) -> bool {
737 match &self.0 {
738 LiteralContent::TypedLiteral { datatype, .. } => {
739 let dt_iri = datatype.as_str();
740 matches!(
741 dt_iri,
742 "http://www.w3.org/2001/XMLSchema#integer"
743 | "http://www.w3.org/2001/XMLSchema#decimal"
744 | "http://www.w3.org/2001/XMLSchema#double"
745 | "http://www.w3.org/2001/XMLSchema#float"
746 | "http://www.w3.org/2001/XMLSchema#long"
747 | "http://www.w3.org/2001/XMLSchema#int"
748 | "http://www.w3.org/2001/XMLSchema#short"
749 | "http://www.w3.org/2001/XMLSchema#byte"
750 | "http://www.w3.org/2001/XMLSchema#unsignedLong"
751 | "http://www.w3.org/2001/XMLSchema#unsignedInt"
752 | "http://www.w3.org/2001/XMLSchema#unsignedShort"
753 | "http://www.w3.org/2001/XMLSchema#unsignedByte"
754 | "http://www.w3.org/2001/XMLSchema#positiveInteger"
755 | "http://www.w3.org/2001/XMLSchema#nonNegativeInteger"
756 | "http://www.w3.org/2001/XMLSchema#negativeInteger"
757 | "http://www.w3.org/2001/XMLSchema#nonPositiveInteger"
758 )
759 }
760 _ => {
761 self.as_f64().is_some()
763 }
764 }
765 }
766
767 pub fn is_boolean(&self) -> bool {
769 match &self.0 {
770 LiteralContent::TypedLiteral { datatype, .. } => {
771 datatype.as_str() == "http://www.w3.org/2001/XMLSchema#boolean"
772 }
773 _ => self.as_bool().is_some(),
774 }
775 }
776
777 pub fn canonical_form(&self) -> Literal {
781 match &self.0 {
782 LiteralContent::TypedLiteral { value, datatype } => {
783 let dt_iri = datatype.as_str();
784 match dt_iri {
785 "http://www.w3.org/2001/XMLSchema#boolean" => {
786 if let Some(bool_val) = self.as_bool() {
787 let canonical_value = if bool_val { "true" } else { "false" };
788 return Literal::new_typed(canonical_value, datatype.clone());
789 }
790 }
791 "http://www.w3.org/2001/XMLSchema#integer"
792 | "http://www.w3.org/2001/XMLSchema#long"
793 | "http://www.w3.org/2001/XMLSchema#int"
794 | "http://www.w3.org/2001/XMLSchema#short"
795 | "http://www.w3.org/2001/XMLSchema#byte" => {
796 if let Some(int_val) = self.as_i64() {
797 return Literal::new_typed(int_val.to_string(), datatype.clone());
798 }
799 }
800 "http://www.w3.org/2001/XMLSchema#unsignedLong"
801 | "http://www.w3.org/2001/XMLSchema#unsignedInt"
802 | "http://www.w3.org/2001/XMLSchema#unsignedShort"
803 | "http://www.w3.org/2001/XMLSchema#unsignedByte"
804 | "http://www.w3.org/2001/XMLSchema#positiveInteger"
805 | "http://www.w3.org/2001/XMLSchema#nonNegativeInteger" => {
806 if let Some(int_val) = self.as_i64() {
807 if int_val >= 0 {
808 return Literal::new_typed(int_val.to_string(), datatype.clone());
809 }
810 }
811 }
812 "http://www.w3.org/2001/XMLSchema#negativeInteger"
813 | "http://www.w3.org/2001/XMLSchema#nonPositiveInteger" => {
814 if let Some(int_val) = self.as_i64() {
815 if int_val <= 0 {
816 return Literal::new_typed(int_val.to_string(), datatype.clone());
817 }
818 }
819 }
820 "http://www.w3.org/2001/XMLSchema#decimal" => {
821 if let Some(dec_val) = self.as_f64() {
822 let formatted = format!("{dec_val}");
824 if formatted.contains('.') {
825 let trimmed = formatted.trim_end_matches('0').trim_end_matches('.');
826 return Literal::new_typed(
827 if trimmed.is_empty() || trimmed == "-" {
828 "0"
829 } else {
830 trimmed
831 },
832 datatype.clone(),
833 );
834 } else {
835 return Literal::new_typed(
836 format!("{formatted}.0"),
837 datatype.clone(),
838 );
839 }
840 }
841 }
842 "http://www.w3.org/2001/XMLSchema#double"
843 | "http://www.w3.org/2001/XMLSchema#float" => {
844 if let Some(float_val) = self.as_f64() {
845 if float_val.is_infinite() {
847 return Literal::new_typed(
848 if float_val.is_sign_positive() {
849 "INF"
850 } else {
851 "-INF"
852 },
853 datatype.clone(),
854 );
855 } else if float_val.is_nan() {
856 return Literal::new_typed("NaN", datatype.clone());
857 } else {
858 let formatted = if float_val.abs() >= 1e6
860 || (float_val.abs() < 1e-3 && float_val != 0.0)
861 {
862 format!("{float_val:E}")
863 } else {
864 format!("{float_val}")
865 };
866 return Literal::new_typed(formatted, datatype.clone());
867 }
868 }
869 }
870 "http://www.w3.org/2001/XMLSchema#normalizedString" => {
871 let normalized = value.replace(['\t', '\n', '\r'], " ");
873 return Literal::new_typed(normalized, datatype.clone());
874 }
875 "http://www.w3.org/2001/XMLSchema#string" => {
876 }
878 "http://www.w3.org/2001/XMLSchema#token" => {
879 let normalized = value.split_whitespace().collect::<Vec<_>>().join(" ");
881 return Literal::new_typed(normalized, datatype.clone());
882 }
883 _ => {}
884 }
885 }
886 LiteralContent::LanguageTaggedString { value, language } => {
887 return Self(LiteralContent::LanguageTaggedString {
889 value: value.clone(),
890 language: language.clone(),
891 });
892 }
893 _ => {}
894 }
895 self.clone()
896 }
897
898 pub fn validate(&self) -> Result<(), OxirsError> {
900 match &self.0 {
901 LiteralContent::String(_) => Ok(()),
902 LiteralContent::LanguageTaggedString { language, .. } => {
903 validate_language_tag(language).map_err(Into::into)
904 }
905 #[cfg(feature = "rdf-12")]
906 LiteralContent::DirectionalLanguageTaggedString { language, .. } => {
907 validate_language_tag(language).map_err(Into::into)
908 }
909 LiteralContent::TypedLiteral { value, datatype } => {
910 validate_xsd_value(value, datatype.as_str())
911 }
912 }
913 }
914}
915
916impl fmt::Display for Literal {
917 #[inline]
918 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
919 self.as_ref().fmt(f)
920 }
921}
922
923impl RdfTerm for Literal {
924 fn as_str(&self) -> &str {
925 self.value()
926 }
927
928 fn is_literal(&self) -> bool {
929 true
930 }
931}
932
933impl ObjectTerm for Literal {}
934
935#[derive(Eq, PartialEq, Debug, Clone, Copy, Hash)]
953pub struct LiteralRef<'a>(LiteralRefContent<'a>);
954
955#[derive(Debug, Clone, Copy)]
956enum LiteralRefContent<'a> {
957 String(&'a str),
958 LanguageTaggedString {
959 value: &'a str,
960 language: &'a str,
961 },
962 #[cfg(feature = "rdf-12")]
963 DirectionalLanguageTaggedString {
964 value: &'a str,
965 language: &'a str,
966 direction: BaseDirection,
967 },
968 TypedLiteral {
969 value: &'a str,
970 datatype: NamedNodeRef<'a>,
971 },
972}
973
974impl PartialEq for LiteralRefContent<'_> {
981 fn eq(&self, other: &Self) -> bool {
982 match (self, other) {
983 (LiteralRefContent::String(a), LiteralRefContent::String(b)) => a == b,
984 (
985 LiteralRefContent::LanguageTaggedString {
986 value: v1,
987 language: l1,
988 },
989 LiteralRefContent::LanguageTaggedString {
990 value: v2,
991 language: l2,
992 },
993 ) => v1 == v2 && l1.eq_ignore_ascii_case(l2),
994 #[cfg(feature = "rdf-12")]
995 (
996 LiteralRefContent::DirectionalLanguageTaggedString {
997 value: v1,
998 language: l1,
999 direction: d1,
1000 },
1001 LiteralRefContent::DirectionalLanguageTaggedString {
1002 value: v2,
1003 language: l2,
1004 direction: d2,
1005 },
1006 ) => v1 == v2 && l1.eq_ignore_ascii_case(l2) && d1 == d2,
1007 (
1008 LiteralRefContent::TypedLiteral {
1009 value: v1,
1010 datatype: d1,
1011 },
1012 LiteralRefContent::TypedLiteral {
1013 value: v2,
1014 datatype: d2,
1015 },
1016 ) => v1 == v2 && d1 == d2,
1017 _ => false,
1018 }
1019 }
1020}
1021
1022impl Eq for LiteralRefContent<'_> {}
1023
1024impl std::hash::Hash for LiteralRefContent<'_> {
1025 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1026 match self {
1027 LiteralRefContent::String(value) => {
1028 0u8.hash(state);
1029 value.hash(state);
1030 }
1031 LiteralRefContent::LanguageTaggedString { value, language } => {
1032 1u8.hash(state);
1033 value.hash(state);
1034 for b in language.bytes() {
1035 b.to_ascii_lowercase().hash(state);
1036 }
1037 }
1038 #[cfg(feature = "rdf-12")]
1039 LiteralRefContent::DirectionalLanguageTaggedString {
1040 value,
1041 language,
1042 direction,
1043 } => {
1044 2u8.hash(state);
1045 value.hash(state);
1046 for b in language.bytes() {
1047 b.to_ascii_lowercase().hash(state);
1048 }
1049 direction.hash(state);
1050 }
1051 LiteralRefContent::TypedLiteral { value, datatype } => {
1052 3u8.hash(state);
1053 value.hash(state);
1054 datatype.hash(state);
1055 }
1056 }
1057 }
1058}
1059
1060impl<'a> LiteralRef<'a> {
1061 #[inline]
1063 pub const fn new_simple_literal(value: &'a str) -> Self {
1064 LiteralRef(LiteralRefContent::String(value))
1065 }
1066
1067 #[inline]
1069 pub const fn new(value: &'a str) -> Self {
1070 Self::new_simple_literal(value)
1071 }
1072
1073 #[inline]
1075 pub fn new_typed_literal(value: &'a str, datatype: impl Into<NamedNodeRef<'a>>) -> Self {
1076 let datatype = datatype.into();
1077 LiteralRef(if datatype == xsd::STRING.as_ref() {
1078 LiteralRefContent::String(value)
1079 } else {
1080 LiteralRefContent::TypedLiteral { value, datatype }
1081 })
1082 }
1083
1084 #[inline]
1086 pub fn new_typed(value: &'a str, datatype: NamedNodeRef<'a>) -> Self {
1087 Self::new_typed_literal(value, datatype)
1088 }
1089
1090 #[inline]
1098 pub const fn new_language_tagged_literal_unchecked(value: &'a str, language: &'a str) -> Self {
1099 LiteralRef(LiteralRefContent::LanguageTaggedString { value, language })
1100 }
1101
1102 #[inline]
1104 pub const fn new_lang(value: &'a str, language: &'a str) -> Self {
1105 Self::new_language_tagged_literal_unchecked(value, language)
1106 }
1107
1108 #[cfg(feature = "rdf-12")]
1116 #[inline]
1117 pub const fn new_directional_language_tagged_literal_unchecked(
1118 value: &'a str,
1119 language: &'a str,
1120 direction: BaseDirection,
1121 ) -> Self {
1122 LiteralRef(LiteralRefContent::DirectionalLanguageTaggedString {
1123 value,
1124 language,
1125 direction,
1126 })
1127 }
1128
1129 #[inline]
1131 pub const fn value(self) -> &'a str {
1132 match self.0 {
1133 LiteralRefContent::String(value)
1134 | LiteralRefContent::LanguageTaggedString { value, .. }
1135 | LiteralRefContent::TypedLiteral { value, .. } => value,
1136 #[cfg(feature = "rdf-12")]
1137 LiteralRefContent::DirectionalLanguageTaggedString { value, .. } => value,
1138 }
1139 }
1140
1141 #[inline]
1146 pub const fn language(self) -> Option<&'a str> {
1147 match self.0 {
1148 LiteralRefContent::LanguageTaggedString { language, .. } => Some(language),
1149 #[cfg(feature = "rdf-12")]
1150 LiteralRefContent::DirectionalLanguageTaggedString { language, .. } => Some(language),
1151 _ => None,
1152 }
1153 }
1154
1155 #[cfg(feature = "rdf-12")]
1159 #[inline]
1160 pub const fn direction(self) -> Option<BaseDirection> {
1161 match self.0 {
1162 LiteralRefContent::DirectionalLanguageTaggedString { direction, .. } => Some(direction),
1163 _ => None,
1164 }
1165 }
1166
1167 #[inline]
1172 pub fn datatype(self) -> NamedNodeRef<'a> {
1173 match self.0 {
1174 LiteralRefContent::String(_) => xsd::STRING.as_ref(),
1175 LiteralRefContent::LanguageTaggedString { .. } => rdf::LANG_STRING.as_ref(),
1176 #[cfg(feature = "rdf-12")]
1177 LiteralRefContent::DirectionalLanguageTaggedString { .. } => {
1178 rdf::DIR_LANG_STRING.as_ref()
1179 }
1180 LiteralRefContent::TypedLiteral { datatype, .. } => datatype,
1181 }
1182 }
1183
1184 #[inline]
1189 #[deprecated(note = "Plain literal concept is removed in RDF 1.1", since = "0.3.0")]
1190 pub const fn is_plain(self) -> bool {
1191 matches!(
1192 self.0,
1193 LiteralRefContent::String(_) | LiteralRefContent::LanguageTaggedString { .. }
1194 )
1195 }
1196
1197 #[inline]
1198 pub fn into_owned(self) -> Literal {
1199 Literal(match self.0 {
1200 LiteralRefContent::String(value) => LiteralContent::String(value.to_owned()),
1201 LiteralRefContent::LanguageTaggedString { value, language } => {
1202 LiteralContent::LanguageTaggedString {
1203 value: value.to_owned(),
1204 language: language.to_owned(),
1205 }
1206 }
1207 #[cfg(feature = "rdf-12")]
1208 LiteralRefContent::DirectionalLanguageTaggedString {
1209 value,
1210 language,
1211 direction,
1212 } => LiteralContent::DirectionalLanguageTaggedString {
1213 value: value.to_owned(),
1214 language: language.to_owned(),
1215 direction,
1216 },
1217 LiteralRefContent::TypedLiteral { value, datatype } => LiteralContent::TypedLiteral {
1218 value: value.to_owned(),
1219 datatype: datatype.into_owned(),
1220 },
1221 })
1222 }
1223
1224 #[inline]
1226 pub fn to_owned(&self) -> Literal {
1227 self.into_owned()
1228 }
1229}
1230
1231impl fmt::Display for LiteralRef<'_> {
1232 #[inline]
1233 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1234 match self.0 {
1235 LiteralRefContent::String(value) => print_quoted_str(value, f),
1236 LiteralRefContent::LanguageTaggedString { value, language } => {
1237 print_quoted_str(value, f)?;
1238 write!(f, "@{language}")
1239 }
1240 #[cfg(feature = "rdf-12")]
1241 LiteralRefContent::DirectionalLanguageTaggedString {
1242 value,
1243 language,
1244 direction,
1245 } => {
1246 print_quoted_str(value, f)?;
1247 write!(f, "@{language}--{direction}")
1248 }
1249 LiteralRefContent::TypedLiteral { value, datatype } => {
1250 print_quoted_str(value, f)?;
1251 write!(f, "^^{datatype}")
1252 }
1253 }
1254 }
1255}
1256
1257impl<'a> RdfTerm for LiteralRef<'a> {
1258 fn as_str(&self) -> &str {
1259 self.value()
1260 }
1261
1262 fn is_literal(&self) -> bool {
1263 true
1264 }
1265}
1266
1267#[inline]
1269pub fn print_quoted_str(string: &str, f: &mut impl Write) -> fmt::Result {
1270 f.write_char('"')?;
1271 for c in string.chars() {
1272 match c {
1273 '\u{08}' => f.write_str("\\b"),
1274 '\t' => f.write_str("\\t"),
1275 '\n' => f.write_str("\\n"),
1276 '\u{0C}' => f.write_str("\\f"),
1277 '\r' => f.write_str("\\r"),
1278 '"' => f.write_str("\\\""),
1279 '\\' => f.write_str("\\\\"),
1280 '\0'..='\u{1F}' | '\u{7F}' => write!(f, "\\u{:04X}", u32::from(c)),
1281 _ => f.write_char(c),
1282 }?;
1283 }
1284 f.write_char('"')
1285}
1286
1287#[cfg(feature = "rdf-12")]
1289#[derive(Eq, PartialEq, Debug, Clone, Copy, Hash, PartialOrd, Ord)]
1290#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1291pub enum BaseDirection {
1292 Ltr,
1294 Rtl,
1296}
1297
1298#[cfg(feature = "rdf-12")]
1299impl fmt::Display for BaseDirection {
1300 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1301 f.write_str(match self {
1302 Self::Ltr => "ltr",
1303 Self::Rtl => "rtl",
1304 })
1305 }
1306}
1307
1308impl<'a> From<&'a Literal> for LiteralRef<'a> {
1309 #[inline]
1310 fn from(node: &'a Literal) -> Self {
1311 node.as_ref()
1312 }
1313}
1314
1315impl<'a> From<LiteralRef<'a>> for Literal {
1316 #[inline]
1317 fn from(node: LiteralRef<'a>) -> Self {
1318 node.into_owned()
1319 }
1320}
1321
1322impl<'a> From<&'a str> for LiteralRef<'a> {
1323 #[inline]
1324 fn from(value: &'a str) -> Self {
1325 LiteralRef(LiteralRefContent::String(value))
1326 }
1327}
1328
1329impl PartialEq<Literal> for LiteralRef<'_> {
1330 #[inline]
1331 fn eq(&self, other: &Literal) -> bool {
1332 *self == other.as_ref()
1333 }
1334}
1335
1336impl PartialEq<LiteralRef<'_>> for Literal {
1337 #[inline]
1338 fn eq(&self, other: &LiteralRef<'_>) -> bool {
1339 self.as_ref() == *other
1340 }
1341}
1342
1343impl<'a> From<&'a str> for Literal {
1345 #[inline]
1346 fn from(value: &'a str) -> Self {
1347 Self(LiteralContent::String(value.into()))
1348 }
1349}
1350
1351impl From<String> for Literal {
1352 #[inline]
1353 fn from(value: String) -> Self {
1354 Self(LiteralContent::String(value))
1355 }
1356}
1357
1358impl<'a> From<Cow<'a, str>> for Literal {
1359 #[inline]
1360 fn from(value: Cow<'a, str>) -> Self {
1361 Self(LiteralContent::String(value.into()))
1362 }
1363}
1364
1365impl From<bool> for Literal {
1366 #[inline]
1367 fn from(value: bool) -> Self {
1368 Self(LiteralContent::TypedLiteral {
1369 value: value.to_string(),
1370 datatype: xsd::BOOLEAN.clone(),
1371 })
1372 }
1373}
1374
1375impl From<i128> for Literal {
1376 #[inline]
1377 fn from(value: i128) -> Self {
1378 Self(LiteralContent::TypedLiteral {
1379 value: value.to_string(),
1380 datatype: xsd::INTEGER.clone(),
1381 })
1382 }
1383}
1384
1385impl From<i64> for Literal {
1386 #[inline]
1387 fn from(value: i64) -> Self {
1388 Self(LiteralContent::TypedLiteral {
1389 value: value.to_string(),
1390 datatype: xsd::INTEGER.clone(),
1391 })
1392 }
1393}
1394
1395impl From<i32> for Literal {
1396 #[inline]
1397 fn from(value: i32) -> Self {
1398 Self(LiteralContent::TypedLiteral {
1399 value: value.to_string(),
1400 datatype: xsd::INTEGER.clone(),
1401 })
1402 }
1403}
1404
1405impl From<i16> for Literal {
1406 #[inline]
1407 fn from(value: i16) -> Self {
1408 Self(LiteralContent::TypedLiteral {
1409 value: value.to_string(),
1410 datatype: xsd::INTEGER.clone(),
1411 })
1412 }
1413}
1414
1415impl From<u64> for Literal {
1416 #[inline]
1417 fn from(value: u64) -> Self {
1418 Self(LiteralContent::TypedLiteral {
1419 value: value.to_string(),
1420 datatype: xsd::INTEGER.clone(),
1421 })
1422 }
1423}
1424
1425impl From<u32> for Literal {
1426 #[inline]
1427 fn from(value: u32) -> Self {
1428 Self(LiteralContent::TypedLiteral {
1429 value: value.to_string(),
1430 datatype: xsd::INTEGER.clone(),
1431 })
1432 }
1433}
1434
1435impl From<u16> for Literal {
1436 #[inline]
1437 fn from(value: u16) -> Self {
1438 Self(LiteralContent::TypedLiteral {
1439 value: value.to_string(),
1440 datatype: xsd::INTEGER.clone(),
1441 })
1442 }
1443}
1444
1445impl From<f32> for Literal {
1446 #[inline]
1447 fn from(value: f32) -> Self {
1448 Self(LiteralContent::TypedLiteral {
1449 value: if value == f32::INFINITY {
1450 "INF".to_owned()
1451 } else if value == f32::NEG_INFINITY {
1452 "-INF".to_owned()
1453 } else {
1454 value.to_string()
1455 },
1456 datatype: xsd::FLOAT.clone(),
1457 })
1458 }
1459}
1460
1461impl From<f64> for Literal {
1462 #[inline]
1463 fn from(value: f64) -> Self {
1464 Self(LiteralContent::TypedLiteral {
1465 value: if value == f64::INFINITY {
1466 "INF".to_owned()
1467 } else if value == f64::NEG_INFINITY {
1468 "-INF".to_owned()
1469 } else {
1470 value.to_string()
1471 },
1472 datatype: xsd::DOUBLE.clone(),
1473 })
1474 }
1475}
1476
1477pub mod xsd_literals {
1479 use super::*;
1480 use crate::vocab::xsd;
1481
1482 pub fn boolean_literal(value: bool) -> Literal {
1486 Literal::new_typed(value.to_string(), xsd::BOOLEAN.clone())
1487 }
1488
1489 pub fn integer_literal(value: i64) -> Literal {
1491 Literal::new_typed(value.to_string(), xsd::INTEGER.clone())
1492 }
1493
1494 pub fn decimal_literal(value: f64) -> Literal {
1496 Literal::new_typed(value.to_string(), xsd::DECIMAL.clone())
1497 }
1498
1499 pub fn double_literal(value: f64) -> Literal {
1501 Literal::new_typed(value.to_string(), xsd::DOUBLE.clone())
1502 }
1503
1504 pub fn string_literal(value: &str) -> Literal {
1506 Literal::new_typed(value, xsd::STRING.clone())
1507 }
1508}
1509
1510#[cfg(test)]
1511mod tests {
1512 use super::*;
1513
1514 #[test]
1515 fn test_simple_literal_equality() {
1516 assert_eq!(
1517 Literal::new_simple_literal("foo"),
1518 Literal::new_typed_literal("foo", xsd::STRING.clone())
1519 );
1520 assert_eq!(
1521 Literal::new_simple_literal("foo"),
1522 LiteralRef::new_typed_literal("foo", xsd::STRING.as_ref())
1523 );
1524 assert_eq!(
1525 LiteralRef::new_simple_literal("foo"),
1526 Literal::new_typed_literal("foo", xsd::STRING.clone())
1527 );
1528 assert_eq!(
1529 LiteralRef::new_simple_literal("foo"),
1530 LiteralRef::new_typed_literal("foo", xsd::STRING.as_ref())
1531 );
1532 }
1533
1534 #[test]
1535 fn test_float_format() {
1536 assert_eq!("INF", Literal::from(f32::INFINITY).value());
1537 assert_eq!("INF", Literal::from(f64::INFINITY).value());
1538 assert_eq!("-INF", Literal::from(f32::NEG_INFINITY).value());
1539 assert_eq!("-INF", Literal::from(f64::NEG_INFINITY).value());
1540 assert_eq!("NaN", Literal::from(f32::NAN).value());
1541 assert_eq!("NaN", Literal::from(f64::NAN).value());
1542 }
1543
1544 #[test]
1545 fn test_plain_literal() {
1546 let literal = Literal::new("Hello");
1547 assert_eq!(literal.value(), "Hello");
1548 #[allow(deprecated)]
1549 {
1550 assert!(literal.is_plain());
1551 }
1552 assert!(!literal.is_lang_string());
1553 assert!(!literal.is_typed());
1554 assert_eq!(format!("{literal}"), "\"Hello\"");
1555 }
1556
1557 #[test]
1558 fn test_lang_literal() {
1559 let literal = Literal::new_lang("Hello", "en").expect("construction should succeed");
1560 assert_eq!(literal.value(), "Hello");
1561 assert_eq!(literal.language(), Some("en"));
1562 #[allow(deprecated)]
1563 {
1564 assert!(literal.is_plain());
1565 }
1566 assert!(literal.is_lang_string());
1567 assert!(!literal.is_typed());
1568 assert_eq!(format!("{literal}"), "\"Hello\"@en");
1569 }
1570
1571 #[test]
1572 fn test_typed_literal() {
1573 let literal = Literal::new_typed("42", xsd::INTEGER.clone());
1574 assert_eq!(literal.value(), "42");
1575 assert_eq!(
1576 literal.datatype().as_str(),
1577 "http://www.w3.org/2001/XMLSchema#integer"
1578 );
1579 #[allow(deprecated)]
1580 {
1581 assert!(!literal.is_plain());
1582 }
1583 assert!(!literal.is_lang_string());
1584 assert!(literal.is_typed());
1585 assert_eq!(
1586 format!("{literal}"),
1587 "\"42\"^^<http://www.w3.org/2001/XMLSchema#integer>"
1588 );
1589 }
1590
1591 #[test]
1592 fn test_literal_ref() {
1593 let literal_ref = LiteralRef::new("test");
1594 assert_eq!(literal_ref.value(), "test");
1595
1596 let owned = literal_ref.to_owned();
1597 assert_eq!(owned.value(), "test");
1598 }
1599
1600 #[test]
1601 fn test_boolean_extraction() {
1602 let bool_literal = xsd_literals::boolean_literal(true);
1603 assert!(bool_literal.is_boolean());
1604 assert_eq!(bool_literal.as_bool(), Some(true));
1605
1606 let false_literal = Literal::new_typed("false", xsd::BOOLEAN.clone());
1607 assert_eq!(false_literal.as_bool(), Some(false));
1608
1609 let true_str = Literal::new("true");
1611 assert_eq!(true_str.as_bool(), Some(true));
1612
1613 let false_str = Literal::new("0");
1614 assert_eq!(false_str.as_bool(), Some(false));
1615 }
1616
1617 #[test]
1618 fn test_numeric_extraction() {
1619 let int_literal = xsd_literals::integer_literal(42);
1620 assert!(int_literal.is_numeric());
1621 assert_eq!(int_literal.as_i64(), Some(42));
1622 assert_eq!(int_literal.as_i32(), Some(42));
1623 assert_eq!(int_literal.as_f64(), Some(42.0));
1624
1625 let decimal_literal = xsd_literals::decimal_literal(3.25);
1626 assert!(decimal_literal.is_numeric());
1627 assert_eq!(decimal_literal.as_f64(), Some(3.25));
1628 assert_eq!(decimal_literal.as_f32(), Some(3.25_f32));
1629
1630 let untyped_num = Literal::new("123");
1632 assert!(untyped_num.is_numeric());
1633 assert_eq!(untyped_num.as_i64(), Some(123));
1634 }
1635
1636 #[test]
1637 fn test_canonical_form() {
1638 let bool_literal = Literal::new_typed("True", xsd::BOOLEAN.clone());
1640 let canonical = bool_literal.canonical_form();
1641 assert_eq!(canonical.value(), "true");
1642
1643 let int_literal = Literal::new_typed(" 42 ", xsd::INTEGER.clone());
1645 let canonical = int_literal.canonical_form();
1648 assert_eq!(
1649 canonical.datatype().as_str(),
1650 "http://www.w3.org/2001/XMLSchema#integer"
1651 );
1652
1653 let dec_literal = Literal::new_typed("3.140", xsd::DECIMAL.clone());
1655 let canonical = dec_literal.canonical_form();
1656 assert_eq!(canonical.value(), "3.14"); }
1658
1659 #[test]
1660 fn test_xsd_convenience_functions() {
1661 assert_eq!(xsd_literals::boolean_literal(true).value(), "true");
1663 assert_eq!(xsd_literals::integer_literal(123).value(), "123");
1664 assert_eq!(xsd_literals::decimal_literal(3.25).value(), "3.25");
1665 assert_eq!(xsd_literals::double_literal(2.71).value(), "2.71");
1666 assert_eq!(xsd_literals::string_literal("hello").value(), "hello");
1667
1668 assert_eq!(
1670 xsd_literals::boolean_literal(true).datatype().as_str(),
1671 "http://www.w3.org/2001/XMLSchema#boolean"
1672 );
1673 assert_eq!(
1674 xsd_literals::integer_literal(123).datatype().as_str(),
1675 "http://www.w3.org/2001/XMLSchema#integer"
1676 );
1677 }
1678
1679 #[test]
1680 fn test_numeric_type_detection() {
1681 let int_lit = Literal::new_typed("42", xsd::INTEGER.clone());
1683 assert!(int_lit.is_numeric());
1684
1685 let float_lit = Literal::new_typed("3.14", xsd::FLOAT.clone());
1686 assert!(float_lit.is_numeric());
1687
1688 let double_lit = Literal::new_typed("2.71", xsd::DOUBLE.clone());
1689 assert!(double_lit.is_numeric());
1690
1691 let string_lit = Literal::new_typed("hello", xsd::STRING.clone());
1693 assert!(!string_lit.is_numeric());
1694
1695 let bool_lit = Literal::new_typed("true", xsd::BOOLEAN.clone());
1696 assert!(!bool_lit.is_numeric());
1697 }
1698
1699 #[test]
1703 fn regression_language_tag_preserves_original_case() {
1704 let literal =
1705 Literal::new_language_tagged_literal("foo", "en-US").expect("valid language literal");
1706 assert_eq!(
1707 literal.language(),
1708 Some("en-US"),
1709 "the stored language tag must keep its original case"
1710 );
1711 assert_eq!(format!("{literal}"), "\"foo\"@en-US");
1712 }
1713
1714 #[test]
1719 fn regression_language_tag_case_insensitive_equality_and_hash() {
1720 use std::collections::hash_map::DefaultHasher;
1721 use std::hash::{Hash, Hasher};
1722
1723 let upper =
1724 Literal::new_language_tagged_literal("foo", "en-US").expect("valid language literal");
1725 let lower =
1726 Literal::new_language_tagged_literal("foo", "en-us").expect("valid language literal");
1727 let different_value =
1728 Literal::new_language_tagged_literal("bar", "en-US").expect("valid language literal");
1729
1730 assert_eq!(upper, lower, "tags differing only in case must be equal");
1731 assert_ne!(upper, different_value);
1732
1733 let hash_of = |l: &Literal| {
1736 let mut hasher = DefaultHasher::new();
1737 l.hash(&mut hasher);
1738 hasher.finish()
1739 };
1740 assert_eq!(hash_of(&upper), hash_of(&lower));
1741
1742 assert_eq!(upper.cmp(&lower), std::cmp::Ordering::Equal);
1744
1745 let lower_ref = LiteralRef::new_language_tagged_literal_unchecked("foo", "en-us");
1747 assert_eq!(upper, lower_ref);
1748 assert_eq!(lower_ref, upper);
1749 }
1750
1751 #[cfg(feature = "rdf-12")]
1755 #[test]
1756 fn regression_directional_language_tag_preserves_original_case() {
1757 let literal =
1758 Literal::new_directional_language_tagged_literal("foo", "en-US", BaseDirection::Ltr)
1759 .expect("valid directional language literal");
1760 assert_eq!(literal.language(), Some("en-US"));
1761 }
1762}