Skip to main content

oxirs_core/model/
literal.rs

1//! RDF Literal implementation
2//!
3//! This implementation is extracted and adapted from Oxigraph's oxrdf literal handling
4//! to provide zero-dependency RDF literal support with full XSD datatype validation.
5
6use 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/// Language tag validation error type
17#[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/// A language tag following BCP 47 specification
37#[derive(Debug, Clone, PartialEq, Eq, Hash)]
38pub struct LanguageTag {
39    tag: String,
40}
41
42impl LanguageTag {
43    /// Parses a language tag from a string
44    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    /// Returns the language tag as a string slice
51    pub fn as_str(&self) -> &str {
52        &self.tag
53    }
54
55    /// Consumes the language tag and returns the inner string
56    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
67/// Validates a language tag according to BCP 47 (RFC 5646) using oxilangtag
68fn 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
76/// Validates a literal value against its XSD datatype
77pub fn validate_xsd_value(value: &str, datatype_iri: &str) -> Result<(), OxirsError> {
78    match datatype_iri {
79        // String types
80        "http://www.w3.org/2001/XMLSchema#string"
81        | "http://www.w3.org/2001/XMLSchema#normalizedString"
82        | "http://www.w3.org/2001/XMLSchema#token" => {
83            // All strings are valid for string types
84            Ok(())
85        }
86
87        // Boolean type - use oxsdatatypes Boolean parsing
88        "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        // Integer types - use oxsdatatypes Integer parsing with range validation
93        "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        // Decimal type - use oxsdatatypes Decimal parsing
110        "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        // Floating point types - use oxsdatatypes Float/Double parsing
115        "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        // Date/time types - use oxsdatatypes parsing
123        "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        // For unknown datatypes, don't validate
136        _ => Ok(()),
137    }
138}
139
140/// Validates integer values against their specific type ranges
141#[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        _ => {} // Other integer types don't have additional range restrictions in this simplified implementation
205    }
206
207    Ok(())
208}
209
210/// Validates integer values against their specific type ranges using oxsdatatypes Integer
211fn validate_integer_range_oxs(integer: Integer, datatype_iri: &str) -> Result<(), OxirsError> {
212    // Convert oxsdatatypes Integer to i64 for range checking
213    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        _ => {} // Other integer types don't have additional range restrictions
275    }
276
277    Ok(())
278}
279
280/// An owned RDF [literal](https://www.w3.org/TR/rdf11-concepts/#dfn-literal).
281///
282/// The default string formatter is returning an N-Triples, Turtle, and SPARQL compatible representation:
283/// ```
284/// use oxirs_core::model::literal::Literal;
285/// use oxirs_core::vocab::xsd;
286///
287/// assert_eq!(
288///     "\"foo\\nbar\"",
289///     Literal::new_simple_literal("foo\nbar").to_string()
290/// );
291///
292/// assert_eq!(
293///     r#""1999-01-01"^^<http://www.w3.org/2001/XMLSchema#date>"#,
294///     Literal::new_typed_literal("1999-01-01", xsd::DATE.clone()).to_string()
295/// );
296///
297/// assert_eq!(
298///     r#""foo"@en"#,
299///     Literal::new_language_tagged_literal("foo", "en").expect("valid language literal").to_string()
300/// );
301/// ```
302#[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
326/// Ordinal used to order/compare `LiteralContent` variants (mirrors the
327/// enum's declaration order, matching what `#[derive(PartialOrd, Ord)]`
328/// would have produced).
329fn 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
339// `LiteralContent`'s `PartialEq`/`Eq`/`Hash`/`PartialOrd`/`Ord` are hand-written
340// rather than derived so that a language tag on `LanguageTaggedString`/
341// `DirectionalLanguageTaggedString` compares and hashes *case-insensitively*,
342// per RDF 1.1 (language tags are compared case-insensitively -- `"foo"@en-US`
343// and `"foo"@en-us` denote the same literal) -- while the *stored* lexical
344// form still preserves whatever case the caller originally supplied (see
345// `Literal::new_language_tagged_literal`, which used to destructively
346// lowercase the tag before storing it). A naive `#[derive]` here would make
347// two RDF-equal literals compare unequal whenever their tags differ only in
348// case.
349impl 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                // Hash the case-folded tag so tags that are `eq_ignore_ascii_case`
401                // (and thus `PartialEq`-equal above) always hash equal.
402                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    /// Builds an RDF [simple literal](https://www.w3.org/TR/rdf11-concepts/#dfn-simple-literal).
481    #[inline]
482    pub fn new_simple_literal(value: impl Into<String>) -> Self {
483        Self(LiteralContent::String(value.into()))
484    }
485
486    /// Creates a new string literal without language or datatype (alias for compatibility)
487    #[inline]
488    pub fn new(value: impl Into<String>) -> Self {
489        Self::new_simple_literal(value)
490    }
491
492    /// Builds an RDF [literal](https://www.w3.org/TR/rdf11-concepts/#dfn-literal) with a [datatype](https://www.w3.org/TR/rdf11-concepts/#dfn-datatype-iri).
493    #[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    /// Creates a new literal with a datatype (alias for compatibility)
505    #[inline]
506    pub fn new_typed(value: impl Into<String>, datatype: NamedNode) -> Self {
507        Self::new_typed_literal(value, datatype)
508    }
509
510    /// Creates a new literal with a datatype and validates the value
511    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    /// Builds an RDF [language-tagged string](https://www.w3.org/TR/rdf11-concepts/#dfn-language-tagged-string).
521    #[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        // RDF 1.1/1.2 language tags are compared case-insensitively, but the
528        // *lexical form* must be preserved as authored (e.g. SPARQL `LANG()`
529        // returns the tag exactly as written, and a parse -> serialize round
530        // trip must not mutate the term). Validate the tag without mutating
531        // it; `LiteralContent`'s `PartialEq`/`Eq`/`Hash`/`Ord` fold case for
532        // language tags so equality/lookup semantics stay RDF-1.1-correct
533        // even though the stored string keeps its original case.
534        validate_language_tag(&language)?;
535        Ok(Self::new_language_tagged_literal_unchecked(value, language))
536    }
537
538    /// Builds an RDF [language-tagged string](https://www.w3.org/TR/rdf11-concepts/#dfn-language-tagged-string).
539    ///
540    /// It is the responsibility of the caller to check that `language`
541    /// is valid [BCP47](https://tools.ietf.org/html/bcp47) language tag,
542    /// and is lowercase.
543    ///
544    /// [`Literal::new_language_tagged_literal()`] is a safe version of this constructor and should be used for untrusted data.
545    #[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    /// Creates a new literal with a language tag (alias for compatibility)
557    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    /// Builds an RDF [directional language-tagged string](https://www.w3.org/TR/rdf12-concepts/#dfn-dir-lang-string).
566    #[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        // See `new_language_tagged_literal`: preserve the tag's original
574        // case for round-tripping; case-insensitive comparison is handled by
575        // `LiteralContent`'s `PartialEq`/`Eq`/`Hash`/`Ord` impls.
576        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    /// Builds an RDF [directional language-tagged string](https://www.w3.org/TR/rdf12-concepts/#dfn-dir-lang-string).
584    ///
585    /// It is the responsibility of the caller to check that `language`
586    /// is valid [BCP47](https://tools.ietf.org/html/bcp47) language tag,
587    /// and is lowercase.
588    ///
589    /// [`Literal::new_directional_language_tagged_literal()`] is a safe version of this constructor and should be used for untrusted data.
590    #[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    /// The literal [lexical form](https://www.w3.org/TR/rdf11-concepts/#dfn-lexical-form).
605    #[inline]
606    pub fn value(&self) -> &str {
607        self.as_ref().value()
608    }
609
610    /// The literal [language tag](https://www.w3.org/TR/rdf11-concepts/#dfn-language-tag) if it is a [language-tagged string](https://www.w3.org/TR/rdf11-concepts/#dfn-language-tagged-string).
611    ///
612    /// Language tags are defined by the [BCP47](https://tools.ietf.org/html/bcp47).
613    /// They are normalized to lowercase by this implementation.
614    #[inline]
615    pub fn language(&self) -> Option<&str> {
616        self.as_ref().language()
617    }
618
619    /// The literal [base direction](https://www.w3.org/TR/rdf12-concepts/#dfn-base-direction) if it is a [directional language-tagged string](https://www.w3.org/TR/rdf12-concepts/#dfn-base-direction).
620    ///
621    /// The two possible base directions are left-to-right (`ltr`) and right-to-left (`rtl`).
622    #[cfg(feature = "rdf-12")]
623    #[inline]
624    pub fn direction(&self) -> Option<BaseDirection> {
625        self.as_ref().direction()
626    }
627
628    /// The literal [datatype](https://www.w3.org/TR/rdf11-concepts/#dfn-datatype-iri).
629    ///
630    /// The datatype of [language-tagged string](https://www.w3.org/TR/rdf11-concepts/#dfn-language-tagged-string) is always [rdf:langString](https://www.w3.org/TR/rdf11-concepts/#dfn-language-tagged-string).
631    /// The datatype of [simple literals](https://www.w3.org/TR/rdf11-concepts/#dfn-simple-literal) is [xsd:string](https://www.w3.org/TR/xmlschema11-2/#string).
632    #[inline]
633    pub fn datatype(&self) -> NamedNodeRef<'_> {
634        self.as_ref().datatype()
635    }
636
637    /// Checks if this literal could be seen as an RDF 1.0 [plain literal](https://www.w3.org/TR/2004/REC-rdf-concepts-20040210/#dfn-plain-literal).
638    ///
639    /// It returns true if the literal is a [language-tagged string](https://www.w3.org/TR/rdf11-concepts/#dfn-language-tagged-string)
640    /// or has the datatype [xsd:string](https://www.w3.org/TR/xmlschema11-2/#string).
641    #[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    /// Returns true if this literal has a language tag
649    pub fn is_lang_string(&self) -> bool {
650        self.language().is_some()
651    }
652
653    /// Returns true if this literal has a datatype (excluding xsd:string which is implicit)
654    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    /// Extract components from this literal (value, datatype, language tag).
683    #[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    /// Attempts to extract the value as a boolean
701    ///
702    /// Works for XSD boolean literals and other representations like "true"/"false"
703    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    /// Attempts to extract the value as an integer
712    ///
713    /// Works for XSD integer literals and other numeric representations
714    pub fn as_i64(&self) -> Option<i64> {
715        self.value().parse().ok()
716    }
717
718    /// Attempts to extract the value as a 32-bit integer
719    pub fn as_i32(&self) -> Option<i32> {
720        self.value().parse().ok()
721    }
722
723    /// Attempts to extract the value as a floating point number
724    ///
725    /// Works for XSD decimal, double, float literals
726    pub fn as_f64(&self) -> Option<f64> {
727        self.value().parse().ok()
728    }
729
730    /// Attempts to extract the value as a 32-bit floating point number
731    pub fn as_f32(&self) -> Option<f32> {
732        self.value().parse().ok()
733    }
734
735    /// Returns true if this literal represents a numeric value
736    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                // Check if the value looks numeric
762                self.as_f64().is_some()
763            }
764        }
765    }
766
767    /// Returns true if this literal represents a boolean value
768    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    /// Returns the canonical form of this literal
778    ///
779    /// This normalizes the literal according to XSD rules and recommendations
780    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                            // Format decimal properly - remove trailing zeros after decimal point
823                            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                            // Handle special values
846                            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                                // Use scientific notation for very large or very small numbers
859                                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                        // Normalize whitespace for normalizedString
872                        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                        // No normalization needed for string
877                    }
878                    "http://www.w3.org/2001/XMLSchema#token" => {
879                        // Normalize whitespace and collapse consecutive spaces
880                        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                // Keep original case for language tags to match RFC 5646 best practices
888                return Self(LiteralContent::LanguageTaggedString {
889                    value: value.clone(),
890                    language: language.clone(),
891                });
892            }
893            _ => {}
894        }
895        self.clone()
896    }
897
898    /// Validates this literal against its datatype (if any)
899    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/// A borrowed RDF [literal](https://www.w3.org/TR/rdf11-concepts/#dfn-literal).
936///
937/// The default string formatter is returning an N-Triples, Turtle, and SPARQL compatible representation:
938/// ```
939/// use oxirs_core::model::literal::LiteralRef;
940/// use oxirs_core::vocab::xsd;
941///
942/// assert_eq!(
943///     "\"foo\\nbar\"",
944///     LiteralRef::new_simple_literal("foo\nbar").to_string()
945/// );
946///
947/// assert_eq!(
948///     r#""1999-01-01"^^<http://www.w3.org/2001/XMLSchema#date>"#,
949///     LiteralRef::new_typed_literal("1999-01-01", xsd::DATE.as_ref()).to_string()
950/// );
951/// ```
952#[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
974// Hand-written to match `LiteralContent`'s case-insensitive language-tag
975// `PartialEq`/`Eq`/`Hash` (see the comment there): `LiteralRef == Literal`
976// comparisons (below) go through this borrowed variant's equality, so it
977// must agree with the owned `LiteralContent`'s semantics or the two
978// directions of the cross-type `PartialEq` impls would disagree with each
979// other.
980impl 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    /// Builds an RDF [simple literal](https://www.w3.org/TR/rdf11-concepts/#dfn-simple-literal).
1062    #[inline]
1063    pub const fn new_simple_literal(value: &'a str) -> Self {
1064        LiteralRef(LiteralRefContent::String(value))
1065    }
1066
1067    /// Creates a new literal reference (alias for compatibility)
1068    #[inline]
1069    pub const fn new(value: &'a str) -> Self {
1070        Self::new_simple_literal(value)
1071    }
1072
1073    /// Builds an RDF [literal](https://www.w3.org/TR/rdf11-concepts/#dfn-literal) with a [datatype](https://www.w3.org/TR/rdf11-concepts/#dfn-datatype-iri).
1074    #[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    /// Creates a new typed literal reference (alias for compatibility)
1085    #[inline]
1086    pub fn new_typed(value: &'a str, datatype: NamedNodeRef<'a>) -> Self {
1087        Self::new_typed_literal(value, datatype)
1088    }
1089
1090    /// Builds an RDF [language-tagged string](https://www.w3.org/TR/rdf11-concepts/#dfn-language-tagged-string).
1091    ///
1092    /// It is the responsibility of the caller to check that `language`
1093    /// is valid [BCP47](https://tools.ietf.org/html/bcp47) language tag,
1094    /// and is lowercase.
1095    ///
1096    /// [`Literal::new_language_tagged_literal()`] is a safe version of this constructor and should be used for untrusted data.
1097    #[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    /// Creates a new language-tagged literal reference (alias for compatibility)
1103    #[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    /// Builds an RDF [directional language-tagged string](https://www.w3.org/TR/rdf12-concepts/#dfn-dir-lang-string).
1109    ///
1110    /// It is the responsibility of the caller to check that `language`
1111    /// is valid [BCP47](https://tools.ietf.org/html/bcp47) language tag,
1112    /// and is lowercase.
1113    ///
1114    /// [`Literal::new_directional_language_tagged_literal()`] is a safe version of this constructor and should be used for untrusted data.
1115    #[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    /// The literal [lexical form](https://www.w3.org/TR/rdf11-concepts/#dfn-lexical-form)
1130    #[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    /// The literal [language tag](https://www.w3.org/TR/rdf11-concepts/#dfn-language-tag) if it is a [language-tagged string](https://www.w3.org/TR/rdf11-concepts/#dfn-language-tagged-string).
1142    ///
1143    /// Language tags are defined by the [BCP47](https://tools.ietf.org/html/bcp47).
1144    /// They are normalized to lowercase by this implementation.
1145    #[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    /// The literal [base direction](https://www.w3.org/TR/rdf12-concepts/#dfn-base-direction) if it is a [directional language-tagged string](https://www.w3.org/TR/rdf12-concepts/#dfn-base-direction).
1156    ///
1157    /// The two possible base directions are left-to-right (`ltr`) and right-to-left (`rtl`).
1158    #[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    /// The literal [datatype](https://www.w3.org/TR/rdf11-concepts/#dfn-datatype-iri).
1168    ///
1169    /// The datatype of [language-tagged string](https://www.w3.org/TR/rdf11-concepts/#dfn-language-tagged-string) is always [rdf:langString](https://www.w3.org/TR/rdf11-concepts/#dfn-language-tagged-string).
1170    /// The datatype of [simple literals](https://www.w3.org/TR/rdf11-concepts/#dfn-simple-literal) is [xsd:string](https://www.w3.org/TR/xmlschema11-2/#string).
1171    #[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    /// Checks if this literal could be seen as an RDF 1.0 [plain literal](https://www.w3.org/TR/2004/REC-rdf-concepts-20040210/#dfn-plain-literal).
1185    ///
1186    /// It returns true if the literal is a [language-tagged string](https://www.w3.org/TR/rdf11-concepts/#dfn-language-tagged-string)
1187    /// or has the datatype [xsd:string](https://www.w3.org/TR/xmlschema11-2/#string).
1188    #[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    /// Converts to an owned Literal (alias for compatibility)
1225    #[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/// Helper function to print a quoted string with proper escaping
1268#[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/// A [directional language-tagged string](https://www.w3.org/TR/rdf12-concepts/#dfn-dir-lang-string) [base-direction](https://www.w3.org/TR/rdf12-concepts/#dfn-base-direction)
1288#[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    /// the initial text direction is set to left-to-right
1293    Ltr,
1294    /// the initial text direction is set to right-to-left
1295    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
1343// Implement standard From traits
1344impl<'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
1477/// Common XSD datatypes as constants and convenience functions
1478pub mod xsd_literals {
1479    use super::*;
1480    use crate::vocab::xsd;
1481
1482    // Convenience functions for creating typed literals
1483
1484    /// Creates a boolean literal
1485    pub fn boolean_literal(value: bool) -> Literal {
1486        Literal::new_typed(value.to_string(), xsd::BOOLEAN.clone())
1487    }
1488
1489    /// Creates an integer literal
1490    pub fn integer_literal(value: i64) -> Literal {
1491        Literal::new_typed(value.to_string(), xsd::INTEGER.clone())
1492    }
1493
1494    /// Creates a decimal literal
1495    pub fn decimal_literal(value: f64) -> Literal {
1496        Literal::new_typed(value.to_string(), xsd::DECIMAL.clone())
1497    }
1498
1499    /// Creates a double literal
1500    pub fn double_literal(value: f64) -> Literal {
1501        Literal::new_typed(value.to_string(), xsd::DOUBLE.clone())
1502    }
1503
1504    /// Creates a string literal
1505    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        // Test string representations
1610        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        // Test untyped numeric strings
1631        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        // Boolean canonicalization
1639        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        // Integer canonicalization
1644        let int_literal = Literal::new_typed("  42  ", xsd::INTEGER.clone());
1645        // Note: This would need actual whitespace trimming in canonical form
1646        // For now, just test that it returns a valid canonical form
1647        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        // Decimal canonicalization
1654        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"); // Should remove trailing zeros
1657    }
1658
1659    #[test]
1660    fn test_xsd_convenience_functions() {
1661        // Test all the convenience functions work
1662        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        // Test datatype assignments
1669        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        // Test various numeric types
1682        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        // Non-numeric types
1692        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    /// Regression test: `new_language_tagged_literal` must preserve the
1700    /// language tag's original case (round-trip fidelity / SPARQL `LANG()`
1701    /// contract) rather than destructively lowercasing it.
1702    #[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    /// Regression test: two language-tagged literals whose tags differ only
1715    /// in case are still RDF-1.1 equal (and hash equal), even though the
1716    /// lexical form of the tag is no longer normalized to lowercase at
1717    /// construction time.
1718    #[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        // Case-insensitively-equal tags must hash equal too, so they behave
1734        // correctly as `HashMap`/`HashSet` keys.
1735        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        // And they must compare `Equal` under `Ord`, consistent with `PartialEq`.
1743        assert_eq!(upper.cmp(&lower), std::cmp::Ordering::Equal);
1744
1745        // The cross-type `Literal == LiteralRef` comparison must agree too.
1746        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    /// Regression test: the directional (rdf-12) language-tagged literal
1752    /// constructor must also preserve tag case, matching
1753    /// `new_language_tagged_literal`.
1754    #[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}