Skip to main content

rdf_structural_model/
literal.rs

1//! Exact lexical literals and storage-independent literal metadata.
2
3use borrow_or_share::Bos;
4use iri_rfc3987::Iri;
5
6use crate::{
7    BaseDirection,
8    LanguageTag,
9    ModelError,
10};
11
12/// The RDF language-string datatype IRI.
13pub const RDF_LANG_STRING: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString";
14/// The RDF directional language-string datatype IRI.
15pub const RDF_DIR_LANG_STRING: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#dirLangString";
16/// The XML Schema string datatype IRI.
17pub const XSD_STRING: &str = "http://www.w3.org/2001/XMLSchema#string";
18
19/// Language and direction without imposing a storage representation.
20#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
21#[serde(
22    deny_unknown_fields,
23    bound(deserialize = "Text: serde::Deserialize<'de>")
24)]
25pub enum LiteralFacets<Text: AsRef<str> = String> {
26    /// Neither language nor direction.
27    None,
28    /// A well-formed language tag.
29    Language(LanguageTag<Text>),
30    /// A language tag and its base direction.
31    Directional {
32        /// The well-formed language tag.
33        language: LanguageTag<Text>,
34        /// The base presentation direction.
35        direction: BaseDirection,
36    },
37}
38
39/// Literal metadata independent of payload access and storage addresses.
40///
41/// Extended literals may carry language facets independently of datatype.
42/// [`Literal::try_new`] separately enforces the standard RDF combinations.
43#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
44#[serde(
45    deny_unknown_fields,
46    bound(
47        serialize = "Text: serde::Serialize",
48        deserialize = "Text: serde::Deserialize<'de>, Iri<Text>: serde::Deserialize<'de>"
49    )
50)]
51pub struct LiteralMetadata<Text: Bos<str> + AsRef<str> = String> {
52    datatype: Iri<Text>,
53    facets: LiteralFacets<Text>,
54}
55
56impl<Text: Bos<str> + AsRef<str>> LiteralMetadata<Text> {
57    /// Construct metadata, including explicitly extended datatype/facet
58    /// combinations.
59    #[must_use]
60    pub const fn new(datatype: Iri<Text>, facets: LiteralFacets<Text>) -> Self {
61        Self { datatype, facets }
62    }
63
64    /// The declared datatype IRI.
65    #[must_use]
66    pub const fn datatype(&self) -> &Iri<Text> {
67        &self.datatype
68    }
69
70    /// Language and direction independent of payload access.
71    #[must_use]
72    pub const fn facets(&self) -> &LiteralFacets<Text> {
73        &self.facets
74    }
75
76    fn validate_standard(&self) -> Result<(), ModelError> {
77        let valid = match &self.facets {
78            LiteralFacets::None => {
79                self.datatype.as_str() != RDF_LANG_STRING
80                    && self.datatype.as_str() != RDF_DIR_LANG_STRING
81            }
82            LiteralFacets::Language(_) => self.datatype.as_str() == RDF_LANG_STRING,
83            LiteralFacets::Directional { .. } => self.datatype.as_str() == RDF_DIR_LANG_STRING,
84        };
85        if !valid {
86            return Err(ModelError::InconsistentLiteralFacets);
87        }
88        Ok(())
89    }
90}
91
92/// A standard RDF literal with an exact UTF-8 lexical form.
93///
94/// Equality is RDF term equality, not datatype value equality. Ill-typed
95/// lexical forms are representable; evaluating datatype lexical spaces is a
96/// separate task.
97#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
98#[serde(
99    try_from = "LiteralInput<Text>",
100    bound(
101        serialize = "Text: serde::Serialize",
102        deserialize = "Text: serde::Deserialize<'de>, Iri<Text>: serde::Deserialize<'de>"
103    )
104)]
105pub struct Literal<Text: Bos<str> + AsRef<str> = String> {
106    lexical: Text,
107    metadata: LiteralMetadata<Text>,
108}
109
110impl<Text: Bos<str> + AsRef<str>> Literal<Text> {
111    /// Validate standard RDF datatype/facet combinations without evaluating
112    /// lexical values.
113    ///
114    /// # Errors
115    /// Returns [`ModelError::InconsistentLiteralFacets`] for inconsistent
116    /// metadata.
117    pub fn try_new(lexical: Text, metadata: LiteralMetadata<Text>) -> Result<Self, ModelError> {
118        metadata.validate_standard()?;
119        Ok(Self { lexical, metadata })
120    }
121
122    /// The exact lexical form, without value normalization.
123    #[must_use]
124    pub fn lexical(&self) -> &str {
125        self.lexical.as_ref()
126    }
127
128    /// Metadata accessible without copying the lexical payload.
129    #[must_use]
130    pub const fn metadata(&self) -> &LiteralMetadata<Text> {
131        &self.metadata
132    }
133}
134
135impl Literal<String> {
136    /// Construct a non-language typed literal.
137    ///
138    /// # Errors
139    /// Language datatypes require their corresponding facets and are rejected
140    /// here.
141    pub fn typed(lexical: impl Into<String>, datatype: Iri<String>) -> Result<Self, ModelError> {
142        Self::try_new(
143            lexical.into(),
144            LiteralMetadata::new(datatype, LiteralFacets::None),
145        )
146    }
147
148    /// Construct an XML Schema string literal.
149    #[must_use]
150    pub fn string(lexical: impl Into<String>) -> Self {
151        Self {
152            lexical: lexical.into(),
153            metadata: LiteralMetadata::new(Self::builtin(XSD_STRING), LiteralFacets::None),
154        }
155    }
156
157    /// Construct a language-tagged literal from a validated tag.
158    #[must_use]
159    pub fn language_tagged(lexical: impl Into<String>, language: LanguageTag) -> Self {
160        Self {
161            lexical: lexical.into(),
162            metadata: LiteralMetadata::new(
163                Self::builtin(RDF_LANG_STRING),
164                LiteralFacets::Language(language),
165            ),
166        }
167    }
168
169    /// Construct a directional language-tagged literal from a validated tag.
170    #[must_use]
171    pub fn directional(
172        lexical: impl Into<String>,
173        language: LanguageTag,
174        direction: BaseDirection,
175    ) -> Self {
176        Self {
177            lexical: lexical.into(),
178            metadata: LiteralMetadata::new(
179                Self::builtin(RDF_DIR_LANG_STRING),
180                LiteralFacets::Directional {
181                    language,
182                    direction,
183                },
184            ),
185        }
186    }
187
188    fn builtin(value: &'static str) -> Iri<String> {
189        Iri::parse(value)
190            .expect("built-in datatype IRI is valid")
191            .into()
192    }
193
194    /// Borrow the lexical form and metadata without copying their text.
195    #[must_use]
196    pub fn as_ref(&self) -> Literal<&str> {
197        Literal {
198            lexical: &self.lexical,
199            metadata: self.metadata.as_ref(),
200        }
201    }
202}
203
204impl LiteralMetadata<String> {
205    /// Borrow metadata without copying its text or accessing a payload.
206    #[must_use]
207    pub fn as_ref(&self) -> LiteralMetadata<&str> {
208        let facets = match &self.facets {
209            LiteralFacets::None => LiteralFacets::None,
210            LiteralFacets::Language(tag) => LiteralFacets::Language(tag.as_ref()),
211            LiteralFacets::Directional {
212                language,
213                direction,
214            } => LiteralFacets::Directional {
215                language: language.as_ref(),
216                direction: *direction,
217            },
218        };
219        LiteralMetadata::new(self.datatype.borrow(), facets)
220    }
221}
222
223#[derive(serde::Deserialize)]
224#[serde(
225    rename = "Literal",
226    deny_unknown_fields,
227    bound(deserialize = "Text: serde::Deserialize<'de>, Iri<Text>: serde::Deserialize<'de>")
228)]
229struct LiteralInput<Text: Bos<str> + AsRef<str>> {
230    lexical: Text,
231    metadata: LiteralMetadata<Text>,
232}
233
234impl<Text: Bos<str> + AsRef<str>> TryFrom<LiteralInput<Text>> for Literal<Text> {
235    type Error = ModelError;
236    fn try_from(input: LiteralInput<Text>) -> Result<Self, Self::Error> {
237        Self::try_new(input.lexical, input.metadata)
238    }
239}