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(PartialEq, Eq, Debug, Clone, Hash, PartialOrd, Ord)]
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
326impl Literal {
327    /// Builds an RDF [simple literal](https://www.w3.org/TR/rdf11-concepts/#dfn-simple-literal).
328    #[inline]
329    pub fn new_simple_literal(value: impl Into<String>) -> Self {
330        Self(LiteralContent::String(value.into()))
331    }
332
333    /// Creates a new string literal without language or datatype (alias for compatibility)
334    #[inline]
335    pub fn new(value: impl Into<String>) -> Self {
336        Self::new_simple_literal(value)
337    }
338
339    /// 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).
340    #[inline]
341    pub fn new_typed_literal(value: impl Into<String>, datatype: impl Into<NamedNode>) -> Self {
342        let value = value.into();
343        let datatype = datatype.into();
344        Self(if datatype == *xsd::STRING {
345            LiteralContent::String(value)
346        } else {
347            LiteralContent::TypedLiteral { value, datatype }
348        })
349    }
350
351    /// Creates a new literal with a datatype (alias for compatibility)
352    #[inline]
353    pub fn new_typed(value: impl Into<String>, datatype: NamedNode) -> Self {
354        Self::new_typed_literal(value, datatype)
355    }
356
357    /// Creates a new literal with a datatype and validates the value
358    pub fn new_typed_validated(
359        value: impl Into<String>,
360        datatype: NamedNode,
361    ) -> Result<Self, OxirsError> {
362        let value = value.into();
363        validate_xsd_value(&value, datatype.as_str())?;
364        Ok(Literal::new_typed_literal(value, datatype))
365    }
366
367    /// Builds an RDF [language-tagged string](https://www.w3.org/TR/rdf11-concepts/#dfn-language-tagged-string).
368    #[inline]
369    pub fn new_language_tagged_literal(
370        value: impl Into<String>,
371        language: impl Into<String>,
372    ) -> Result<Self, LanguageTagParseError> {
373        let language = language.into().to_ascii_lowercase();
374        // Normalize to lowercase per RDF 1.1 spec (language tags are case-insensitive,
375        // stored as lowercase for consistent comparison and lookup).
376        validate_language_tag(&language)?;
377        Ok(Self::new_language_tagged_literal_unchecked(value, language))
378    }
379
380    /// Builds an RDF [language-tagged string](https://www.w3.org/TR/rdf11-concepts/#dfn-language-tagged-string).
381    ///
382    /// It is the responsibility of the caller to check that `language`
383    /// is valid [BCP47](https://tools.ietf.org/html/bcp47) language tag,
384    /// and is lowercase.
385    ///
386    /// [`Literal::new_language_tagged_literal()`] is a safe version of this constructor and should be used for untrusted data.
387    #[inline]
388    pub fn new_language_tagged_literal_unchecked(
389        value: impl Into<String>,
390        language: impl Into<String>,
391    ) -> Self {
392        Self(LiteralContent::LanguageTaggedString {
393            value: value.into(),
394            language: language.into(),
395        })
396    }
397
398    /// Creates a new literal with a language tag (alias for compatibility)
399    pub fn new_lang(
400        value: impl Into<String>,
401        language: impl Into<String>,
402    ) -> Result<Self, OxirsError> {
403        let result = Self::new_language_tagged_literal(value, language)?;
404        Ok(result)
405    }
406
407    /// Builds an RDF [directional language-tagged string](https://www.w3.org/TR/rdf12-concepts/#dfn-dir-lang-string).
408    #[cfg(feature = "rdf-12")]
409    #[inline]
410    pub fn new_directional_language_tagged_literal(
411        value: impl Into<String>,
412        language: impl Into<String>,
413        direction: impl Into<BaseDirection>,
414    ) -> Result<Self, LanguageTagParseError> {
415        let mut language = language.into();
416        language.make_ascii_lowercase();
417        validate_language_tag(&language)?;
418        Ok(Self::new_directional_language_tagged_literal_unchecked(
419            value, language, direction,
420        ))
421    }
422
423    /// Builds an RDF [directional language-tagged string](https://www.w3.org/TR/rdf12-concepts/#dfn-dir-lang-string).
424    ///
425    /// It is the responsibility of the caller to check that `language`
426    /// is valid [BCP47](https://tools.ietf.org/html/bcp47) language tag,
427    /// and is lowercase.
428    ///
429    /// [`Literal::new_directional_language_tagged_literal()`] is a safe version of this constructor and should be used for untrusted data.
430    #[cfg(feature = "rdf-12")]
431    #[inline]
432    pub fn new_directional_language_tagged_literal_unchecked(
433        value: impl Into<String>,
434        language: impl Into<String>,
435        direction: impl Into<BaseDirection>,
436    ) -> Self {
437        Self(LiteralContent::DirectionalLanguageTaggedString {
438            value: value.into(),
439            language: language.into(),
440            direction: direction.into(),
441        })
442    }
443
444    /// The literal [lexical form](https://www.w3.org/TR/rdf11-concepts/#dfn-lexical-form).
445    #[inline]
446    pub fn value(&self) -> &str {
447        self.as_ref().value()
448    }
449
450    /// 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).
451    ///
452    /// Language tags are defined by the [BCP47](https://tools.ietf.org/html/bcp47).
453    /// They are normalized to lowercase by this implementation.
454    #[inline]
455    pub fn language(&self) -> Option<&str> {
456        self.as_ref().language()
457    }
458
459    /// 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).
460    ///
461    /// The two possible base directions are left-to-right (`ltr`) and right-to-left (`rtl`).
462    #[cfg(feature = "rdf-12")]
463    #[inline]
464    pub fn direction(&self) -> Option<BaseDirection> {
465        self.as_ref().direction()
466    }
467
468    /// The literal [datatype](https://www.w3.org/TR/rdf11-concepts/#dfn-datatype-iri).
469    ///
470    /// 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).
471    /// 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).
472    #[inline]
473    pub fn datatype(&self) -> NamedNodeRef<'_> {
474        self.as_ref().datatype()
475    }
476
477    /// 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).
478    ///
479    /// It returns true if the literal is a [language-tagged string](https://www.w3.org/TR/rdf11-concepts/#dfn-language-tagged-string)
480    /// or has the datatype [xsd:string](https://www.w3.org/TR/xmlschema11-2/#string).
481    #[inline]
482    #[deprecated(note = "Plain literal concept is removed in RDF 1.1", since = "0.3.0")]
483    pub fn is_plain(&self) -> bool {
484        #[allow(deprecated)]
485        self.as_ref().is_plain()
486    }
487
488    /// Returns true if this literal has a language tag
489    pub fn is_lang_string(&self) -> bool {
490        self.language().is_some()
491    }
492
493    /// Returns true if this literal has a datatype (excluding xsd:string which is implicit)
494    pub fn is_typed(&self) -> bool {
495        matches!(&self.0, LiteralContent::TypedLiteral { .. })
496    }
497
498    #[inline]
499    pub fn as_ref(&self) -> LiteralRef<'_> {
500        LiteralRef(match &self.0 {
501            LiteralContent::String(value) => LiteralRefContent::String(value),
502            LiteralContent::LanguageTaggedString { value, language } => {
503                LiteralRefContent::LanguageTaggedString { value, language }
504            }
505            #[cfg(feature = "rdf-12")]
506            LiteralContent::DirectionalLanguageTaggedString {
507                value,
508                language,
509                direction,
510            } => LiteralRefContent::DirectionalLanguageTaggedString {
511                value,
512                language,
513                direction: *direction,
514            },
515            LiteralContent::TypedLiteral { value, datatype } => LiteralRefContent::TypedLiteral {
516                value,
517                datatype: NamedNodeRef::new_unchecked(datatype.as_str()),
518            },
519        })
520    }
521
522    /// Extract components from this literal (value, datatype, language tag).
523    #[inline]
524    pub fn destruct(self) -> (String, Option<NamedNode>, Option<String>) {
525        match self.0 {
526            LiteralContent::String(s) => (s, None, None),
527            LiteralContent::LanguageTaggedString { value, language } => {
528                (value, None, Some(language))
529            }
530            #[cfg(feature = "rdf-12")]
531            LiteralContent::DirectionalLanguageTaggedString {
532                value,
533                language,
534                direction: _,
535            } => (value, None, Some(language)),
536            LiteralContent::TypedLiteral { value, datatype } => (value, Some(datatype), None),
537        }
538    }
539
540    /// Attempts to extract the value as a boolean
541    ///
542    /// Works for XSD boolean literals and other representations like "true"/"false"
543    pub fn as_bool(&self) -> Option<bool> {
544        match self.value().to_lowercase().as_str() {
545            "true" | "1" => Some(true),
546            "false" | "0" => Some(false),
547            _ => None,
548        }
549    }
550
551    /// Attempts to extract the value as an integer
552    ///
553    /// Works for XSD integer literals and other numeric representations
554    pub fn as_i64(&self) -> Option<i64> {
555        self.value().parse().ok()
556    }
557
558    /// Attempts to extract the value as a 32-bit integer
559    pub fn as_i32(&self) -> Option<i32> {
560        self.value().parse().ok()
561    }
562
563    /// Attempts to extract the value as a floating point number
564    ///
565    /// Works for XSD decimal, double, float literals
566    pub fn as_f64(&self) -> Option<f64> {
567        self.value().parse().ok()
568    }
569
570    /// Attempts to extract the value as a 32-bit floating point number
571    pub fn as_f32(&self) -> Option<f32> {
572        self.value().parse().ok()
573    }
574
575    /// Returns true if this literal represents a numeric value
576    pub fn is_numeric(&self) -> bool {
577        match &self.0 {
578            LiteralContent::TypedLiteral { datatype, .. } => {
579                let dt_iri = datatype.as_str();
580                matches!(
581                    dt_iri,
582                    "http://www.w3.org/2001/XMLSchema#integer"
583                        | "http://www.w3.org/2001/XMLSchema#decimal"
584                        | "http://www.w3.org/2001/XMLSchema#double"
585                        | "http://www.w3.org/2001/XMLSchema#float"
586                        | "http://www.w3.org/2001/XMLSchema#long"
587                        | "http://www.w3.org/2001/XMLSchema#int"
588                        | "http://www.w3.org/2001/XMLSchema#short"
589                        | "http://www.w3.org/2001/XMLSchema#byte"
590                        | "http://www.w3.org/2001/XMLSchema#unsignedLong"
591                        | "http://www.w3.org/2001/XMLSchema#unsignedInt"
592                        | "http://www.w3.org/2001/XMLSchema#unsignedShort"
593                        | "http://www.w3.org/2001/XMLSchema#unsignedByte"
594                        | "http://www.w3.org/2001/XMLSchema#positiveInteger"
595                        | "http://www.w3.org/2001/XMLSchema#nonNegativeInteger"
596                        | "http://www.w3.org/2001/XMLSchema#negativeInteger"
597                        | "http://www.w3.org/2001/XMLSchema#nonPositiveInteger"
598                )
599            }
600            _ => {
601                // Check if the value looks numeric
602                self.as_f64().is_some()
603            }
604        }
605    }
606
607    /// Returns true if this literal represents a boolean value
608    pub fn is_boolean(&self) -> bool {
609        match &self.0 {
610            LiteralContent::TypedLiteral { datatype, .. } => {
611                datatype.as_str() == "http://www.w3.org/2001/XMLSchema#boolean"
612            }
613            _ => self.as_bool().is_some(),
614        }
615    }
616
617    /// Returns the canonical form of this literal
618    ///
619    /// This normalizes the literal according to XSD rules and recommendations
620    pub fn canonical_form(&self) -> Literal {
621        match &self.0 {
622            LiteralContent::TypedLiteral { value, datatype } => {
623                let dt_iri = datatype.as_str();
624                match dt_iri {
625                    "http://www.w3.org/2001/XMLSchema#boolean" => {
626                        if let Some(bool_val) = self.as_bool() {
627                            let canonical_value = if bool_val { "true" } else { "false" };
628                            return Literal::new_typed(canonical_value, datatype.clone());
629                        }
630                    }
631                    "http://www.w3.org/2001/XMLSchema#integer"
632                    | "http://www.w3.org/2001/XMLSchema#long"
633                    | "http://www.w3.org/2001/XMLSchema#int"
634                    | "http://www.w3.org/2001/XMLSchema#short"
635                    | "http://www.w3.org/2001/XMLSchema#byte" => {
636                        if let Some(int_val) = self.as_i64() {
637                            return Literal::new_typed(int_val.to_string(), datatype.clone());
638                        }
639                    }
640                    "http://www.w3.org/2001/XMLSchema#unsignedLong"
641                    | "http://www.w3.org/2001/XMLSchema#unsignedInt"
642                    | "http://www.w3.org/2001/XMLSchema#unsignedShort"
643                    | "http://www.w3.org/2001/XMLSchema#unsignedByte"
644                    | "http://www.w3.org/2001/XMLSchema#positiveInteger"
645                    | "http://www.w3.org/2001/XMLSchema#nonNegativeInteger" => {
646                        if let Some(int_val) = self.as_i64() {
647                            if int_val >= 0 {
648                                return Literal::new_typed(int_val.to_string(), datatype.clone());
649                            }
650                        }
651                    }
652                    "http://www.w3.org/2001/XMLSchema#negativeInteger"
653                    | "http://www.w3.org/2001/XMLSchema#nonPositiveInteger" => {
654                        if let Some(int_val) = self.as_i64() {
655                            if int_val <= 0 {
656                                return Literal::new_typed(int_val.to_string(), datatype.clone());
657                            }
658                        }
659                    }
660                    "http://www.w3.org/2001/XMLSchema#decimal" => {
661                        if let Some(dec_val) = self.as_f64() {
662                            // Format decimal properly - remove trailing zeros after decimal point
663                            let formatted = format!("{dec_val}");
664                            if formatted.contains('.') {
665                                let trimmed = formatted.trim_end_matches('0').trim_end_matches('.');
666                                return Literal::new_typed(
667                                    if trimmed.is_empty() || trimmed == "-" {
668                                        "0"
669                                    } else {
670                                        trimmed
671                                    },
672                                    datatype.clone(),
673                                );
674                            } else {
675                                return Literal::new_typed(
676                                    format!("{formatted}.0"),
677                                    datatype.clone(),
678                                );
679                            }
680                        }
681                    }
682                    "http://www.w3.org/2001/XMLSchema#double"
683                    | "http://www.w3.org/2001/XMLSchema#float" => {
684                        if let Some(float_val) = self.as_f64() {
685                            // Handle special values
686                            if float_val.is_infinite() {
687                                return Literal::new_typed(
688                                    if float_val.is_sign_positive() {
689                                        "INF"
690                                    } else {
691                                        "-INF"
692                                    },
693                                    datatype.clone(),
694                                );
695                            } else if float_val.is_nan() {
696                                return Literal::new_typed("NaN", datatype.clone());
697                            } else {
698                                // Use scientific notation for very large or very small numbers
699                                let formatted = if float_val.abs() >= 1e6
700                                    || (float_val.abs() < 1e-3 && float_val != 0.0)
701                                {
702                                    format!("{float_val:E}")
703                                } else {
704                                    format!("{float_val}")
705                                };
706                                return Literal::new_typed(formatted, datatype.clone());
707                            }
708                        }
709                    }
710                    "http://www.w3.org/2001/XMLSchema#normalizedString" => {
711                        // Normalize whitespace for normalizedString
712                        let normalized = value.replace(['\t', '\n', '\r'], " ");
713                        return Literal::new_typed(normalized, datatype.clone());
714                    }
715                    "http://www.w3.org/2001/XMLSchema#string" => {
716                        // No normalization needed for string
717                    }
718                    "http://www.w3.org/2001/XMLSchema#token" => {
719                        // Normalize whitespace and collapse consecutive spaces
720                        let normalized = value.split_whitespace().collect::<Vec<_>>().join(" ");
721                        return Literal::new_typed(normalized, datatype.clone());
722                    }
723                    _ => {}
724                }
725            }
726            LiteralContent::LanguageTaggedString { value, language } => {
727                // Keep original case for language tags to match RFC 5646 best practices
728                return Self(LiteralContent::LanguageTaggedString {
729                    value: value.clone(),
730                    language: language.clone(),
731                });
732            }
733            _ => {}
734        }
735        self.clone()
736    }
737
738    /// Validates this literal against its datatype (if any)
739    pub fn validate(&self) -> Result<(), OxirsError> {
740        match &self.0 {
741            LiteralContent::String(_) => Ok(()),
742            LiteralContent::LanguageTaggedString { language, .. } => {
743                validate_language_tag(language).map_err(Into::into)
744            }
745            #[cfg(feature = "rdf-12")]
746            LiteralContent::DirectionalLanguageTaggedString { language, .. } => {
747                validate_language_tag(language).map_err(Into::into)
748            }
749            LiteralContent::TypedLiteral { value, datatype } => {
750                validate_xsd_value(value, datatype.as_str())
751            }
752        }
753    }
754}
755
756impl fmt::Display for Literal {
757    #[inline]
758    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
759        self.as_ref().fmt(f)
760    }
761}
762
763impl RdfTerm for Literal {
764    fn as_str(&self) -> &str {
765        self.value()
766    }
767
768    fn is_literal(&self) -> bool {
769        true
770    }
771}
772
773impl ObjectTerm for Literal {}
774
775/// A borrowed RDF [literal](https://www.w3.org/TR/rdf11-concepts/#dfn-literal).
776///
777/// The default string formatter is returning an N-Triples, Turtle, and SPARQL compatible representation:
778/// ```
779/// use oxirs_core::model::literal::LiteralRef;
780/// use oxirs_core::vocab::xsd;
781///
782/// assert_eq!(
783///     "\"foo\\nbar\"",
784///     LiteralRef::new_simple_literal("foo\nbar").to_string()
785/// );
786///
787/// assert_eq!(
788///     r#""1999-01-01"^^<http://www.w3.org/2001/XMLSchema#date>"#,
789///     LiteralRef::new_typed_literal("1999-01-01", xsd::DATE.as_ref()).to_string()
790/// );
791/// ```
792#[derive(Eq, PartialEq, Debug, Clone, Copy, Hash)]
793pub struct LiteralRef<'a>(LiteralRefContent<'a>);
794
795#[derive(PartialEq, Eq, Debug, Clone, Copy, Hash)]
796enum LiteralRefContent<'a> {
797    String(&'a str),
798    LanguageTaggedString {
799        value: &'a str,
800        language: &'a str,
801    },
802    #[cfg(feature = "rdf-12")]
803    DirectionalLanguageTaggedString {
804        value: &'a str,
805        language: &'a str,
806        direction: BaseDirection,
807    },
808    TypedLiteral {
809        value: &'a str,
810        datatype: NamedNodeRef<'a>,
811    },
812}
813
814impl<'a> LiteralRef<'a> {
815    /// Builds an RDF [simple literal](https://www.w3.org/TR/rdf11-concepts/#dfn-simple-literal).
816    #[inline]
817    pub const fn new_simple_literal(value: &'a str) -> Self {
818        LiteralRef(LiteralRefContent::String(value))
819    }
820
821    /// Creates a new literal reference (alias for compatibility)
822    #[inline]
823    pub const fn new(value: &'a str) -> Self {
824        Self::new_simple_literal(value)
825    }
826
827    /// 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).
828    #[inline]
829    pub fn new_typed_literal(value: &'a str, datatype: impl Into<NamedNodeRef<'a>>) -> Self {
830        let datatype = datatype.into();
831        LiteralRef(if datatype == xsd::STRING.as_ref() {
832            LiteralRefContent::String(value)
833        } else {
834            LiteralRefContent::TypedLiteral { value, datatype }
835        })
836    }
837
838    /// Creates a new typed literal reference (alias for compatibility)
839    #[inline]
840    pub fn new_typed(value: &'a str, datatype: NamedNodeRef<'a>) -> Self {
841        Self::new_typed_literal(value, datatype)
842    }
843
844    /// Builds an RDF [language-tagged string](https://www.w3.org/TR/rdf11-concepts/#dfn-language-tagged-string).
845    ///
846    /// It is the responsibility of the caller to check that `language`
847    /// is valid [BCP47](https://tools.ietf.org/html/bcp47) language tag,
848    /// and is lowercase.
849    ///
850    /// [`Literal::new_language_tagged_literal()`] is a safe version of this constructor and should be used for untrusted data.
851    #[inline]
852    pub const fn new_language_tagged_literal_unchecked(value: &'a str, language: &'a str) -> Self {
853        LiteralRef(LiteralRefContent::LanguageTaggedString { value, language })
854    }
855
856    /// Creates a new language-tagged literal reference (alias for compatibility)
857    #[inline]
858    pub const fn new_lang(value: &'a str, language: &'a str) -> Self {
859        Self::new_language_tagged_literal_unchecked(value, language)
860    }
861
862    /// Builds an RDF [directional language-tagged string](https://www.w3.org/TR/rdf12-concepts/#dfn-dir-lang-string).
863    ///
864    /// It is the responsibility of the caller to check that `language`
865    /// is valid [BCP47](https://tools.ietf.org/html/bcp47) language tag,
866    /// and is lowercase.
867    ///
868    /// [`Literal::new_directional_language_tagged_literal()`] is a safe version of this constructor and should be used for untrusted data.
869    #[cfg(feature = "rdf-12")]
870    #[inline]
871    pub const fn new_directional_language_tagged_literal_unchecked(
872        value: &'a str,
873        language: &'a str,
874        direction: BaseDirection,
875    ) -> Self {
876        LiteralRef(LiteralRefContent::DirectionalLanguageTaggedString {
877            value,
878            language,
879            direction,
880        })
881    }
882
883    /// The literal [lexical form](https://www.w3.org/TR/rdf11-concepts/#dfn-lexical-form)
884    #[inline]
885    pub const fn value(self) -> &'a str {
886        match self.0 {
887            LiteralRefContent::String(value)
888            | LiteralRefContent::LanguageTaggedString { value, .. }
889            | LiteralRefContent::TypedLiteral { value, .. } => value,
890            #[cfg(feature = "rdf-12")]
891            LiteralRefContent::DirectionalLanguageTaggedString { value, .. } => value,
892        }
893    }
894
895    /// 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).
896    ///
897    /// Language tags are defined by the [BCP47](https://tools.ietf.org/html/bcp47).
898    /// They are normalized to lowercase by this implementation.
899    #[inline]
900    pub const fn language(self) -> Option<&'a str> {
901        match self.0 {
902            LiteralRefContent::LanguageTaggedString { language, .. } => Some(language),
903            #[cfg(feature = "rdf-12")]
904            LiteralRefContent::DirectionalLanguageTaggedString { language, .. } => Some(language),
905            _ => None,
906        }
907    }
908
909    /// 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).
910    ///
911    /// The two possible base directions are left-to-right (`ltr`) and right-to-left (`rtl`).
912    #[cfg(feature = "rdf-12")]
913    #[inline]
914    pub const fn direction(self) -> Option<BaseDirection> {
915        match self.0 {
916            LiteralRefContent::DirectionalLanguageTaggedString { direction, .. } => Some(direction),
917            _ => None,
918        }
919    }
920
921    /// The literal [datatype](https://www.w3.org/TR/rdf11-concepts/#dfn-datatype-iri).
922    ///
923    /// 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).
924    /// 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).
925    #[inline]
926    pub fn datatype(self) -> NamedNodeRef<'a> {
927        match self.0 {
928            LiteralRefContent::String(_) => xsd::STRING.as_ref(),
929            LiteralRefContent::LanguageTaggedString { .. } => rdf::LANG_STRING.as_ref(),
930            #[cfg(feature = "rdf-12")]
931            LiteralRefContent::DirectionalLanguageTaggedString { .. } => {
932                rdf::DIR_LANG_STRING.as_ref()
933            }
934            LiteralRefContent::TypedLiteral { datatype, .. } => datatype,
935        }
936    }
937
938    /// 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).
939    ///
940    /// It returns true if the literal is a [language-tagged string](https://www.w3.org/TR/rdf11-concepts/#dfn-language-tagged-string)
941    /// or has the datatype [xsd:string](https://www.w3.org/TR/xmlschema11-2/#string).
942    #[inline]
943    #[deprecated(note = "Plain literal concept is removed in RDF 1.1", since = "0.3.0")]
944    pub const fn is_plain(self) -> bool {
945        matches!(
946            self.0,
947            LiteralRefContent::String(_) | LiteralRefContent::LanguageTaggedString { .. }
948        )
949    }
950
951    #[inline]
952    pub fn into_owned(self) -> Literal {
953        Literal(match self.0 {
954            LiteralRefContent::String(value) => LiteralContent::String(value.to_owned()),
955            LiteralRefContent::LanguageTaggedString { value, language } => {
956                LiteralContent::LanguageTaggedString {
957                    value: value.to_owned(),
958                    language: language.to_owned(),
959                }
960            }
961            #[cfg(feature = "rdf-12")]
962            LiteralRefContent::DirectionalLanguageTaggedString {
963                value,
964                language,
965                direction,
966            } => LiteralContent::DirectionalLanguageTaggedString {
967                value: value.to_owned(),
968                language: language.to_owned(),
969                direction,
970            },
971            LiteralRefContent::TypedLiteral { value, datatype } => LiteralContent::TypedLiteral {
972                value: value.to_owned(),
973                datatype: datatype.into_owned(),
974            },
975        })
976    }
977
978    /// Converts to an owned Literal (alias for compatibility)
979    #[inline]
980    pub fn to_owned(&self) -> Literal {
981        self.into_owned()
982    }
983}
984
985impl fmt::Display for LiteralRef<'_> {
986    #[inline]
987    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
988        match self.0 {
989            LiteralRefContent::String(value) => print_quoted_str(value, f),
990            LiteralRefContent::LanguageTaggedString { value, language } => {
991                print_quoted_str(value, f)?;
992                write!(f, "@{language}")
993            }
994            #[cfg(feature = "rdf-12")]
995            LiteralRefContent::DirectionalLanguageTaggedString {
996                value,
997                language,
998                direction,
999            } => {
1000                print_quoted_str(value, f)?;
1001                write!(f, "@{language}--{direction}")
1002            }
1003            LiteralRefContent::TypedLiteral { value, datatype } => {
1004                print_quoted_str(value, f)?;
1005                write!(f, "^^{datatype}")
1006            }
1007        }
1008    }
1009}
1010
1011impl<'a> RdfTerm for LiteralRef<'a> {
1012    fn as_str(&self) -> &str {
1013        self.value()
1014    }
1015
1016    fn is_literal(&self) -> bool {
1017        true
1018    }
1019}
1020
1021/// Helper function to print a quoted string with proper escaping
1022#[inline]
1023pub fn print_quoted_str(string: &str, f: &mut impl Write) -> fmt::Result {
1024    f.write_char('"')?;
1025    for c in string.chars() {
1026        match c {
1027            '\u{08}' => f.write_str("\\b"),
1028            '\t' => f.write_str("\\t"),
1029            '\n' => f.write_str("\\n"),
1030            '\u{0C}' => f.write_str("\\f"),
1031            '\r' => f.write_str("\\r"),
1032            '"' => f.write_str("\\\""),
1033            '\\' => f.write_str("\\\\"),
1034            '\0'..='\u{1F}' | '\u{7F}' => write!(f, "\\u{:04X}", u32::from(c)),
1035            _ => f.write_char(c),
1036        }?;
1037    }
1038    f.write_char('"')
1039}
1040
1041/// 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)
1042#[cfg(feature = "rdf-12")]
1043#[derive(Eq, PartialEq, Debug, Clone, Copy, Hash, PartialOrd, Ord)]
1044#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1045pub enum BaseDirection {
1046    /// the initial text direction is set to left-to-right
1047    Ltr,
1048    /// the initial text direction is set to right-to-left
1049    Rtl,
1050}
1051
1052#[cfg(feature = "rdf-12")]
1053impl fmt::Display for BaseDirection {
1054    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1055        f.write_str(match self {
1056            Self::Ltr => "ltr",
1057            Self::Rtl => "rtl",
1058        })
1059    }
1060}
1061
1062impl<'a> From<&'a Literal> for LiteralRef<'a> {
1063    #[inline]
1064    fn from(node: &'a Literal) -> Self {
1065        node.as_ref()
1066    }
1067}
1068
1069impl<'a> From<LiteralRef<'a>> for Literal {
1070    #[inline]
1071    fn from(node: LiteralRef<'a>) -> Self {
1072        node.into_owned()
1073    }
1074}
1075
1076impl<'a> From<&'a str> for LiteralRef<'a> {
1077    #[inline]
1078    fn from(value: &'a str) -> Self {
1079        LiteralRef(LiteralRefContent::String(value))
1080    }
1081}
1082
1083impl PartialEq<Literal> for LiteralRef<'_> {
1084    #[inline]
1085    fn eq(&self, other: &Literal) -> bool {
1086        *self == other.as_ref()
1087    }
1088}
1089
1090impl PartialEq<LiteralRef<'_>> for Literal {
1091    #[inline]
1092    fn eq(&self, other: &LiteralRef<'_>) -> bool {
1093        self.as_ref() == *other
1094    }
1095}
1096
1097// Implement standard From traits
1098impl<'a> From<&'a str> for Literal {
1099    #[inline]
1100    fn from(value: &'a str) -> Self {
1101        Self(LiteralContent::String(value.into()))
1102    }
1103}
1104
1105impl From<String> for Literal {
1106    #[inline]
1107    fn from(value: String) -> Self {
1108        Self(LiteralContent::String(value))
1109    }
1110}
1111
1112impl<'a> From<Cow<'a, str>> for Literal {
1113    #[inline]
1114    fn from(value: Cow<'a, str>) -> Self {
1115        Self(LiteralContent::String(value.into()))
1116    }
1117}
1118
1119impl From<bool> for Literal {
1120    #[inline]
1121    fn from(value: bool) -> Self {
1122        Self(LiteralContent::TypedLiteral {
1123            value: value.to_string(),
1124            datatype: xsd::BOOLEAN.clone(),
1125        })
1126    }
1127}
1128
1129impl From<i128> for Literal {
1130    #[inline]
1131    fn from(value: i128) -> Self {
1132        Self(LiteralContent::TypedLiteral {
1133            value: value.to_string(),
1134            datatype: xsd::INTEGER.clone(),
1135        })
1136    }
1137}
1138
1139impl From<i64> for Literal {
1140    #[inline]
1141    fn from(value: i64) -> Self {
1142        Self(LiteralContent::TypedLiteral {
1143            value: value.to_string(),
1144            datatype: xsd::INTEGER.clone(),
1145        })
1146    }
1147}
1148
1149impl From<i32> for Literal {
1150    #[inline]
1151    fn from(value: i32) -> Self {
1152        Self(LiteralContent::TypedLiteral {
1153            value: value.to_string(),
1154            datatype: xsd::INTEGER.clone(),
1155        })
1156    }
1157}
1158
1159impl From<i16> for Literal {
1160    #[inline]
1161    fn from(value: i16) -> Self {
1162        Self(LiteralContent::TypedLiteral {
1163            value: value.to_string(),
1164            datatype: xsd::INTEGER.clone(),
1165        })
1166    }
1167}
1168
1169impl From<u64> for Literal {
1170    #[inline]
1171    fn from(value: u64) -> Self {
1172        Self(LiteralContent::TypedLiteral {
1173            value: value.to_string(),
1174            datatype: xsd::INTEGER.clone(),
1175        })
1176    }
1177}
1178
1179impl From<u32> for Literal {
1180    #[inline]
1181    fn from(value: u32) -> Self {
1182        Self(LiteralContent::TypedLiteral {
1183            value: value.to_string(),
1184            datatype: xsd::INTEGER.clone(),
1185        })
1186    }
1187}
1188
1189impl From<u16> for Literal {
1190    #[inline]
1191    fn from(value: u16) -> Self {
1192        Self(LiteralContent::TypedLiteral {
1193            value: value.to_string(),
1194            datatype: xsd::INTEGER.clone(),
1195        })
1196    }
1197}
1198
1199impl From<f32> for Literal {
1200    #[inline]
1201    fn from(value: f32) -> Self {
1202        Self(LiteralContent::TypedLiteral {
1203            value: if value == f32::INFINITY {
1204                "INF".to_owned()
1205            } else if value == f32::NEG_INFINITY {
1206                "-INF".to_owned()
1207            } else {
1208                value.to_string()
1209            },
1210            datatype: xsd::FLOAT.clone(),
1211        })
1212    }
1213}
1214
1215impl From<f64> for Literal {
1216    #[inline]
1217    fn from(value: f64) -> Self {
1218        Self(LiteralContent::TypedLiteral {
1219            value: if value == f64::INFINITY {
1220                "INF".to_owned()
1221            } else if value == f64::NEG_INFINITY {
1222                "-INF".to_owned()
1223            } else {
1224                value.to_string()
1225            },
1226            datatype: xsd::DOUBLE.clone(),
1227        })
1228    }
1229}
1230
1231/// Common XSD datatypes as constants and convenience functions
1232pub mod xsd_literals {
1233    use super::*;
1234    use crate::vocab::xsd;
1235
1236    // Convenience functions for creating typed literals
1237
1238    /// Creates a boolean literal
1239    pub fn boolean_literal(value: bool) -> Literal {
1240        Literal::new_typed(value.to_string(), xsd::BOOLEAN.clone())
1241    }
1242
1243    /// Creates an integer literal
1244    pub fn integer_literal(value: i64) -> Literal {
1245        Literal::new_typed(value.to_string(), xsd::INTEGER.clone())
1246    }
1247
1248    /// Creates a decimal literal
1249    pub fn decimal_literal(value: f64) -> Literal {
1250        Literal::new_typed(value.to_string(), xsd::DECIMAL.clone())
1251    }
1252
1253    /// Creates a double literal
1254    pub fn double_literal(value: f64) -> Literal {
1255        Literal::new_typed(value.to_string(), xsd::DOUBLE.clone())
1256    }
1257
1258    /// Creates a string literal
1259    pub fn string_literal(value: &str) -> Literal {
1260        Literal::new_typed(value, xsd::STRING.clone())
1261    }
1262}
1263
1264#[cfg(test)]
1265mod tests {
1266    use super::*;
1267
1268    #[test]
1269    fn test_simple_literal_equality() {
1270        assert_eq!(
1271            Literal::new_simple_literal("foo"),
1272            Literal::new_typed_literal("foo", xsd::STRING.clone())
1273        );
1274        assert_eq!(
1275            Literal::new_simple_literal("foo"),
1276            LiteralRef::new_typed_literal("foo", xsd::STRING.as_ref())
1277        );
1278        assert_eq!(
1279            LiteralRef::new_simple_literal("foo"),
1280            Literal::new_typed_literal("foo", xsd::STRING.clone())
1281        );
1282        assert_eq!(
1283            LiteralRef::new_simple_literal("foo"),
1284            LiteralRef::new_typed_literal("foo", xsd::STRING.as_ref())
1285        );
1286    }
1287
1288    #[test]
1289    fn test_float_format() {
1290        assert_eq!("INF", Literal::from(f32::INFINITY).value());
1291        assert_eq!("INF", Literal::from(f64::INFINITY).value());
1292        assert_eq!("-INF", Literal::from(f32::NEG_INFINITY).value());
1293        assert_eq!("-INF", Literal::from(f64::NEG_INFINITY).value());
1294        assert_eq!("NaN", Literal::from(f32::NAN).value());
1295        assert_eq!("NaN", Literal::from(f64::NAN).value());
1296    }
1297
1298    #[test]
1299    fn test_plain_literal() {
1300        let literal = Literal::new("Hello");
1301        assert_eq!(literal.value(), "Hello");
1302        #[allow(deprecated)]
1303        {
1304            assert!(literal.is_plain());
1305        }
1306        assert!(!literal.is_lang_string());
1307        assert!(!literal.is_typed());
1308        assert_eq!(format!("{literal}"), "\"Hello\"");
1309    }
1310
1311    #[test]
1312    fn test_lang_literal() {
1313        let literal = Literal::new_lang("Hello", "en").expect("construction should succeed");
1314        assert_eq!(literal.value(), "Hello");
1315        assert_eq!(literal.language(), Some("en"));
1316        #[allow(deprecated)]
1317        {
1318            assert!(literal.is_plain());
1319        }
1320        assert!(literal.is_lang_string());
1321        assert!(!literal.is_typed());
1322        assert_eq!(format!("{literal}"), "\"Hello\"@en");
1323    }
1324
1325    #[test]
1326    fn test_typed_literal() {
1327        let literal = Literal::new_typed("42", xsd::INTEGER.clone());
1328        assert_eq!(literal.value(), "42");
1329        assert_eq!(
1330            literal.datatype().as_str(),
1331            "http://www.w3.org/2001/XMLSchema#integer"
1332        );
1333        #[allow(deprecated)]
1334        {
1335            assert!(!literal.is_plain());
1336        }
1337        assert!(!literal.is_lang_string());
1338        assert!(literal.is_typed());
1339        assert_eq!(
1340            format!("{literal}"),
1341            "\"42\"^^<http://www.w3.org/2001/XMLSchema#integer>"
1342        );
1343    }
1344
1345    #[test]
1346    fn test_literal_ref() {
1347        let literal_ref = LiteralRef::new("test");
1348        assert_eq!(literal_ref.value(), "test");
1349
1350        let owned = literal_ref.to_owned();
1351        assert_eq!(owned.value(), "test");
1352    }
1353
1354    #[test]
1355    fn test_boolean_extraction() {
1356        let bool_literal = xsd_literals::boolean_literal(true);
1357        assert!(bool_literal.is_boolean());
1358        assert_eq!(bool_literal.as_bool(), Some(true));
1359
1360        let false_literal = Literal::new_typed("false", xsd::BOOLEAN.clone());
1361        assert_eq!(false_literal.as_bool(), Some(false));
1362
1363        // Test string representations
1364        let true_str = Literal::new("true");
1365        assert_eq!(true_str.as_bool(), Some(true));
1366
1367        let false_str = Literal::new("0");
1368        assert_eq!(false_str.as_bool(), Some(false));
1369    }
1370
1371    #[test]
1372    fn test_numeric_extraction() {
1373        let int_literal = xsd_literals::integer_literal(42);
1374        assert!(int_literal.is_numeric());
1375        assert_eq!(int_literal.as_i64(), Some(42));
1376        assert_eq!(int_literal.as_i32(), Some(42));
1377        assert_eq!(int_literal.as_f64(), Some(42.0));
1378
1379        let decimal_literal = xsd_literals::decimal_literal(3.25);
1380        assert!(decimal_literal.is_numeric());
1381        assert_eq!(decimal_literal.as_f64(), Some(3.25));
1382        assert_eq!(decimal_literal.as_f32(), Some(3.25_f32));
1383
1384        // Test untyped numeric strings
1385        let untyped_num = Literal::new("123");
1386        assert!(untyped_num.is_numeric());
1387        assert_eq!(untyped_num.as_i64(), Some(123));
1388    }
1389
1390    #[test]
1391    fn test_canonical_form() {
1392        // Boolean canonicalization
1393        let bool_literal = Literal::new_typed("True", xsd::BOOLEAN.clone());
1394        let canonical = bool_literal.canonical_form();
1395        assert_eq!(canonical.value(), "true");
1396
1397        // Integer canonicalization
1398        let int_literal = Literal::new_typed("  42  ", xsd::INTEGER.clone());
1399        // Note: This would need actual whitespace trimming in canonical form
1400        // For now, just test that it returns a valid canonical form
1401        let canonical = int_literal.canonical_form();
1402        assert_eq!(
1403            canonical.datatype().as_str(),
1404            "http://www.w3.org/2001/XMLSchema#integer"
1405        );
1406
1407        // Decimal canonicalization
1408        let dec_literal = Literal::new_typed("3.140", xsd::DECIMAL.clone());
1409        let canonical = dec_literal.canonical_form();
1410        assert_eq!(canonical.value(), "3.14"); // Should remove trailing zeros
1411    }
1412
1413    #[test]
1414    fn test_xsd_convenience_functions() {
1415        // Test all the convenience functions work
1416        assert_eq!(xsd_literals::boolean_literal(true).value(), "true");
1417        assert_eq!(xsd_literals::integer_literal(123).value(), "123");
1418        assert_eq!(xsd_literals::decimal_literal(3.25).value(), "3.25");
1419        assert_eq!(xsd_literals::double_literal(2.71).value(), "2.71");
1420        assert_eq!(xsd_literals::string_literal("hello").value(), "hello");
1421
1422        // Test datatype assignments
1423        assert_eq!(
1424            xsd_literals::boolean_literal(true).datatype().as_str(),
1425            "http://www.w3.org/2001/XMLSchema#boolean"
1426        );
1427        assert_eq!(
1428            xsd_literals::integer_literal(123).datatype().as_str(),
1429            "http://www.w3.org/2001/XMLSchema#integer"
1430        );
1431    }
1432
1433    #[test]
1434    fn test_numeric_type_detection() {
1435        // Test various numeric types
1436        let int_lit = Literal::new_typed("42", xsd::INTEGER.clone());
1437        assert!(int_lit.is_numeric());
1438
1439        let float_lit = Literal::new_typed("3.14", xsd::FLOAT.clone());
1440        assert!(float_lit.is_numeric());
1441
1442        let double_lit = Literal::new_typed("2.71", xsd::DOUBLE.clone());
1443        assert!(double_lit.is_numeric());
1444
1445        // Non-numeric types
1446        let string_lit = Literal::new_typed("hello", xsd::STRING.clone());
1447        assert!(!string_lit.is_numeric());
1448
1449        let bool_lit = Literal::new_typed("true", xsd::BOOLEAN.clone());
1450        assert!(!bool_lit.is_numeric());
1451    }
1452}